Website Structure

This commit is contained in:
supalerk-ar66 2026-01-13 10:46:40 +07:00
parent 62812f2090
commit 71f0676a62
22365 changed files with 4265753 additions and 791 deletions

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Anthony Fu <https://github.com/antfu>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,223 @@
# eslint-flat-config-utils
[![npm version][npm-version-src]][npm-version-href]
[![npm downloads][npm-downloads-src]][npm-downloads-href]
[![bundle][bundle-src]][bundle-href]
[![JSDocs][jsdocs-src]][jsdocs-href]
[![License][license-src]][license-href]
Utils for managing and manipulating ESLint flat config arrays
[Documentation](https://jsr.io/@antfu/eslint-flat-config-utils/doc)
## Install
```bash
npm i eslint-flat-config-utils
```
## Utils
Most of the descriptions are written in JSDoc, you can find more details in the [documentation](https://jsr.io/@antfu/eslint-flat-config-utils/doc) via JSR.
Here listing a few highlighted ones:
### `concat`
Concatenate multiple ESLint flat configs into one, resolve the promises, and flatten the array.
```ts
// eslint.config.mjs
import { concat } from 'eslint-flat-config-utils'
export default concat(
{
plugins: {},
rules: {},
},
// It can also takes a array of configs:
[
{
plugins: {},
rules: {},
}
// ...
],
// Or promises:
Promise.resolve({
files: ['*.ts'],
rules: {},
})
// ...
)
```
### `composer`
Create a chainable composer that makes manipulating ESLint flat config easier.
It extends Promise, so that you can directly await or export it to `eslint.config.mjs`
```ts
// eslint.config.mjs
import { composer } from 'eslint-flat-config-utils'
export default composer(
{
plugins: {},
rules: {},
}
// ...some configs, accepts same arguments as `concat`
)
.append(
// appends more configs at the end, accepts same arguments as `concat`
)
.prepend(
// prepends more configs at the beginning, accepts same arguments as `concat`
)
.insertAfter(
'config-name', // specify the name of the target config, or index
// insert more configs after the target, accepts same arguments as `concat`
)
.renamePlugins({
// rename plugins
'old-name': 'new-name',
// for example, rename `n` from `eslint-plugin-n` to more a explicit prefix `node`
'n': 'node'
// applies to all plugins and rules in the configs
})
.override(
'config-name', // specify the name of the target config, or index
{
// merge with the target config
rules: {
'no-console': 'off'
},
}
)
// And you can directly return the composer object to `eslint.config.mjs`
```
##### `composer.renamePlugins`
This helper renames plugins in all configurations in the composer. It is useful when you want to enforce a plugin to a custom name:
```ts
const config = await composer([
{
plugins: {
n: pluginN,
},
rules: {
'n/foo': 'error',
}
}
])
.renamePlugins({
n: 'node'
})
// The final config will have `node/foo` rule instead of `n/foo`
```
##### `composer.removeRules`
This helper removes specified rules from all configurations in the composer. It is useful when you are certain that these rules are not needed in the final configuration. Unlike overriding with `off`, removed rules are not affected by priority considerations.
```ts
const config = await composer([
{
rules: {
'foo/bar': 'error',
'foo/baz': 'warn',
}
},
{
files: ['*.ts'],
rules: {
'foo/bar': 'off',
}
}
// ...
])
.removeRules(
'foo/bar',
'foo/baz',
)
// The final config will not have `foo/bar` and `foo/baz` rules at all
```
##### `composer.disableRulesFix`
This helper **hijack** plugins to make fixable rules non-fixable, useful when you want to disable auto-fixing for some rules but still keep them enabled.
For example, if we want the rule to error when we use `let` on a const, but we don't want auto-fix to change it to `const` automatically:
```ts
const config = await composer([
{
plugins: {
'unused-imports': pluginUnusedImports,
},
rules: {
'perfer-const': 'error',
'unused-imports/no-unused-imports': 'error',
}
}
])
.disableRulesFix(
[
'prefer-const',
'unused-imports/no-unused-imports',
],
{
// this is required only when patching core rules like `prefer-const` (rules without a plugin prefix)
builtinRules: () => import('eslint/use-at-your-own-risk').then(r => r.builtinRules),
},
)
```
> [!NOTE]
> This function **mutate** the plugin object which will affect all the references to the plugin object globally. The changes are not reversible in the current runtime.
### `extend`
Extend another flat config from a different root, and rewrite the glob paths accordingly:
```ts
import { extend } from 'eslint-flat-config-utils'
export default [
...await extend(
import('./sub-package/eslint.config.mjs'),
'./sub-package/'
)
]
```
## Sponsors
<p align="center">
<a href="https://cdn.jsdelivr.net/gh/antfu/static/sponsors.svg">
<img src='https://cdn.jsdelivr.net/gh/antfu/static/sponsors.svg'/>
</a>
</p>
## License
[MIT](./LICENSE) License © 2023-PRESENT [Anthony Fu](https://github.com/antfu)
<!-- Badges -->
[npm-version-src]: https://img.shields.io/npm/v/eslint-flat-config-utils?style=flat&colorA=080f12&colorB=1fa669
[npm-version-href]: https://npmjs.com/package/eslint-flat-config-utils
[npm-downloads-src]: https://img.shields.io/npm/dm/eslint-flat-config-utils?style=flat&colorA=080f12&colorB=1fa669
[npm-downloads-href]: https://npmjs.com/package/eslint-flat-config-utils
[bundle-src]: https://img.shields.io/bundlephobia/minzip/eslint-flat-config-utils?style=flat&colorA=080f12&colorB=1fa669&label=minzip
[bundle-href]: https://bundlephobia.com/result?p=eslint-flat-config-utils
[license-src]: https://img.shields.io/github/license/antfu/eslint-flat-config-utils.svg?style=flat&colorA=080f12&colorB=1fa669
[license-href]: https://github.com/antfu/eslint-flat-config-utils/blob/main/LICENSE
[jsdocs-src]: https://img.shields.io/badge/jsdocs-reference-080f12?style=flat&colorA=080f12&colorB=1fa669
[jsdocs-href]: https://www.jsdocs.io/package/eslint-flat-config-utils

View file

@ -0,0 +1,366 @@
import { Linter, Rule, ESLint } from 'eslint';
/**
* Alias to `Linter.Config`
*
* @deprecated
*/
interface FlatConfigItem extends Linter.Config {
}
/**
* A type that can be awaited. Promise<T> or T.
*/
type Awaitable<T> = T | Promise<T>;
/**
* A type that can be an array or a single item.
*/
type Arrayable<T> = T | T[];
/**
* Default config names map. Used for type augmentation.
*
* @example
* ```ts
* declare module 'eslint-flat-config-utils' {
* interface DefaultConfigNamesMap {
* 'my-custom-config': true
* }
* }
* ```
*/
interface DefaultConfigNamesMap {
}
interface Nothing {
}
/**
* type StringLiteralUnion<'foo'> = 'foo' | string
* This has auto completion whereas `'foo' | string` doesn't
* Adapted from https://github.com/microsoft/TypeScript/issues/29729
*/
type StringLiteralUnion<T extends U, U = string> = T | (U & Nothing);
type FilterType<T, F> = T extends F ? T : never;
type NullableObject<T> = {
[K in keyof T]?: T[K] | null | undefined;
};
type GetRuleRecordFromConfig<T> = T extends {
rules?: infer R;
} ? R : Linter.RulesRecord;
declare const DEFAULT_PLUGIN_CONFLICTS_ERROR = "Different instances of plugin \"{{pluginName}}\" found in multiple configs: {{configNames}}. It's likely you misconfigured the merge of these configs.";
interface DisableFixesOptions {
builtinRules?: Map<string, Rule.RuleModule> | (() => Awaitable<Map<string, Rule.RuleModule>>);
}
type PluginConflictsError<T extends Linter.Config = Linter.Config> = (pluginName: string, configs: T[]) => string;
/**
* Awaitable array of ESLint flat configs or a composer object.
*/
type ResolvableFlatConfig<T extends Linter.Config = Linter.Config> = Awaitable<Arrayable<(T | false | undefined | null)>> | Awaitable<(Linter.Config | false | undefined | null)[]> | FlatConfigComposer<any>;
/**
* Create a chainable composer object that makes manipulating ESLint flat config easier.
*
* It extends Promise, so that you can directly await or export it to `eslint.config.mjs`
*
* ```ts
* // eslint.config.mjs
* import { composer } from 'eslint-flat-config-utils'
*
* export default composer(
* {
* plugins: {},
* rules: {},
* }
* // ...some configs, accepts same arguments as `concat`
* )
* .append(
* // appends more configs at the end, accepts same arguments as `concat`
* )
* .prepend(
* // prepends more configs at the beginning, accepts same arguments as `concat`
* )
* .insertAfter(
* 'config-name', // specify the name of the target config, or index
* // insert more configs after the target, accepts same arguments as `concat`
* )
* .renamePlugins({
* // rename plugins
* 'old-name': 'new-name',
* // for example, rename `n` from `eslint-plugin-n` to more a explicit prefix `node`
* 'n': 'node'
* // applies to all plugins and rules in the configs
* })
* .override(
* 'config-name', // specify the name of the target config, or index
* {
* // merge with the target config
* rules: {
* 'no-console': 'off'
* },
* }
* )
*
* // And you an directly return the composer object to `eslint.config.mjs`
* ```
*/
declare function composer<T extends Linter.Config = Linter.Config, ConfigNames extends string = keyof DefaultConfigNamesMap>(...configs: ResolvableFlatConfig<Linter.Config extends T ? T : Linter.Config>[]): FlatConfigComposer<Linter.Config extends T ? T : Linter.Config, ConfigNames>;
/**
* The underlying impolementation of `composer()`.
*/
declare class FlatConfigComposer<T extends object = Linter.Config, ConfigNames extends string = keyof DefaultConfigNamesMap> extends Promise<T[]> {
private _operations;
private _operationsOverrides;
private _operationsResolved;
private _renames;
private _pluginsConflictsError;
constructor(...configs: ResolvableFlatConfig<T>[]);
/**
* Set plugin renames, like `n` -> `node`, `import-x` -> `import`, etc.
*
* This will runs after all config items are resolved. Applies to `plugins` and `rules`.
*/
renamePlugins(renames: Record<string, string>): this;
/**
* Append configs to the end of the current configs array.
*/
append(...items: ResolvableFlatConfig<T>[]): this;
/**
* Prepend configs to the beginning of the current configs array.
*/
prepend(...items: ResolvableFlatConfig<T>[]): this;
/**
* Insert configs before a specific config.
*/
insertBefore(nameOrIndex: StringLiteralUnion<ConfigNames, string | number>, ...items: ResolvableFlatConfig<T>[]): this;
/**
* Insert configs after a specific config.
*/
insertAfter(nameOrIndex: StringLiteralUnion<ConfigNames, string | number>, ...items: ResolvableFlatConfig<T>[]): this;
/**
* Provide overrides to a specific config.
*
* It will be merged with the original config, or provide a custom function to replace the config entirely.
*/
override(nameOrIndex: StringLiteralUnion<ConfigNames, string | number>, config: T | ((config: T) => Awaitable<T>)): this;
/**
* Provide overrides to multiple configs as an object map.
*
* Same as calling `override` multiple times.
*/
overrides(overrides: Partial<Record<StringLiteralUnion<ConfigNames, string | number>, T | ((config: T) => Awaitable<T>)>>): this;
/**
* Override rules and it's options in **all configs**.
*
* Pass `null` as the value to remove the rule.
*
* @example
* ```ts
* composer
* .overrideRules({
* 'no-console': 'off',
* 'no-unused-vars': ['error', { vars: 'all', args: 'after-used' }],
* // remove the rule from all configs
* 'no-undef': null,
* })
* ```
*/
overrideRules(rules: NullableObject<GetRuleRecordFromConfig<T>>): this;
/**
* Remove rules from **all configs**.
*
* @example
* ```ts
* composer
* .removeRules(
* 'no-console',
* 'no-unused-vars'
* )
* ```
*/
removeRules(...rules: StringLiteralUnion<FilterType<keyof GetRuleRecordFromConfig<T>, string>, string>[]): this;
/**
* Remove plugins by name and all the rules referenced by them.
*
* @example
* ```ts
* composer
* .removePlugins(
* 'node'
* )
* ```
*
* The `plugins: { node }` and `rules: { 'node/xxx': 'error' }` will be removed from all configs.
*/
removePlugins(...names: string[]): this;
/**
* Remove a specific config by name or index.
*/
remove(nameOrIndex: ConfigNames | string | number): this;
/**
* Replace a specific config by name or index.
*
* The original config will be removed and replaced with the new one.
*/
replace(nameOrIndex: StringLiteralUnion<ConfigNames, string | number>, ...items: ResolvableFlatConfig<T>[]): this;
/**
* Hijack into plugins to disable fixes for specific rules.
*
* Note this mutates the plugin object, use with caution.
*
* @example
* ```ts
* const config = await composer(...)
* .disableRulesFix([
* 'unused-imports/no-unused-imports',
* 'vitest/no-only-tests'
* ])
* ```
*/
disableRulesFix(ruleIds: string[], options?: DisableFixesOptions): this;
/**
* Set a custom warning message for plugins conflicts.
*
* The error message can be a string or a function that returns a string.
*
* Error message accepts template strings:
* - `{{pluginName}}`: the name of the plugin that has conflicts
* - `{{configName1}}`: the name of the first config that uses the plugin
* - `{{configName2}}`: the name of the second config that uses the plugin
* - `{{configNames}}`: a list of config names that uses the plugin
*
* When only one argument is provided, it will be used as the default error message.
*/
setPluginConflictsError(warning?: string | PluginConflictsError): this;
setPluginConflictsError(pluginName: string, warning: string | PluginConflictsError): this;
private _verifyPluginsConflicts;
/**
* Hook when all configs are resolved but before returning the final configs.
*
* You can modify the final configs here.
*/
onResolved(callback: (configs: T[]) => Awaitable<T[] | void>): this;
/**
* Clone the composer object.
*/
clone(): FlatConfigComposer<T>;
/**
* Resolve the pipeline and return the final configs.
*
* This returns a promise. Calling `.then()` has the same effect.
*/
toConfigs(): Promise<T[]>;
then(onFulfilled: (value: T[]) => any, onRejected?: (reason: any) => any): Promise<any>;
catch(onRejected: (reason: any) => any): Promise<any>;
finally(onFinally: () => any): Promise<T[]>;
}
/**
* @deprecated Renamed to `composer`.
*/
declare const pipe: typeof composer;
/**
* @deprecated Renamed to `FlatConfigComposer`.
*/
declare class FlatConfigPipeline<T extends object = Linter.Config, ConfigNames extends string = string> extends FlatConfigComposer<T, ConfigNames> {
}
/**
* Concat multiple flat configs into a single flat config array.
*
* It also resolves promises and flattens the result.
*
* @example
*
* ```ts
* import { concat } from 'eslint-flat-config-utils'
* import eslint from '@eslint/js'
* import stylistic from '@stylistic/eslint-plugin'
*
* export default concat(
* eslint,
* stylistic.configs.customize(),
* { rules: { 'no-console': 'off' } },
* // ...
* )
* ```
*/
declare function concat<T extends Linter.Config = Linter.Config>(...configs: Awaitable<T | T[]>[]): Promise<T[]>;
/**
* A function that returns the config as-is, useful for providing type hints.
*/
declare function defineFlatConfig<T extends Linter.Config = Linter.Config>(config: T): T;
/**
* Extend another flat configs and rename globs paths.
*
* @example
* ```ts
* import { extend } from 'eslint-flat-config-utils'
*
* export default [
* ...await extend(
* // configs to extend
* import('./other-configs/eslint.config.js').then(m => m.default),
* // relative directory path
* 'other-configs/',
* ),
* ]
* ```
*/
declare function extend(configs: Awaitable<Linter.Config[]>, relativePath: string): Promise<Linter.Config[]>;
/**
* Replace a rule in a plugin with given factory.
*/
declare function hijackPluginRule(plugin: ESLint.Plugin, name: string, factory: (rule: Rule.RuleModule) => Rule.RuleModule): ESLint.Plugin;
/**
* Hijack into a rule's `context.report` to disable fixes.
*/
declare function disableRuleFixes(rule: Rule.RuleModule): Rule.RuleModule;
/**
* Merge multiple flat configs into a single flat config.
*
* Note there is no guarantee that the result works the same as the original configs.
*/
declare function mergeConfigs<T extends Linter.Config = Linter.Config>(...configs: T[]): T;
declare function parseRuleId(ruleId: string): {
plugin: string | null;
rule: string;
};
/**
* Rename plugin prefixes in a rule object.
* Accepts a map of prefixes to rename.
*
* @example
* ```ts
* import { renamePluginsInRules } from 'eslint-flat-config-utils'
*
* export default [{
* rules: renamePluginsInRules(
* {
* '@typescript-eslint/indent': 'error'
* },
* { '@typescript-eslint': 'ts' }
* )
* }]
* ```
*/
declare function renamePluginsInRules(rules: Record<string, any>, map: Record<string, string>): Record<string, any>;
/**
* Rename plugin names a flat configs array
*
* @example
* ```ts
* import { renamePluginsInConfigs } from 'eslint-flat-config-utils'
* import someConfigs from './some-configs'
*
* export default renamePluginsInConfigs(someConfigs, {
* '@typescript-eslint': 'ts',
* 'import-x': 'import',
* })
* ```
*/
declare function renamePluginsInConfigs<T extends Linter.Config = Linter.Config>(configs: T[], map: Record<string, string>): T[];
export { DEFAULT_PLUGIN_CONFLICTS_ERROR, FlatConfigComposer, FlatConfigPipeline, composer, concat, defineFlatConfig, disableRuleFixes, extend, hijackPluginRule, mergeConfigs, parseRuleId, pipe, renamePluginsInConfigs, renamePluginsInRules };
export type { Arrayable, Awaitable, DefaultConfigNamesMap, DisableFixesOptions, FilterType, FlatConfigItem, GetRuleRecordFromConfig, NullableObject, PluginConflictsError, ResolvableFlatConfig, StringLiteralUnion };

View file

@ -0,0 +1,366 @@
import { Linter, Rule, ESLint } from 'eslint';
/**
* Alias to `Linter.Config`
*
* @deprecated
*/
interface FlatConfigItem extends Linter.Config {
}
/**
* A type that can be awaited. Promise<T> or T.
*/
type Awaitable<T> = T | Promise<T>;
/**
* A type that can be an array or a single item.
*/
type Arrayable<T> = T | T[];
/**
* Default config names map. Used for type augmentation.
*
* @example
* ```ts
* declare module 'eslint-flat-config-utils' {
* interface DefaultConfigNamesMap {
* 'my-custom-config': true
* }
* }
* ```
*/
interface DefaultConfigNamesMap {
}
interface Nothing {
}
/**
* type StringLiteralUnion<'foo'> = 'foo' | string
* This has auto completion whereas `'foo' | string` doesn't
* Adapted from https://github.com/microsoft/TypeScript/issues/29729
*/
type StringLiteralUnion<T extends U, U = string> = T | (U & Nothing);
type FilterType<T, F> = T extends F ? T : never;
type NullableObject<T> = {
[K in keyof T]?: T[K] | null | undefined;
};
type GetRuleRecordFromConfig<T> = T extends {
rules?: infer R;
} ? R : Linter.RulesRecord;
declare const DEFAULT_PLUGIN_CONFLICTS_ERROR = "Different instances of plugin \"{{pluginName}}\" found in multiple configs: {{configNames}}. It's likely you misconfigured the merge of these configs.";
interface DisableFixesOptions {
builtinRules?: Map<string, Rule.RuleModule> | (() => Awaitable<Map<string, Rule.RuleModule>>);
}
type PluginConflictsError<T extends Linter.Config = Linter.Config> = (pluginName: string, configs: T[]) => string;
/**
* Awaitable array of ESLint flat configs or a composer object.
*/
type ResolvableFlatConfig<T extends Linter.Config = Linter.Config> = Awaitable<Arrayable<(T | false | undefined | null)>> | Awaitable<(Linter.Config | false | undefined | null)[]> | FlatConfigComposer<any>;
/**
* Create a chainable composer object that makes manipulating ESLint flat config easier.
*
* It extends Promise, so that you can directly await or export it to `eslint.config.mjs`
*
* ```ts
* // eslint.config.mjs
* import { composer } from 'eslint-flat-config-utils'
*
* export default composer(
* {
* plugins: {},
* rules: {},
* }
* // ...some configs, accepts same arguments as `concat`
* )
* .append(
* // appends more configs at the end, accepts same arguments as `concat`
* )
* .prepend(
* // prepends more configs at the beginning, accepts same arguments as `concat`
* )
* .insertAfter(
* 'config-name', // specify the name of the target config, or index
* // insert more configs after the target, accepts same arguments as `concat`
* )
* .renamePlugins({
* // rename plugins
* 'old-name': 'new-name',
* // for example, rename `n` from `eslint-plugin-n` to more a explicit prefix `node`
* 'n': 'node'
* // applies to all plugins and rules in the configs
* })
* .override(
* 'config-name', // specify the name of the target config, or index
* {
* // merge with the target config
* rules: {
* 'no-console': 'off'
* },
* }
* )
*
* // And you an directly return the composer object to `eslint.config.mjs`
* ```
*/
declare function composer<T extends Linter.Config = Linter.Config, ConfigNames extends string = keyof DefaultConfigNamesMap>(...configs: ResolvableFlatConfig<Linter.Config extends T ? T : Linter.Config>[]): FlatConfigComposer<Linter.Config extends T ? T : Linter.Config, ConfigNames>;
/**
* The underlying impolementation of `composer()`.
*/
declare class FlatConfigComposer<T extends object = Linter.Config, ConfigNames extends string = keyof DefaultConfigNamesMap> extends Promise<T[]> {
private _operations;
private _operationsOverrides;
private _operationsResolved;
private _renames;
private _pluginsConflictsError;
constructor(...configs: ResolvableFlatConfig<T>[]);
/**
* Set plugin renames, like `n` -> `node`, `import-x` -> `import`, etc.
*
* This will runs after all config items are resolved. Applies to `plugins` and `rules`.
*/
renamePlugins(renames: Record<string, string>): this;
/**
* Append configs to the end of the current configs array.
*/
append(...items: ResolvableFlatConfig<T>[]): this;
/**
* Prepend configs to the beginning of the current configs array.
*/
prepend(...items: ResolvableFlatConfig<T>[]): this;
/**
* Insert configs before a specific config.
*/
insertBefore(nameOrIndex: StringLiteralUnion<ConfigNames, string | number>, ...items: ResolvableFlatConfig<T>[]): this;
/**
* Insert configs after a specific config.
*/
insertAfter(nameOrIndex: StringLiteralUnion<ConfigNames, string | number>, ...items: ResolvableFlatConfig<T>[]): this;
/**
* Provide overrides to a specific config.
*
* It will be merged with the original config, or provide a custom function to replace the config entirely.
*/
override(nameOrIndex: StringLiteralUnion<ConfigNames, string | number>, config: T | ((config: T) => Awaitable<T>)): this;
/**
* Provide overrides to multiple configs as an object map.
*
* Same as calling `override` multiple times.
*/
overrides(overrides: Partial<Record<StringLiteralUnion<ConfigNames, string | number>, T | ((config: T) => Awaitable<T>)>>): this;
/**
* Override rules and it's options in **all configs**.
*
* Pass `null` as the value to remove the rule.
*
* @example
* ```ts
* composer
* .overrideRules({
* 'no-console': 'off',
* 'no-unused-vars': ['error', { vars: 'all', args: 'after-used' }],
* // remove the rule from all configs
* 'no-undef': null,
* })
* ```
*/
overrideRules(rules: NullableObject<GetRuleRecordFromConfig<T>>): this;
/**
* Remove rules from **all configs**.
*
* @example
* ```ts
* composer
* .removeRules(
* 'no-console',
* 'no-unused-vars'
* )
* ```
*/
removeRules(...rules: StringLiteralUnion<FilterType<keyof GetRuleRecordFromConfig<T>, string>, string>[]): this;
/**
* Remove plugins by name and all the rules referenced by them.
*
* @example
* ```ts
* composer
* .removePlugins(
* 'node'
* )
* ```
*
* The `plugins: { node }` and `rules: { 'node/xxx': 'error' }` will be removed from all configs.
*/
removePlugins(...names: string[]): this;
/**
* Remove a specific config by name or index.
*/
remove(nameOrIndex: ConfigNames | string | number): this;
/**
* Replace a specific config by name or index.
*
* The original config will be removed and replaced with the new one.
*/
replace(nameOrIndex: StringLiteralUnion<ConfigNames, string | number>, ...items: ResolvableFlatConfig<T>[]): this;
/**
* Hijack into plugins to disable fixes for specific rules.
*
* Note this mutates the plugin object, use with caution.
*
* @example
* ```ts
* const config = await composer(...)
* .disableRulesFix([
* 'unused-imports/no-unused-imports',
* 'vitest/no-only-tests'
* ])
* ```
*/
disableRulesFix(ruleIds: string[], options?: DisableFixesOptions): this;
/**
* Set a custom warning message for plugins conflicts.
*
* The error message can be a string or a function that returns a string.
*
* Error message accepts template strings:
* - `{{pluginName}}`: the name of the plugin that has conflicts
* - `{{configName1}}`: the name of the first config that uses the plugin
* - `{{configName2}}`: the name of the second config that uses the plugin
* - `{{configNames}}`: a list of config names that uses the plugin
*
* When only one argument is provided, it will be used as the default error message.
*/
setPluginConflictsError(warning?: string | PluginConflictsError): this;
setPluginConflictsError(pluginName: string, warning: string | PluginConflictsError): this;
private _verifyPluginsConflicts;
/**
* Hook when all configs are resolved but before returning the final configs.
*
* You can modify the final configs here.
*/
onResolved(callback: (configs: T[]) => Awaitable<T[] | void>): this;
/**
* Clone the composer object.
*/
clone(): FlatConfigComposer<T>;
/**
* Resolve the pipeline and return the final configs.
*
* This returns a promise. Calling `.then()` has the same effect.
*/
toConfigs(): Promise<T[]>;
then(onFulfilled: (value: T[]) => any, onRejected?: (reason: any) => any): Promise<any>;
catch(onRejected: (reason: any) => any): Promise<any>;
finally(onFinally: () => any): Promise<T[]>;
}
/**
* @deprecated Renamed to `composer`.
*/
declare const pipe: typeof composer;
/**
* @deprecated Renamed to `FlatConfigComposer`.
*/
declare class FlatConfigPipeline<T extends object = Linter.Config, ConfigNames extends string = string> extends FlatConfigComposer<T, ConfigNames> {
}
/**
* Concat multiple flat configs into a single flat config array.
*
* It also resolves promises and flattens the result.
*
* @example
*
* ```ts
* import { concat } from 'eslint-flat-config-utils'
* import eslint from '@eslint/js'
* import stylistic from '@stylistic/eslint-plugin'
*
* export default concat(
* eslint,
* stylistic.configs.customize(),
* { rules: { 'no-console': 'off' } },
* // ...
* )
* ```
*/
declare function concat<T extends Linter.Config = Linter.Config>(...configs: Awaitable<T | T[]>[]): Promise<T[]>;
/**
* A function that returns the config as-is, useful for providing type hints.
*/
declare function defineFlatConfig<T extends Linter.Config = Linter.Config>(config: T): T;
/**
* Extend another flat configs and rename globs paths.
*
* @example
* ```ts
* import { extend } from 'eslint-flat-config-utils'
*
* export default [
* ...await extend(
* // configs to extend
* import('./other-configs/eslint.config.js').then(m => m.default),
* // relative directory path
* 'other-configs/',
* ),
* ]
* ```
*/
declare function extend(configs: Awaitable<Linter.Config[]>, relativePath: string): Promise<Linter.Config[]>;
/**
* Replace a rule in a plugin with given factory.
*/
declare function hijackPluginRule(plugin: ESLint.Plugin, name: string, factory: (rule: Rule.RuleModule) => Rule.RuleModule): ESLint.Plugin;
/**
* Hijack into a rule's `context.report` to disable fixes.
*/
declare function disableRuleFixes(rule: Rule.RuleModule): Rule.RuleModule;
/**
* Merge multiple flat configs into a single flat config.
*
* Note there is no guarantee that the result works the same as the original configs.
*/
declare function mergeConfigs<T extends Linter.Config = Linter.Config>(...configs: T[]): T;
declare function parseRuleId(ruleId: string): {
plugin: string | null;
rule: string;
};
/**
* Rename plugin prefixes in a rule object.
* Accepts a map of prefixes to rename.
*
* @example
* ```ts
* import { renamePluginsInRules } from 'eslint-flat-config-utils'
*
* export default [{
* rules: renamePluginsInRules(
* {
* '@typescript-eslint/indent': 'error'
* },
* { '@typescript-eslint': 'ts' }
* )
* }]
* ```
*/
declare function renamePluginsInRules(rules: Record<string, any>, map: Record<string, string>): Record<string, any>;
/**
* Rename plugin names a flat configs array
*
* @example
* ```ts
* import { renamePluginsInConfigs } from 'eslint-flat-config-utils'
* import someConfigs from './some-configs'
*
* export default renamePluginsInConfigs(someConfigs, {
* '@typescript-eslint': 'ts',
* 'import-x': 'import',
* })
* ```
*/
declare function renamePluginsInConfigs<T extends Linter.Config = Linter.Config>(configs: T[], map: Record<string, string>): T[];
export { DEFAULT_PLUGIN_CONFLICTS_ERROR, FlatConfigComposer, FlatConfigPipeline, composer, concat, defineFlatConfig, disableRuleFixes, extend, hijackPluginRule, mergeConfigs, parseRuleId, pipe, renamePluginsInConfigs, renamePluginsInRules };
export type { Arrayable, Awaitable, DefaultConfigNamesMap, DisableFixesOptions, FilterType, FlatConfigItem, GetRuleRecordFromConfig, NullableObject, PluginConflictsError, ResolvableFlatConfig, StringLiteralUnion };

View file

@ -0,0 +1,545 @@
function hijackPluginRule(plugin, name, factory) {
const original = plugin.rules?.[name];
if (!original) {
throw new Error(`Rule "${name}" not found in plugin "${plugin.meta?.name || plugin.name}"`);
}
const patched = factory(original);
if (patched !== plugin.rules[name])
plugin.rules[name] = patched;
return plugin;
}
const disabledRuleFixes = /* @__PURE__ */ new WeakSet();
function disableRuleFixes(rule) {
if (disabledRuleFixes.has(rule)) {
return rule;
}
const originalCreate = rule.create.bind(rule);
rule.create = (context) => {
const clonedContext = { ...context };
const proxiedContext = new Proxy(clonedContext, {
get(target, prop, receiver) {
if (prop === "report") {
return function(report) {
if (report.fix) {
delete report.fix;
}
return Reflect.get(context, prop, receiver)({
...report,
fix: void 0
});
};
}
return Reflect.get(context, prop, receiver);
},
set(target, prop, value, receiver) {
return Reflect.set(context, prop, value, receiver);
}
});
const proxy = originalCreate(proxiedContext);
return proxy;
};
disabledRuleFixes.add(rule);
return rule;
}
function mergeConfigs(...configs) {
const keys = new Set(configs.flatMap((i) => Object.keys(i)));
const merged = configs.reduce((acc, cur) => {
return {
...acc,
...cur,
files: [
...acc.files || [],
...cur.files || []
],
ignores: [
...acc.ignores || [],
...cur.ignores || []
],
plugins: {
...acc.plugins,
...cur.plugins
},
rules: {
...acc.rules,
...cur.rules
},
languageOptions: {
...acc.languageOptions,
...cur.languageOptions
},
linterOptions: {
...acc.linterOptions,
...cur.linterOptions
}
};
}, {});
for (const key of Object.keys(merged)) {
if (!keys.has(key))
delete merged[key];
}
return merged;
}
function parseRuleId(ruleId) {
let plugin;
let rule = ruleId;
if (ruleId.includes("/")) {
if (ruleId.startsWith("@")) {
plugin = ruleId.slice(0, ruleId.lastIndexOf("/"));
} else {
plugin = ruleId.slice(0, ruleId.indexOf("/"));
}
rule = ruleId.slice(plugin.length + 1);
} else {
plugin = null;
rule = ruleId;
}
return {
plugin,
rule
};
}
function renamePluginsInRules(rules, map) {
return Object.fromEntries(
Object.entries(rules).map(([key, value]) => {
for (const [from, to] of Object.entries(map)) {
if (key.startsWith(`${from}/`))
return [to + key.slice(from.length), value];
}
return [key, value];
})
);
}
function renamePluginsInConfigs(configs, map) {
return configs.map((i) => {
const clone = { ...i };
if (clone.rules)
clone.rules = renamePluginsInRules(clone.rules, map);
if (clone.plugins) {
clone.plugins = Object.fromEntries(
Object.entries(clone.plugins).map(([key, value]) => {
if (key in map)
return [map[key], value];
return [key, value];
})
);
}
return clone;
});
}
const DEFAULT_PLUGIN_CONFLICTS_ERROR = `Different instances of plugin "{{pluginName}}" found in multiple configs: {{configNames}}. It's likely you misconfigured the merge of these configs.`;
function composer(...configs) {
return new FlatConfigComposer(
...configs
);
}
class FlatConfigComposer extends Promise {
_operations = [];
_operationsOverrides = [];
_operationsResolved = [];
_renames = {};
_pluginsConflictsError = /* @__PURE__ */ new Map();
constructor(...configs) {
super(() => {
});
if (configs.length)
this.append(...configs);
}
/**
* Set plugin renames, like `n` -> `node`, `import-x` -> `import`, etc.
*
* This will runs after all config items are resolved. Applies to `plugins` and `rules`.
*/
renamePlugins(renames) {
Object.assign(this._renames, renames);
return this;
}
/**
* Append configs to the end of the current configs array.
*/
append(...items) {
const promise = Promise.all(items);
this._operations.push(async (configs) => {
const resolved = (await promise).flat().filter(Boolean);
return [...configs, ...resolved];
});
return this;
}
/**
* Prepend configs to the beginning of the current configs array.
*/
prepend(...items) {
const promise = Promise.all(items);
this._operations.push(async (configs) => {
const resolved = (await promise).flat().filter(Boolean);
return [...resolved, ...configs];
});
return this;
}
/**
* Insert configs before a specific config.
*/
insertBefore(nameOrIndex, ...items) {
const promise = Promise.all(items);
this._operations.push(async (configs) => {
const resolved = (await promise).flat().filter(Boolean);
const index = getConfigIndex(configs, nameOrIndex);
configs.splice(index, 0, ...resolved);
return configs;
});
return this;
}
/**
* Insert configs after a specific config.
*/
insertAfter(nameOrIndex, ...items) {
const promise = Promise.all(items);
this._operations.push(async (configs) => {
const resolved = (await promise).flat().filter(Boolean);
const index = getConfigIndex(configs, nameOrIndex);
configs.splice(index + 1, 0, ...resolved);
return configs;
});
return this;
}
/**
* Provide overrides to a specific config.
*
* It will be merged with the original config, or provide a custom function to replace the config entirely.
*/
override(nameOrIndex, config) {
this._operationsOverrides.push(async (configs) => {
const index = getConfigIndex(configs, nameOrIndex);
const extended = typeof config === "function" ? await config(configs[index]) : mergeConfigs(configs[index], config);
configs.splice(index, 1, extended);
return configs;
});
return this;
}
/**
* Provide overrides to multiple configs as an object map.
*
* Same as calling `override` multiple times.
*/
overrides(overrides) {
for (const [name, config] of Object.entries(overrides)) {
if (config)
this.override(name, config);
}
return this;
}
/**
* Override rules and it's options in **all configs**.
*
* Pass `null` as the value to remove the rule.
*
* @example
* ```ts
* composer
* .overrideRules({
* 'no-console': 'off',
* 'no-unused-vars': ['error', { vars: 'all', args: 'after-used' }],
* // remove the rule from all configs
* 'no-undef': null,
* })
* ```
*/
overrideRules(rules) {
this._operationsOverrides.push(async (configs) => {
for (const config of configs) {
if (!("rules" in config) || !config.rules)
continue;
const configRules = config.rules;
for (const [key, value] of Object.entries(rules)) {
if (!(key in configRules))
continue;
if (value == null)
delete configRules[key];
else
configRules[key] = value;
}
}
return configs;
});
return this;
}
/**
* Remove rules from **all configs**.
*
* @example
* ```ts
* composer
* .removeRules(
* 'no-console',
* 'no-unused-vars'
* )
* ```
*/
removeRules(...rules) {
return this.overrideRules(Object.fromEntries(
rules.map((rule) => [rule, null])
));
}
/**
* Remove plugins by name and all the rules referenced by them.
*
* @example
* ```ts
* composer
* .removePlugins(
* 'node'
* )
* ```
*
* The `plugins: { node }` and `rules: { 'node/xxx': 'error' }` will be removed from all configs.
*/
removePlugins(...names) {
this._operationsOverrides.push(async (configs) => {
for (const config of configs) {
if ("plugins" in config && typeof config.plugins === "object" && config.plugins) {
for (const name of names) {
if (name in config.plugins)
delete config.plugins[name];
}
}
if ("rules" in config && typeof config.rules === "object" && config.rules) {
for (const key of Object.keys(config.rules)) {
if (names.some((n) => key.startsWith(`${n}/`)))
delete config.rules[key];
}
}
}
return configs;
});
return this;
}
/**
* Remove a specific config by name or index.
*/
remove(nameOrIndex) {
this._operations.push(async (configs) => {
const index = getConfigIndex(configs, nameOrIndex);
configs.splice(index, 1);
return configs;
});
return this;
}
/**
* Replace a specific config by name or index.
*
* The original config will be removed and replaced with the new one.
*/
replace(nameOrIndex, ...items) {
const promise = Promise.all(items);
this._operations.push(async (configs) => {
const resolved = (await promise).flat().filter(Boolean);
const index = getConfigIndex(configs, nameOrIndex);
configs.splice(index, 1, ...resolved);
return configs;
});
return this;
}
/**
* Hijack into plugins to disable fixes for specific rules.
*
* Note this mutates the plugin object, use with caution.
*
* @example
* ```ts
* const config = await composer(...)
* .disableRulesFix([
* 'unused-imports/no-unused-imports',
* 'vitest/no-only-tests'
* ])
* ```
*/
disableRulesFix(ruleIds, options = {}) {
this._operations.push(async (configs) => {
for (const name of ruleIds) {
const parsed = parseRuleId(name);
if (!parsed.plugin) {
if (!options.builtinRules)
throw new Error(`Patching core rule "${name}" require pass \`{ builtinRules: () => import('eslint/use-at-your-own-risk').then(r => r.builtinRules) }\` in the options`);
const builtinRules = typeof options.builtinRules === "function" ? await options.builtinRules() : options.builtinRules;
const rule = builtinRules.get(name);
if (!rule)
throw new Error(`Rule "${name}" not found in core rules`);
disableRuleFixes(rule);
} else {
const plugins = new Set(configs.map((c) => c.plugins?.[parsed.plugin]).filter((x) => !!x));
for (const plugin of plugins) {
hijackPluginRule(plugin, parsed.rule, (rule) => disableRuleFixes(rule));
}
}
}
return configs;
});
return this;
}
setPluginConflictsError(arg1 = DEFAULT_PLUGIN_CONFLICTS_ERROR, arg2) {
if (arg2 != null)
this._pluginsConflictsError.set(arg1, arg2);
else
this._pluginsConflictsError.set("*", arg1);
return this;
}
_verifyPluginsConflicts(configs) {
if (!this._pluginsConflictsError.size)
return;
const plugins = /* @__PURE__ */ new Map();
const names = /* @__PURE__ */ new Set();
for (const config of configs) {
if (!config.plugins)
continue;
for (const [name, plugin] of Object.entries(config.plugins)) {
names.add(name);
if (!plugins.has(plugin))
plugins.set(plugin, { name, configs: [] });
plugins.get(plugin).configs.push(config);
}
}
function getConfigName(config) {
return config.name || `#${configs.indexOf(config)}`;
}
const errors = [];
for (const name of names) {
const instancesOfName = [...plugins.values()].filter((p) => p.name === name);
if (instancesOfName.length <= 1)
continue;
const configsOfName = instancesOfName.map((p) => p.configs[0]);
const message = this._pluginsConflictsError.get(name) || this._pluginsConflictsError.get("*");
if (typeof message === "function") {
errors.push(message(name, configsOfName));
} else if (message) {
errors.push(
message.replace(/\{\{pluginName\}\}/g, name).replace(/\{\{configName1\}\}/g, getConfigName(configsOfName[0])).replace(/\{\{configName2\}\}/g, getConfigName(configsOfName[1])).replace(/\{\{configNames\}\}/g, configsOfName.map(getConfigName).join(", "))
);
}
}
if (errors.length) {
if (errors.length === 1)
throw new Error(`ESLintFlatConfigUtils: ${errors[0]}`);
else
throw new Error(`ESLintFlatConfigUtils:
${errors.map((e, i) => ` ${i + 1}: ${e}`).join("\n")}`);
}
}
/**
* Hook when all configs are resolved but before returning the final configs.
*
* You can modify the final configs here.
*/
onResolved(callback) {
this._operationsResolved.push(callback);
return this;
}
/**
* Clone the composer object.
*/
clone() {
const composer2 = new FlatConfigComposer();
composer2._operations = this._operations.slice();
composer2._operationsOverrides = this._operationsOverrides.slice();
composer2._operationsResolved = this._operationsResolved.slice();
composer2._renames = { ...this._renames };
composer2._pluginsConflictsError = new Map(this._pluginsConflictsError);
return composer2;
}
/**
* Resolve the pipeline and return the final configs.
*
* This returns a promise. Calling `.then()` has the same effect.
*/
async toConfigs() {
let configs = [];
for (const promise of this._operations)
configs = await promise(configs);
for (const promise of this._operationsOverrides)
configs = await promise(configs);
configs = renamePluginsInConfigs(configs, this._renames);
for (const promise of this._operationsResolved)
configs = await promise(configs) || configs;
this._verifyPluginsConflicts(configs);
return configs;
}
// eslint-disable-next-line ts/explicit-function-return-type
then(onFulfilled, onRejected) {
return this.toConfigs().then(onFulfilled, onRejected);
}
// eslint-disable-next-line ts/explicit-function-return-type
catch(onRejected) {
return this.toConfigs().catch(onRejected);
}
// eslint-disable-next-line ts/explicit-function-return-type
finally(onFinally) {
return this.toConfigs().finally(onFinally);
}
}
function getConfigIndex(configs, nameOrIndex) {
if (typeof nameOrIndex === "number") {
if (nameOrIndex < 0 || nameOrIndex >= configs.length)
throw new Error(`ESLintFlatConfigUtils: Failed to locate config at index ${nameOrIndex}
(${configs.length} configs in total)`);
return nameOrIndex;
} else {
const index = configs.findIndex((config) => config.name === nameOrIndex);
if (index === -1) {
const named = configs.map((config) => config.name).filter(Boolean);
const countUnnamed = configs.length - named.length;
const messages = [
`Failed to locate config with name "${nameOrIndex}"`,
`Available names are: ${named.join(", ")}`,
countUnnamed ? `(${countUnnamed} unnamed configs)` : ""
].filter(Boolean).join("\n");
throw new Error(`ESLintFlatConfigUtils: ${messages}`);
}
return index;
}
}
const pipe = composer;
class FlatConfigPipeline extends FlatConfigComposer {
}
async function concat(...configs) {
const resolved = await Promise.all(configs);
return resolved.flat();
}
function defineFlatConfig(config) {
return config;
}
async function extend(configs, relativePath) {
const { join } = await import('pathe');
const resolved = await configs;
if (relativePath === "")
return resolved;
function renameGlobs(i) {
if (typeof i !== "string")
return i;
if (i.startsWith("!"))
return `!${join(relativePath, i.slice(1))}`;
return join(relativePath, i);
}
return resolved.map((i) => {
if (!i || !i.files && !i.ignores)
return i;
const clone = { ...i };
if (clone.files) {
clone.files = clone.files.map(
(f) => Array.isArray(f) ? f.map((t) => renameGlobs(t)) : renameGlobs(f)
);
}
if (clone.ignores) {
clone.ignores = clone.ignores.map(
(f) => renameGlobs(f)
);
}
return clone;
});
}
export { DEFAULT_PLUGIN_CONFLICTS_ERROR, FlatConfigComposer, FlatConfigPipeline, composer, concat, defineFlatConfig, disableRuleFixes, extend, hijackPluginRule, mergeConfigs, parseRuleId, pipe, renamePluginsInConfigs, renamePluginsInRules };

View file

@ -0,0 +1,77 @@
{
"name": "eslint-flat-config-utils",
"type": "module",
"version": "2.1.4",
"description": "Utils for managing and manipulating ESLint flat config arrays",
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
"license": "MIT",
"funding": "https://github.com/sponsors/antfu",
"homepage": "https://github.com/antfu/eslint-flat-config-utils#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/antfu/eslint-flat-config-utils.git"
},
"bugs": "https://github.com/antfu/eslint-flat-config-utils/issues",
"keywords": [
"eslint",
"eslint-flat-config"
],
"sideEffects": false,
"exports": {
".": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
}
},
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.mts",
"typesVersions": {
"*": {
"*": [
"./dist/*",
"./dist/index.d.ts"
]
}
},
"files": [
"dist"
],
"dependencies": {
"pathe": "^2.0.3"
},
"devDependencies": {
"@antfu/eslint-config": "^5.4.1",
"@antfu/ni": "^26.0.1",
"@antfu/utils": "^9.2.1",
"@types/node": "^24.5.2",
"bumpp": "^10.2.3",
"eslint": "^9.36.0",
"eslint-plugin-unused-imports": "^4.2.0",
"jsr": "^0.13.5",
"lint-staged": "^16.1.6",
"pnpm": "^10.17.0",
"rimraf": "^6.0.1",
"simple-git-hooks": "^2.13.1",
"tsx": "^4.20.5",
"typescript": "^5.9.2",
"unbuild": "^3.6.1",
"vite": "^7.1.6",
"vitest": "^3.2.4"
},
"simple-git-hooks": {
"pre-commit": "pnpm lint-staged"
},
"lint-staged": {
"*": "eslint --fix"
},
"scripts": {
"build": "unbuild",
"dev": "unbuild --stub",
"lint": "eslint .",
"release": "bumpp && npx jsr publish --allow-slow-types",
"start": "tsx src/index.ts",
"test": "vitest",
"typecheck": "tsc --noEmit"
}
}