Skip to content

Commit

Permalink
Add support for eslint v9 config files + settings
Browse files Browse the repository at this point in the history
  • Loading branch information
webpro committed Feb 25, 2025
1 parent ee2c94d commit f55dc1b
Show file tree
Hide file tree
Showing 17 changed files with 184 additions and 21 deletions.
19 changes: 19 additions & 0 deletions packages/knip/fixtures/plugins/eslint2/eslint.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';

export default [
...tseslint.config(eslint.configs.recommended, tseslint.configs.recommended),
{
files: ['**/*.{ts,tsx}'],
settings: {
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx'],
},
'import/resolver': {
typescript: true,
node: true,
},
},
rules: {},
},
];
3 changes: 3 additions & 0 deletions packages/knip/fixtures/plugins/eslint2/knip.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"eslint": ["eslint.config.ts"]
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions packages/knip/fixtures/plugins/eslint2/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "@fixtures/eslint2",
"scripts": {
"lint": "eslint"
},
"devDependencies": {
"@eslint/js": "*",
"eslint": "*",
"eslint-import-resolver-typescript": "*",
"typescript-eslint": "*"
}
}
35 changes: 26 additions & 9 deletions packages/knip/src/plugins/eslint/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,29 @@ import type { PluginOptions } from '../../types/config.js';
import { compact } from '../../util/array.js';
import { type ConfigInput, type Input, toConfig, toDeferResolve } from '../../util/input.js';
import { getPackageNameFromFilePath, getPackageNameFromModuleSpecifier } from '../../util/modules.js';
import { isAbsolute, isInternal } from '../../util/path.js';
import { extname, isAbsolute, isInternal } from '../../util/path.js';
import { getDependenciesFromConfig } from '../babel/index.js';
import type { ESLintConfig, OverrideConfig } from './types.js';
import type { ESLintConfig, ESLintConfigDeprecated, OverrideConfigDeprecated } from './types.js';

export const getDependencies = (
config: ESLintConfig | OverrideConfig,
export const getInputs = (
config: ESLintConfigDeprecated | OverrideConfigDeprecated | ESLintConfig,
options: PluginOptions
): (Input | ConfigInput)[] => {
const { configFileName } = options;

if (extname(configFileName) === '.json' || !/eslint\.config/.test(configFileName)) {
return getInputsDeprecated(config as ESLintConfigDeprecated | OverrideConfigDeprecated, options);
}

const dependencies = (config as ESLintConfig).flatMap(config =>
config.settings ? getDependenciesFromSettings(config.settings).filter(id => id !== '@typescript-eslint/parser') : []
);

return compact(dependencies).map(id => toDeferResolve(id));
};

const getInputsDeprecated = (
config: ESLintConfigDeprecated | OverrideConfigDeprecated,
options: PluginOptions
): (Input | ConfigInput)[] => {
const extendsSpecifiers = config.extends ? compact([config.extends].flat().map(resolveExtendSpecifier)) : [];
Expand All @@ -25,9 +42,9 @@ export const getDependencies = (
const settings = config.settings ? getDependenciesFromSettings(config.settings) : [];
// const rules = getDependenciesFromRules(config.rules); // TODO enable in next major? Unexpected/breaking in certain cases w/ eslint v8
const rules = getDependenciesFromRules({});
const overrides = config.overrides ? [config.overrides].flat().flatMap(d => getDependencies(d, options)) : [];
const x = compact([...extendsSpecifiers, ...plugins, parser, ...settings, ...rules]).map(toDeferResolve);
return [...extendConfigs, ...x, ...babelDependencies, ...overrides];
const overrides = config.overrides ? [config.overrides].flat().flatMap(d => getInputsDeprecated(d, options)) : [];
const deferred = compact([...extendsSpecifiers, ...plugins, parser, ...settings, ...rules]).map(toDeferResolve);
return [...extendConfigs, ...deferred, ...babelDependencies, ...overrides];
};

const isQualifiedSpecifier = (specifier: string) =>
Expand Down Expand Up @@ -59,12 +76,12 @@ const resolveExtendSpecifier = (specifier: string) => {
return resolveSpecifier(namespace, specifier);
};

const getDependenciesFromRules = (rules: ESLintConfig['rules'] = {}) =>
const getDependenciesFromRules = (rules: ESLintConfigDeprecated['rules'] = {}) =>
Object.keys(rules).flatMap(ruleKey =>
ruleKey.includes('/') ? [resolveSpecifier('eslint-plugin', ruleKey.split('/').slice(0, -1).join('/'))] : []
);

const getDependenciesFromSettings = (settings: ESLintConfig['settings'] = {}) => {
const getDependenciesFromSettings = (settings: ESLintConfigDeprecated['settings'] = {}) => {
return Object.entries(settings).flatMap(([settingKey, settings]) => {
if (settingKey === 'import/resolver') {
return (typeof settings === 'string' ? [settings] : Object.keys(settings))
Expand Down
28 changes: 23 additions & 5 deletions packages/knip/src/plugins/eslint/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { IsPluginEnabled, Plugin, ResolveConfig } from '../../types/config.js';
import { hasDependency } from '../../util/plugin.js';
import { getDependencies } from './helpers.js';
import type { ESLintConfig } from './types.js';
import { getInputs } from './helpers.js';
import type { ESLintConfigDeprecated } from './types.js';

// New: https://eslint.org/docs/latest/use/configure/configuration-files
// Old: https://eslint.org/docs/latest/use/configure/configuration-files-deprecated
Expand All @@ -24,10 +24,28 @@ const entry = ['eslint.config.{js,cjs,mjs,ts,cts,mts}'];

const config = ['.eslintrc', '.eslintrc.{js,json,cjs}', '.eslintrc.{yml,yaml}', 'package.json'];

const resolveConfig: ResolveConfig<ESLintConfig> = (localConfig, options) => getDependencies(localConfig, options);
const resolveConfig: ResolveConfig<ESLintConfigDeprecated> = (localConfig, options) => getInputs(localConfig, options);

const note = `For ESLint v8 users: if relying on [configuration cascading](https://eslint.org/docs/v8.x/use/configure/configuration-files#cascading-and-hierarchy),
consider using something like this:
const note = `### ESLint v9
Only regular \`import\` statements are considered by default.
The configuration object is not resolved to find dependencies for \`settings\` such as \`"eslint-import-resolver-typescript"\`.
To enable this, lift the \`entry\` to a \`config\` file like so:
\`\`\`json
{
"eslint": ["eslint.config.ts"]
}
\`\`\`
This is not enabled by default, since this exception may be thrown by a \`@rushstack/eslint-*\` package:
> \`Error: Failed to patch ESLint because the calling module was not recognized.\`
### ESLint v8
If relying on [configuration cascading](https://eslint.org/docs/v8.x/use/configure/configuration-files#cascading-and-hierarchy),
consider using an extended glob pattern like this:
\`\`\`json
{
Expand Down
8 changes: 5 additions & 3 deletions packages/knip/src/plugins/eslint/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ type BaseConfig = {
rules?: Rules;
};

export type OverrideConfig = BaseConfig & { files: string[]; overrides: OverrideConfig };
export type ESLintConfig = BaseConfig[];

export type ESLintConfig = BaseConfig & {
export type OverrideConfigDeprecated = BaseConfig & { files: string[]; overrides: OverrideConfigDeprecated };

export type ESLintConfigDeprecated = BaseConfig & {
env?: Record<string, boolean>;
overrides?: OverrideConfig[];
overrides?: OverrideConfigDeprecated[];
};
4 changes: 2 additions & 2 deletions packages/knip/src/plugins/xo/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { IsPluginEnabled, Plugin, ResolveConfig } from '../../types/config.js';
import { hasDependency } from '../../util/plugin.js';
import { getDependencies } from '../eslint/helpers.js';
import { getInputs } from '../eslint/helpers.js';
import type { XOConfig } from './types.js';

// https://github.com/xojs/xo#config
Expand All @@ -18,7 +18,7 @@ const config = ['package.json', '.xo-config', '.xo-config.{js,cjs,json}', 'xo.co
const entry = ['.xo-config.{js,cjs}', 'xo.config.{js,cjs}'];

const resolveConfig: ResolveConfig<XOConfig> = async (config, options) => {
const inputs = getDependencies(config, options);
const inputs = getInputs(config, options);
return [...inputs];
};

Expand Down
4 changes: 2 additions & 2 deletions packages/knip/src/plugins/xo/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ESLintConfig } from '../eslint/types.js';
import type { ESLintConfigDeprecated } from '../eslint/types.js';

export type XOConfig = ESLintConfig & {
export type XOConfig = ESLintConfigDeprecated & {
envs?: string[] | undefined;
globals?: string[] | undefined;
ignores?: string[] | undefined;
Expand Down
21 changes: 21 additions & 0 deletions packages/knip/test/plugins/eslint2.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { test } from 'bun:test';
import assert from 'node:assert/strict';
import { main } from '../../src/index.js';
import { resolve } from '../../src/util/path.js';
import baseArguments from '../helpers/baseArguments.js';
import baseCounters from '../helpers/baseCounters.js';

const cwd = resolve('fixtures/plugins/eslint2');

test('Find dependencies with the ESLint plugin (2)', async () => {
const { counters } = await main({
...baseArguments,
cwd,
});

assert.deepEqual(counters, {
...baseCounters,
processed: 1,
total: 1,
});
});

0 comments on commit f55dc1b

Please sign in to comment.