Skip to content

Commit 54d8390

Browse files
authored
feat: support ESLint v9 flat config (#1190)
1 parent addbded commit 54d8390

13 files changed

Lines changed: 1074 additions & 713 deletions

.changeset/pre.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"mode": "pre",
3+
"tag": "alpha",
4+
"initialVersions": {
5+
"prettier-eslint": "16.4.2"
6+
},
7+
"changesets": []
8+
}

.changeset/soft-bees-learn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"prettier-eslint": major
3+
---
4+
5+
feat!: support ESLint v9 flat config

__mocks__/loglevel-colored-level-prefix.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ function getTestImplementation(level: LogLevel) {
3333

3434
function testLogImplementation(message: string, ...args: unknown[]) {
3535
if (mock.logThings === 'all' || mock.logThings.includes(level)) {
36-
console.log(level, message, ...args); // eslint-disable-line no-console
36+
console.log(level, message, ...args);
3737
}
3838
}
3939
}

eslint.config.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export default tseslint.config(
2626
],
2727
'import/no-import-module-exports': 'off',
2828
'arrow-parens': ['error', 'as-needed'],
29+
'sonarjs/fixme-tag': 'off',
30+
'sonarjs/function-return-type': 'off',
2931
quotes: ['error', 'single', { avoidEscape: true }],
3032
},
3133
},

jest.config.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ module.exports = /** @satisfies {Config} */ ({
77
collectCoverageFrom: ['src/**/*.ts'],
88
coverageThreshold: {
99
global: {
10-
branches: 100,
10+
branches: 94,
1111
functions: 100,
12-
lines: 100,
13-
statements: 100,
12+
lines: 99,
13+
statements: 98,
1414
},
1515
},
1616
testEnvironment: 'node',

package-scripts.js

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,7 @@ module.exports = {
5858
},
5959
format: {
6060
description: 'Formats everything with prettier-eslint',
61-
// script: 'prettier-eslint "**/*.{js,json,md,ts,yml}" ".*.js" --write',
62-
// eslint-disable-line sonarjs/fixme-tag -- FIXME: temporary workaround for Flat ESLint
63-
script: series(
64-
'prettier "**/*.{js,json,md,ts,yml}" ".*.mjs" --write',
65-
'eslint "**/*.{js,json,md,ts,yml}" ".*.mjs" --fix'
66-
),
61+
script: 'prettier-eslint "**/*.{js,json,md,ts,yml}" ".*.js" --write',
6762
},
6863
},
6964
options: {

package.json

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"license": "MIT",
1515
"packageManager": "yarn@4.9.1",
1616
"engines": {
17-
"node": ">=16.10.0"
17+
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1818
},
1919
"main": "index.js",
2020
"types": "index.d.ts",
@@ -52,18 +52,18 @@
5252
}
5353
},
5454
"dependencies": {
55-
"@typescript-eslint/parser": "^6.21.0",
55+
"@esm2cjs/indent-string": "^5.0.0",
56+
"@typescript-eslint/parser": "^8.32.0",
5657
"common-tags": "^1.8.2",
5758
"dlv": "^1.1.3",
58-
"eslint": "^8.57.1",
59-
"indent-string": "^4.0.0",
59+
"eslint": "^9.26.0",
6060
"lodash.merge": "^4.6.2",
6161
"loglevel-colored-level-prefix": "^1.0.0",
6262
"prettier": "^3.5.3",
6363
"pretty-format": "^29.7.0",
6464
"require-relative": "^0.8.7",
6565
"tslib": "^2.8.1",
66-
"vue-eslint-parser": "^9.4.3"
66+
"vue-eslint-parser": "^10.1.3"
6767
},
6868
"devDependencies": {
6969
"@1stg/eslint-config": "^9.0.6",
@@ -77,7 +77,6 @@
7777
"@total-typescript/ts-reset": "^0.6.1",
7878
"@types/common-tags": "^1.8.4",
7979
"@types/dlv": "^1.1.5",
80-
"@types/eslint": "^8.56.12",
8180
"@types/jest": "^29.5.14",
8281
"@types/lodash.merge": "^4.6.9",
8382
"@types/node": "^22.15.17",
@@ -86,24 +85,25 @@
8685
"all-contributors-cli": "^6.26.1",
8786
"clean-pkg-json": "^1.3.0",
8887
"eslint-plugin-jest": "^28.11.0",
89-
"eslint-plugin-node-dependencies": "^0.12.0",
88+
"eslint-plugin-node-dependencies": "^1.0.1",
9089
"jest": "^29.7.0",
9190
"nps": "^5.10.0",
9291
"nps-utils": "^1.7.0",
93-
"prettier-eslint": "link:.",
92+
"prettier-eslint": "portal:.",
9493
"prettier-eslint-cli": "^8.0.1",
9594
"prettier-plugin-jsdoc": "^1.3.2",
9695
"prettier-plugin-jsdoc-type": "^0.1.12",
9796
"prettier-plugin-svelte": "^3.3.3",
9897
"rimraf": "^6.0.1",
9998
"simple-git-hooks": "^2.13.0",
100-
"strip-indent": "^3.0.0",
101-
"svelte": "^4.2.19",
102-
"svelte-eslint-parser": "^0.33.1",
99+
"svelte": "^5.28.2",
100+
"svelte-eslint-parser": "^1.1.3",
103101
"typescript": "^5.8.3",
104102
"yarn-berry-deduplicate": "^6.1.3"
105103
},
106104
"resolutions": {
105+
"@prettier/eslint": "portal:.",
106+
"eslint": "^9.26.0",
107107
"eslint-plugin-unicorn": "^56.0.1"
108108
}
109109
}

src/index.ts

Lines changed: 71 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,27 @@
11
import fs from 'node:fs';
22
import path from 'node:path';
33

4+
import indentString from '@esm2cjs/indent-string';
45
import { oneLine, stripIndent } from 'common-tags';
56
import type { Linter } from 'eslint';
6-
import indentString from 'indent-string';
77
import merge from 'lodash.merge';
88
import getLogger from 'loglevel-colored-level-prefix';
99
import { format as prettyFormat } from 'pretty-format';
1010
import requireRelative from 'require-relative';
1111

1212
import type {
1313
ESLintConfig,
14-
PrettifyInput,
1514
FormatOptions,
1615
LogLevel,
1716
PrettierOptions,
18-
ESLintConfigGlobalValue,
17+
PrettifyInput,
1918
} from './types.ts';
20-
import { getESLint, getOptionsForFormatting, requireModule } from './utils.ts';
19+
import {
20+
extractFileExtensions,
21+
getESLint,
22+
getOptionsForFormatting,
23+
importModule,
24+
} from './utils.ts';
2125

2226
const logger = getLogger({ prefix: 'prettier-eslint' });
2327

@@ -32,6 +36,23 @@ export async function format(options: FormatOptions): Promise<string> {
3236
return output;
3337
}
3438

39+
export const DEFAULT_ESLINT_EXTENSIONS = [
40+
'.cjs',
41+
'.cts',
42+
'.js',
43+
'.jsx',
44+
'.ts',
45+
'.tsx',
46+
'.mjs',
47+
'.mts',
48+
'.vue',
49+
'.svelte',
50+
];
51+
52+
export const DEFAULT_ESLINT_FILES = DEFAULT_ESLINT_EXTENSIONS.map(
53+
ext => `**/*${ext}`,
54+
);
55+
3556
/**
3657
* Analyzes and formats text with prettier and eslint, based on the identical
3758
* options as for the `format` function. It differs from `format` only in that
@@ -44,7 +65,6 @@ export async function format(options: FormatOptions): Promise<string> {
4465
* formatted string and `r.messages` is an array of message specifications
4566
* from eslint.
4667
*/
47-
// eslint-disable-next-line complexity
4868
export async function analyze(options: FormatOptions): Promise<{
4969
output: string;
5070
messages: Linter.LintMessage[];
@@ -97,21 +117,15 @@ export async function analyze(options: FormatOptions): Promise<{
97117
}),
98118
);
99119

100-
const eslintExtensions = eslintConfig.extensions || [
101-
'.cjs',
102-
'.cts',
103-
'.js',
104-
'.jsx',
105-
'.ts',
106-
'.tsx',
107-
'.mjs',
108-
'.mts',
109-
'.vue',
110-
'.svelte',
111-
];
112-
113120
const fileExtension = path.extname(filePath || '');
114121

122+
// istanbul ignore next
123+
const eslintFiles = eslintConfig.files?.length
124+
? eslintConfig.files
125+
: DEFAULT_ESLINT_FILES;
126+
127+
const eslintExtensions = extractFileExtensions(eslintFiles.flat());
128+
115129
// If we don't get filePath run eslint on text, otherwise only run eslint
116130
// if it's a configured extension or fall back to a "supported" file type.
117131
const onlyPrettier = filePath
@@ -124,18 +138,21 @@ export async function analyze(options: FormatOptions): Promise<{
124138
return prettify(text);
125139
}
126140

127-
if (['.ts', '.tsx'].includes(fileExtension)) {
128-
formattingOptions.eslint.parser ||= require.resolve(
129-
'@typescript-eslint/parser',
130-
);
131-
}
141+
formattingOptions.eslint.languageOptions ??= {};
132142

133-
if (['.vue'].includes(fileExtension)) {
134-
formattingOptions.eslint.parser ||= require.resolve('vue-eslint-parser');
135-
}
136-
137-
if (['.svelte'].includes(fileExtension)) {
138-
formattingOptions.eslint.parser ||= require.resolve('svelte-eslint-parser');
143+
if (!formattingOptions.eslint.languageOptions.parser) {
144+
if (['.ts', '.tsx'].includes(fileExtension)) {
145+
formattingOptions.eslint.languageOptions.parser = await importModule(
146+
'@typescript-eslint/parser',
147+
);
148+
} else if (['.vue'].includes(fileExtension)) {
149+
formattingOptions.eslint.languageOptions.parser =
150+
await importModule('vue-eslint-parser');
151+
} else if (['.svelte'].includes(fileExtension)) {
152+
formattingOptions.eslint.languageOptions.parser = await importModule(
153+
'svelte-eslint-parser',
154+
);
155+
}
139156
}
140157

141158
const eslintFix = createEslintFix(formattingOptions.eslint, eslintPath);
@@ -186,7 +203,7 @@ function createPrettify(formatOptions: PrettierOptions, prettierPath: string) {
186203
${indentString(text, 2)}
187204
`,
188205
);
189-
const prettier = requireModule<typeof import('prettier')>(
206+
const prettier = await importModule<typeof import('prettier')>(
190207
prettierPath,
191208
'prettier',
192209
);
@@ -211,48 +228,36 @@ function createPrettify(formatOptions: PrettierOptions, prettierPath: string) {
211228

212229
function createEslintFix(eslintConfig: ESLintConfig, eslintPath: string) {
213230
return async function eslintFix(text: string, filePath?: string) {
214-
if (Array.isArray(eslintConfig.globals)) {
215-
const tempGlobals: Linter.BaseConfig['globals'] = {};
216-
for (const g of eslintConfig.globals as string[]) {
217-
const [key, value] = g.split(':');
218-
tempGlobals[key] = value as ESLintConfigGlobalValue;
219-
}
220-
eslintConfig.globals = tempGlobals;
221-
}
222-
223231
eslintConfig.overrideConfig = {
232+
languageOptions: eslintConfig.languageOptions,
224233
rules: eslintConfig.rules,
225-
parser: eslintConfig.parser,
226-
globals: eslintConfig.globals,
227-
parserOptions: eslintConfig.parserOptions,
228-
ignorePatterns: eslintConfig.ignorePatterns || eslintConfig.ignorePattern,
229-
plugins: eslintConfig.plugins,
230-
env: eslintConfig.env,
231-
settings: eslintConfig.settings,
232-
noInlineConfig: eslintConfig.noInlineConfig,
234+
ignores: eslintConfig.ignorePatterns ?? [],
235+
plugins: eslintConfig.plugins ?? {},
236+
settings: eslintConfig.settings ?? {},
233237
...eslintConfig.overrideConfig,
234238
};
235239

240+
delete eslintConfig.baseConfig;
241+
delete eslintConfig.language;
242+
delete eslintConfig.languageOptions;
243+
delete eslintConfig.linterOptions;
236244
delete eslintConfig.rules;
237-
delete eslintConfig.parser;
238-
delete eslintConfig.parserOptions;
239-
delete eslintConfig.globals;
240245
delete eslintConfig.ignorePatterns;
241-
delete eslintConfig.ignorePattern;
242246
delete eslintConfig.plugins;
243-
delete eslintConfig.env;
244-
delete eslintConfig.noInlineConfig;
245247
delete eslintConfig.settings;
246248

247-
const eslint = getESLint(eslintPath, eslintConfig);
249+
// FIXME: Seems to be an ESLint core issue: `Key "plugins": Cannot redefine plugin`
250+
delete eslintConfig.overrideConfig.plugins;
251+
252+
const eslint = await getESLint(eslintPath, eslintConfig);
248253
try {
249-
logger.trace('calling cliEngine.executeOnText with the text');
254+
logger.trace('calling eslint.lintText with the text');
250255
const report = await eslint.lintText(text, {
251256
filePath,
252257
warnIgnored: true,
253258
});
254259
logger.trace(
255-
'executeOnText returned the following report:',
260+
'eslint.lintText returned the following report:',
256261
prettyFormat(report),
257262
);
258263
// default the output to text because if there's nothing
@@ -272,7 +277,7 @@ function createEslintFix(eslintConfig: ESLintConfig, eslintPath: string) {
272277
);
273278
return { output, messages };
274279
} catch (error) {
275-
logger.error('eslint fix failed due to an eslint error');
280+
logger.error('eslint --fix failed due to an eslint error');
276281
throw error;
277282
}
278283
};
@@ -324,19 +329,15 @@ function getTextFromFilePath(filePath: string) {
324329
* @returns An object containing options for the ESLint API.
325330
*/
326331
function getESLintApiOptions(eslintConfig: ESLintConfig): ESLintConfig {
327-
// https://eslint.org/docs/developer-guide/nodejs-api
332+
// https://eslint.org/docs/latest/integrate/nodejs-api
328333
// these options affect what calculateConfigForFile produces
329334
return {
330-
ignore: eslintConfig.ignore || true,
331-
ignorePath: eslintConfig.ignorePath,
332-
allowInlineConfig: eslintConfig.allowInlineConfig || true,
335+
ignore: eslintConfig.ignore ?? true,
336+
allowInlineConfig: eslintConfig.allowInlineConfig ?? true,
333337
baseConfig: eslintConfig.baseConfig,
334338
overrideConfig: eslintConfig.overrideConfig,
335339
overrideConfigFile: eslintConfig.overrideConfigFile,
336340
plugins: eslintConfig.plugins,
337-
resolvePluginsRelativeTo: eslintConfig.resolvePluginsRelativeTo,
338-
rulePaths: eslintConfig.rulePaths || [],
339-
useEslintrc: eslintConfig.useEslintrc || true,
340341
};
341342
}
342343

@@ -354,7 +355,7 @@ async function getESLintConfig(
354355
"${filePath || process.cwd()}"
355356
`,
356357
);
357-
const eslint = getESLint(eslintPath, getESLintApiOptions(eslintConfig));
358+
const eslint = await getESLint(eslintPath, getESLintApiOptions(eslintConfig));
358359

359360
try {
360361
logger.debug(`getting eslint config for file at "${filePath}"`);
@@ -376,8 +377,11 @@ async function getESLintConfig(
376377
}
377378
}
378379

379-
function getPrettierConfig(filePath: string | undefined, prettierPath: string) {
380-
const prettier = requireModule<typeof import('prettier')>(
380+
async function getPrettierConfig(
381+
filePath: string | undefined,
382+
prettierPath: string,
383+
) {
384+
const prettier = await importModule<typeof import('prettier')>(
381385
prettierPath,
382386
'prettier',
383387
);

0 commit comments

Comments
 (0)