feat(core): Add ESM support for Eslint config file (#29613)

This pull request includes changes to migrate ESLint configuration files
from CommonJS (`.cjs`) to ECMAScript modules (`.mjs`) as the default.

### ESLint Configuration Generation Changes

The changes also ensure consistent generated eslint configs based on the
base eslint config.
- If the workspace root has an `eslint.config.cjs` or `eslint.config.js`
with `module.exports`. When you create a library or application it will
generate an accompanying config at path
`{projectRoot}/eslint.config.cjs` of the same format.
- If the workspace root has an `eslint.config.mjs` or
`eslint.config.mjs` with `export default`. When you create a library or
application it will generate an accompanying config at path
`{projectRoot}/eslint.config.mjs`.
- If no eslint config is found at the workspace root one will be created
`eslint.config.mjs`
This commit is contained in:
Nicholas Cunningham 2025-01-17 11:39:45 -07:00 committed by GitHub
parent a468d72c7f
commit dec21662b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 4332 additions and 1312 deletions

View File

@ -39,7 +39,7 @@ describe('Move Angular Project', () => {
expect(moveOutput).toContain(`CREATE ${newPath}/tsconfig.app.json`); expect(moveOutput).toContain(`CREATE ${newPath}/tsconfig.app.json`);
expect(moveOutput).toContain(`CREATE ${newPath}/tsconfig.json`); expect(moveOutput).toContain(`CREATE ${newPath}/tsconfig.json`);
expect(moveOutput).toContain(`CREATE ${newPath}/tsconfig.spec.json`); expect(moveOutput).toContain(`CREATE ${newPath}/tsconfig.spec.json`);
expect(moveOutput).toContain(`CREATE ${newPath}/eslint.config.cjs`); expect(moveOutput).toContain(`CREATE ${newPath}/eslint.config.mjs`);
expect(moveOutput).toContain(`CREATE ${newPath}/public/favicon.ico`); expect(moveOutput).toContain(`CREATE ${newPath}/public/favicon.ico`);
expect(moveOutput).toContain(`CREATE ${newPath}/src/index.html`); expect(moveOutput).toContain(`CREATE ${newPath}/src/index.html`);
expect(moveOutput).toContain(`CREATE ${newPath}/src/main.ts`); expect(moveOutput).toContain(`CREATE ${newPath}/src/main.ts`);

View File

@ -164,13 +164,13 @@ describe('Angular Projects', () => {
it('should lint correctly with eslint and handle external HTML files and inline templates', async () => { it('should lint correctly with eslint and handle external HTML files and inline templates', async () => {
// disable the prefer-standalone rule for app1 which is not standalone // disable the prefer-standalone rule for app1 which is not standalone
let app1EslintConfig = readFile(`${app1}/eslint.config.cjs`); let app1EslintConfig = readFile(`${app1}/eslint.config.mjs`);
app1EslintConfig = app1EslintConfig.replace( app1EslintConfig = app1EslintConfig.replace(
`'@angular-eslint/directive-selector': [`, `'@angular-eslint/directive-selector': [`,
`'@angular-eslint/prefer-standalone': 'off', `'@angular-eslint/prefer-standalone': 'off',
'@angular-eslint/directive-selector': [` '@angular-eslint/directive-selector': [`
); );
updateFile(`${app1}/eslint.config.cjs`, app1EslintConfig); updateFile(`${app1}/eslint.config.mjs`, app1EslintConfig);
// check apps and lib pass linting for initial generated code // check apps and lib pass linting for initial generated code
runCLI(`run-many --target lint --projects=${app1},${lib1} --parallel`); runCLI(`run-many --target lint --projects=${app1},${lib1} --parallel`);

View File

@ -150,10 +150,10 @@ describe('Linter (legacy)', () => {
env: { NX_ADD_PLUGINS: 'false' }, env: { NX_ADD_PLUGINS: 'false' },
}); });
checkFilesExist( checkFilesExist(
'eslint.config.cjs', 'eslint.config.mjs',
`apps/${myapp}/eslint.config.cjs`, `apps/${myapp}/eslint.config.mjs`,
`libs/${mylib}/eslint.config.cjs`, `libs/${mylib}/eslint.config.mjs`,
`libs/${mylib2}/eslint.config.cjs` `libs/${mylib2}/eslint.config.mjs`
); );
checkFilesDoNotExist( checkFilesDoNotExist(
'.eslintrc.json', '.eslintrc.json',
@ -164,12 +164,12 @@ describe('Linter (legacy)', () => {
// move eslint.config one step up // move eslint.config one step up
// to test the absence of the flat eslint config in the project root folder // to test the absence of the flat eslint config in the project root folder
renameFile(`libs/${mylib2}/eslint.config.cjs`, `libs/eslint.config.cjs`); renameFile(`libs/${mylib2}/eslint.config.mjs`, `libs/eslint.config.mjs`);
updateFile( updateFile(
`libs/eslint.config.cjs`, `libs/eslint.config.mjs`,
readFile(`libs/eslint.config.cjs`).replace( readFile(`libs/eslint.config.mjs`).replace(
`../../eslint.config.cjs`, `../../eslint.config.mjs`,
`../eslint.config.cjs` `../eslint.config.mjs`
) )
); );
@ -202,9 +202,9 @@ describe('Linter (legacy)', () => {
env: { NX_ADD_PLUGINS: 'false' }, env: { NX_ADD_PLUGINS: 'false' },
}); });
checkFilesExist( checkFilesExist(
'eslint.config.cjs', 'eslint.config.mjs',
`${mylib}/eslint.config.cjs`, `${mylib}/eslint.config.mjs`,
'eslint.base.config.cjs' 'eslint.base.config.mjs'
); );
checkFilesDoNotExist( checkFilesDoNotExist(
'.eslintrc.json', '.eslintrc.json',

View File

@ -615,8 +615,8 @@ describe('Linter', () => {
runCLI(`generate @nx/js:lib ${jsLib} --linter eslint`); runCLI(`generate @nx/js:lib ${jsLib} --linter eslint`);
checkFilesExist( checkFilesExist(
`${reactLib}/eslint.config.cjs`, `${reactLib}/eslint.config.mjs`,
`${jsLib}/eslint.config.cjs` `${jsLib}/eslint.config.mjs`
); );
checkFilesDoNotExist( checkFilesDoNotExist(
`${reactLib}/.eslintrc.json`, `${reactLib}/.eslintrc.json`,

View File

@ -26,7 +26,7 @@ exports[`Extra Nx Misc Tests task graph inputs should correctly expand dependent
], ],
"lib-base-123": [ "lib-base-123": [
"libs/lib-base-123/README.md", "libs/lib-base-123/README.md",
"libs/lib-base-123/eslint.config.cjs", "libs/lib-base-123/eslint.config.mjs",
"libs/lib-base-123/jest.config.ts", "libs/lib-base-123/jest.config.ts",
"libs/lib-base-123/package.json", "libs/lib-base-123/package.json",
"libs/lib-base-123/project.json", "libs/lib-base-123/project.json",
@ -39,7 +39,7 @@ exports[`Extra Nx Misc Tests task graph inputs should correctly expand dependent
], ],
"lib-dependent-123": [ "lib-dependent-123": [
"libs/lib-dependent-123/README.md", "libs/lib-dependent-123/README.md",
"libs/lib-dependent-123/eslint.config.cjs", "libs/lib-dependent-123/eslint.config.mjs",
"libs/lib-dependent-123/jest.config.ts", "libs/lib-dependent-123/jest.config.ts",
"libs/lib-dependent-123/package.json", "libs/lib-dependent-123/package.json",
"libs/lib-dependent-123/project.json", "libs/lib-dependent-123/project.json",

View File

@ -94,6 +94,7 @@ exports[`workspace move to nx layout should create nx.json 1`] = `
"!{projectRoot}/karma.conf.js", "!{projectRoot}/karma.conf.js",
"!{projectRoot}/.eslintrc.json", "!{projectRoot}/.eslintrc.json",
"!{projectRoot}/eslint.config.cjs", "!{projectRoot}/eslint.config.cjs",
"!{projectRoot}/eslint.config.mjs",
], ],
"sharedGlobals": [], "sharedGlobals": [],
}, },
@ -104,7 +105,7 @@ exports[`workspace move to nx layout should create nx.json 1`] = `
"default", "default",
"{workspaceRoot}/.eslintrc.json", "{workspaceRoot}/.eslintrc.json",
"{workspaceRoot}/.eslintignore", "{workspaceRoot}/.eslintignore",
"{workspaceRoot}/eslint.config.cjs", "{workspaceRoot}/eslint.config.mjs",
], ],
}, },
"build": { "build": {

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`convert-to-flat-config generator should add env configuration 1`] = ` exports[`convert-to-flat-config generator CJS should add env configuration 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc'); "const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js'); const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin'); const nxEslintPlugin = require('@nx/eslint-plugin');
@ -61,7 +61,7 @@ module.exports = [
" "
`; `;
exports[`convert-to-flat-config generator should add global and env configuration 1`] = ` exports[`convert-to-flat-config generator CJS should add global and env configuration 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc'); "const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js'); const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin'); const nxEslintPlugin = require('@nx/eslint-plugin');
@ -126,7 +126,7 @@ module.exports = [
" "
`; `;
exports[`convert-to-flat-config generator should add global configuration 1`] = ` exports[`convert-to-flat-config generator CJS should add global configuration 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc'); "const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js'); const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin'); const nxEslintPlugin = require('@nx/eslint-plugin');
@ -186,7 +186,7 @@ module.exports = [
" "
`; `;
exports[`convert-to-flat-config generator should add global eslintignores 1`] = ` exports[`convert-to-flat-config generator CJS should add global eslintignores 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc'); "const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js'); const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin'); const nxEslintPlugin = require('@nx/eslint-plugin');
@ -248,7 +248,7 @@ module.exports = [
" "
`; `;
exports[`convert-to-flat-config generator should add parser 1`] = ` exports[`convert-to-flat-config generator CJS should add parser 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc'); "const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js'); const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin'); const nxEslintPlugin = require('@nx/eslint-plugin');
@ -309,7 +309,7 @@ module.exports = [
" "
`; `;
exports[`convert-to-flat-config generator should add plugins 1`] = ` exports[`convert-to-flat-config generator CJS should add plugins 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc'); "const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js'); const js = require('@eslint/js');
const eslintPluginImport = require('eslint-plugin-import'); const eslintPluginImport = require('eslint-plugin-import');
@ -378,7 +378,7 @@ module.exports = [
" "
`; `;
exports[`convert-to-flat-config generator should add settings 1`] = ` exports[`convert-to-flat-config generator CJS should add settings 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc'); "const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js'); const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin'); const nxEslintPlugin = require('@nx/eslint-plugin');
@ -442,7 +442,7 @@ module.exports = [
" "
`; `;
exports[`convert-to-flat-config generator should convert json successfully 1`] = ` exports[`convert-to-flat-config generator CJS should convert json successfully 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc'); "const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js'); const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin'); const nxEslintPlugin = require('@nx/eslint-plugin');
@ -501,7 +501,7 @@ module.exports = [
" "
`; `;
exports[`convert-to-flat-config generator should convert json successfully 2`] = ` exports[`convert-to-flat-config generator CJS should convert json successfully 2`] = `
"const baseConfig = require('../../eslint.config.cjs'); "const baseConfig = require('../../eslint.config.cjs');
module.exports = [ module.exports = [
@ -528,7 +528,7 @@ module.exports = [
" "
`; `;
exports[`convert-to-flat-config generator should convert yaml successfully 1`] = ` exports[`convert-to-flat-config generator CJS should convert yaml successfully 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc'); "const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js'); const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin'); const nxEslintPlugin = require('@nx/eslint-plugin');
@ -587,7 +587,7 @@ module.exports = [
" "
`; `;
exports[`convert-to-flat-config generator should convert yaml successfully 2`] = ` exports[`convert-to-flat-config generator CJS should convert yaml successfully 2`] = `
"const baseConfig = require('../../eslint.config.cjs'); "const baseConfig = require('../../eslint.config.cjs');
module.exports = [ module.exports = [
@ -614,7 +614,7 @@ module.exports = [
" "
`; `;
exports[`convert-to-flat-config generator should convert yml successfully 1`] = ` exports[`convert-to-flat-config generator CJS should convert yml successfully 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc'); "const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js'); const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin'); const nxEslintPlugin = require('@nx/eslint-plugin');
@ -673,7 +673,7 @@ module.exports = [
" "
`; `;
exports[`convert-to-flat-config generator should convert yml successfully 2`] = ` exports[`convert-to-flat-config generator CJS should convert yml successfully 2`] = `
"const baseConfig = require('../../eslint.config.cjs'); "const baseConfig = require('../../eslint.config.cjs');
module.exports = [ module.exports = [
@ -700,7 +700,7 @@ module.exports = [
" "
`; `;
exports[`convert-to-flat-config generator should handle custom eslintignores 1`] = ` exports[`convert-to-flat-config generator CJS should handle custom eslintignores 1`] = `
"const baseConfig = require('../../eslint.config.cjs'); "const baseConfig = require('../../eslint.config.cjs');
module.exports = [ module.exports = [
@ -732,3 +732,756 @@ module.exports = [
]; ];
" "
`; `;
exports[`convert-to-flat-config generator MJS should add env configuration 1`] = `
"import { FlatCompat } from '@eslint/eslintrc';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import js from '@eslint/js';
import nxEslintPlugin from '@nx/eslint-plugin';
import globals from 'globals';
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
export default [
{
ignores: ['**/dist'],
},
{ plugins: { '@nx': nxEslintPlugin } },
{ languageOptions: { globals: { ...globals.browser, ...globals.node } } },
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
rules: {
'@nx/enforce-module-boundaries': [
'error',
{
enforceBuildableLibDependency: true,
allow: [],
depConstraints: [
{
sourceTag: '*',
onlyDependOnLibsWithTags: ['*'],
},
],
},
],
},
},
...compat
.config({
extends: ['plugin:@nx/typescript'],
})
.map((config) => ({
...config,
files: ['**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts'],
rules: {
...config.rules,
},
})),
...compat
.config({
extends: ['plugin:@nx/javascript'],
})
.map((config) => ({
...config,
files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'],
rules: {
...config.rules,
},
})),
];
"
`;
exports[`convert-to-flat-config generator MJS should add global and env configuration 1`] = `
"import { FlatCompat } from '@eslint/eslintrc';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import js from '@eslint/js';
import nxEslintPlugin from '@nx/eslint-plugin';
import globals from 'globals';
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
export default [
{
ignores: ['**/dist'],
},
{ plugins: { '@nx': nxEslintPlugin } },
{
languageOptions: {
globals: { ...globals.browser, myCustomGlobal: 'readonly' },
},
},
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
rules: {
'@nx/enforce-module-boundaries': [
'error',
{
enforceBuildableLibDependency: true,
allow: [],
depConstraints: [
{
sourceTag: '*',
onlyDependOnLibsWithTags: ['*'],
},
],
},
],
},
},
...compat
.config({
extends: ['plugin:@nx/typescript'],
})
.map((config) => ({
...config,
files: ['**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts'],
rules: {
...config.rules,
},
})),
...compat
.config({
extends: ['plugin:@nx/javascript'],
})
.map((config) => ({
...config,
files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'],
rules: {
...config.rules,
},
})),
];
"
`;
exports[`convert-to-flat-config generator MJS should add global configuration 1`] = `
"import { FlatCompat } from '@eslint/eslintrc';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import js from '@eslint/js';
import nxEslintPlugin from '@nx/eslint-plugin';
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
export default [
{
ignores: ['**/dist'],
},
{ plugins: { '@nx': nxEslintPlugin } },
{ languageOptions: { globals: { myCustomGlobal: 'readonly' } } },
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
rules: {
'@nx/enforce-module-boundaries': [
'error',
{
enforceBuildableLibDependency: true,
allow: [],
depConstraints: [
{
sourceTag: '*',
onlyDependOnLibsWithTags: ['*'],
},
],
},
],
},
},
...compat
.config({
extends: ['plugin:@nx/typescript'],
})
.map((config) => ({
...config,
files: ['**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts'],
rules: {
...config.rules,
},
})),
...compat
.config({
extends: ['plugin:@nx/javascript'],
})
.map((config) => ({
...config,
files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'],
rules: {
...config.rules,
},
})),
];
"
`;
exports[`convert-to-flat-config generator MJS should add global eslintignores 1`] = `
"import { FlatCompat } from '@eslint/eslintrc';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import js from '@eslint/js';
import nxEslintPlugin from '@nx/eslint-plugin';
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
export default [
{
ignores: ['**/dist'],
},
{ plugins: { '@nx': nxEslintPlugin } },
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
rules: {
'@nx/enforce-module-boundaries': [
'error',
{
enforceBuildableLibDependency: true,
allow: [],
depConstraints: [
{
sourceTag: '*',
onlyDependOnLibsWithTags: ['*'],
},
],
},
],
},
},
...compat
.config({
extends: ['plugin:@nx/typescript'],
})
.map((config) => ({
...config,
files: ['**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts'],
rules: {
...config.rules,
},
})),
...compat
.config({
extends: ['plugin:@nx/javascript'],
})
.map((config) => ({
...config,
files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'],
rules: {
...config.rules,
},
})),
{
ignores: ['ignore/me'],
},
];
"
`;
exports[`convert-to-flat-config generator MJS should add parser 1`] = `
"import { FlatCompat } from '@eslint/eslintrc';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import js from '@eslint/js';
import nxEslintPlugin from '@nx/eslint-plugin';
import typescriptEslintParser from '@typescript-eslint/parser';
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
export default [
{
ignores: ['**/dist'],
},
{ plugins: { '@nx': nxEslintPlugin } },
{ languageOptions: { parser: typescriptEslintParser } },
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
rules: {
'@nx/enforce-module-boundaries': [
'error',
{
enforceBuildableLibDependency: true,
allow: [],
depConstraints: [
{
sourceTag: '*',
onlyDependOnLibsWithTags: ['*'],
},
],
},
],
},
},
...compat
.config({
extends: ['plugin:@nx/typescript'],
})
.map((config) => ({
...config,
files: ['**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts'],
rules: {
...config.rules,
},
})),
...compat
.config({
extends: ['plugin:@nx/javascript'],
})
.map((config) => ({
...config,
files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'],
rules: {
...config.rules,
},
})),
];
"
`;
exports[`convert-to-flat-config generator MJS should add plugins 1`] = `
"import { FlatCompat } from '@eslint/eslintrc';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import js from '@eslint/js';
import eslintPluginImport from 'eslint-plugin-import';
import eslintPluginSingleName from 'eslint-plugin-single-name';
import scopeEslintPluginWithName from '@scope/eslint-plugin-with-name';
import justScopeEslintPlugin from '@just-scope/eslint-plugin';
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
export default [
{
ignores: ['**/dist'],
},
{
plugins: {
'eslint-plugin-import': eslintPluginImport,
'single-name': eslintPluginSingleName,
'@scope/with-name': scopeEslintPluginWithName,
'@just-scope': justScopeEslintPlugin,
},
},
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
rules: {
'@nx/enforce-module-boundaries': [
'error',
{
enforceBuildableLibDependency: true,
allow: [],
depConstraints: [
{
sourceTag: '*',
onlyDependOnLibsWithTags: ['*'],
},
],
},
],
},
},
...compat
.config({
extends: ['plugin:@nx/typescript'],
})
.map((config) => ({
...config,
files: ['**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts'],
rules: {
...config.rules,
},
})),
...compat
.config({
extends: ['plugin:@nx/javascript'],
})
.map((config) => ({
...config,
files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'],
rules: {
...config.rules,
},
})),
];
"
`;
exports[`convert-to-flat-config generator MJS should add settings 1`] = `
"import { FlatCompat } from '@eslint/eslintrc';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import js from '@eslint/js';
import nxEslintPlugin from '@nx/eslint-plugin';
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
export default [
{
ignores: ['**/dist'],
},
{ plugins: { '@nx': nxEslintPlugin } },
{
settings: {
sharedData: 'Hello',
},
},
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
rules: {
'@nx/enforce-module-boundaries': [
'error',
{
enforceBuildableLibDependency: true,
allow: [],
depConstraints: [
{
sourceTag: '*',
onlyDependOnLibsWithTags: ['*'],
},
],
},
],
},
},
...compat
.config({
extends: ['plugin:@nx/typescript'],
})
.map((config) => ({
...config,
files: ['**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts'],
rules: {
...config.rules,
},
})),
...compat
.config({
extends: ['plugin:@nx/javascript'],
})
.map((config) => ({
...config,
files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'],
rules: {
...config.rules,
},
})),
];
"
`;
exports[`convert-to-flat-config generator MJS should convert json successfully 1`] = `
"import { FlatCompat } from '@eslint/eslintrc';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import js from '@eslint/js';
import nxEslintPlugin from '@nx/eslint-plugin';
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
export default [
{
ignores: ['**/dist'],
},
{ plugins: { '@nx': nxEslintPlugin } },
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
rules: {
'@nx/enforce-module-boundaries': [
'error',
{
enforceBuildableLibDependency: true,
allow: [],
depConstraints: [
{
sourceTag: '*',
onlyDependOnLibsWithTags: ['*'],
},
],
},
],
},
},
...compat
.config({
extends: ['plugin:@nx/typescript'],
})
.map((config) => ({
...config,
files: ['**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts'],
rules: {
...config.rules,
},
})),
...compat
.config({
extends: ['plugin:@nx/javascript'],
})
.map((config) => ({
...config,
files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'],
rules: {
...config.rules,
},
})),
];
"
`;
exports[`convert-to-flat-config generator MJS should convert json successfully 2`] = `
"import baseConfig from '../../eslint.config.mjs';
export default [
{
ignores: ['**/dist'],
},
...baseConfig,
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {},
},
{
files: ['**/*.ts', '**/*.tsx'],
// Override or add rules here
rules: {},
},
{
files: ['**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {},
},
];
"
`;
exports[`convert-to-flat-config generator MJS should convert yaml successfully 1`] = `
"import { FlatCompat } from '@eslint/eslintrc';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import js from '@eslint/js';
import nxEslintPlugin from '@nx/eslint-plugin';
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
export default [
{
ignores: ['**/dist'],
},
{ plugins: { '@nx': nxEslintPlugin } },
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
rules: {
'@nx/enforce-module-boundaries': [
'error',
{
enforceBuildableLibDependency: true,
allow: [],
depConstraints: [
{
sourceTag: '*',
onlyDependOnLibsWithTags: ['*'],
},
],
},
],
},
},
...compat
.config({
extends: ['plugin:@nx/typescript'],
})
.map((config) => ({
...config,
files: ['**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts'],
rules: {
...config.rules,
},
})),
...compat
.config({
extends: ['plugin:@nx/javascript'],
})
.map((config) => ({
...config,
files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'],
rules: {
...config.rules,
},
})),
];
"
`;
exports[`convert-to-flat-config generator MJS should convert yaml successfully 2`] = `
"import baseConfig from '../../eslint.config.mjs';
export default [
{
ignores: ['**/dist'],
},
...baseConfig,
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {},
},
{
files: ['**/*.ts', '**/*.tsx'],
// Override or add rules here
rules: {},
},
{
files: ['**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {},
},
];
"
`;
exports[`convert-to-flat-config generator MJS should convert yml successfully 1`] = `
"import { FlatCompat } from '@eslint/eslintrc';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import js from '@eslint/js';
import nxEslintPlugin from '@nx/eslint-plugin';
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
export default [
{
ignores: ['**/dist'],
},
{ plugins: { '@nx': nxEslintPlugin } },
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
rules: {
'@nx/enforce-module-boundaries': [
'error',
{
enforceBuildableLibDependency: true,
allow: [],
depConstraints: [
{
sourceTag: '*',
onlyDependOnLibsWithTags: ['*'],
},
],
},
],
},
},
...compat
.config({
extends: ['plugin:@nx/typescript'],
})
.map((config) => ({
...config,
files: ['**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts'],
rules: {
...config.rules,
},
})),
...compat
.config({
extends: ['plugin:@nx/javascript'],
})
.map((config) => ({
...config,
files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'],
rules: {
...config.rules,
},
})),
];
"
`;
exports[`convert-to-flat-config generator MJS should convert yml successfully 2`] = `
"import baseConfig from '../../eslint.config.mjs';
export default [
{
ignores: ['**/dist'],
},
...baseConfig,
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {},
},
{
files: ['**/*.ts', '**/*.tsx'],
// Override or add rules here
rules: {},
},
{
files: ['**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {},
},
];
"
`;
exports[`convert-to-flat-config generator MJS should handle custom eslintignores 1`] = `
"import baseConfig from '../../eslint.config.mjs';
export default [
{
ignores: ['**/dist'],
},
...baseConfig,
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {},
},
{
files: ['**/*.ts', '**/*.tsx'],
// Override or add rules here
rules: {},
},
{
files: ['**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {},
},
{
ignores: ['ignore/me'],
},
{
ignores: ['ignore/me/as/well'],
},
];
"
`;

View File

@ -9,306 +9,623 @@ describe('convertEslintJsonToFlatConfig', () => {
tree = createTreeWithEmptyWorkspace(); tree = createTreeWithEmptyWorkspace();
}); });
it('should convert root configs', async () => { describe('ESM', () => {
tree.write( it('should convert root configs', async () => {
'.eslintrc.json', tree.write(
JSON.stringify({ '.eslintrc.json',
root: true, JSON.stringify({
ignorePatterns: ['**/*', 'src/ignore/to/keep.ts'], root: true,
plugins: ['@nx'], ignorePatterns: ['**/*', 'src/ignore/to/keep.ts'],
overrides: [ plugins: ['@nx'],
{ overrides: [
files: ['*.ts', '*.tsx', '*.js', '*.jsx'], {
rules: { files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
'@nx/enforce-module-boundaries': [
'error',
{
enforceBuildableLibDependency: true,
allow: [],
depConstraints: [
{
sourceTag: '*',
onlyDependOnLibsWithTags: ['*'],
},
],
},
],
},
},
{
files: ['*.ts', '*.tsx'],
extends: ['plugin:@nx/typescript'],
rules: {},
},
{
files: ['*.js', '*.jsx'],
extends: ['plugin:@nx/javascript'],
rules: {},
},
{
files: [
'**/*.spec.ts',
'**/*.spec.tsx',
'**/*.spec.js',
'**/*.spec.jsx',
],
env: {
jest: true,
},
rules: {},
},
],
})
);
tree.write('.eslintignore', 'node_modules\nsomething/else');
const { content } = convertEslintJsonToFlatConfig(
tree,
'',
readJson(tree, '.eslintrc.json'),
['.eslintignore']
);
expect(content).toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc");
const js = require("@eslint/js");
const nxEslintPlugin = require("@nx/eslint-plugin");
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
});
module.exports = [
{
ignores: [
"**/dist"
]
},
{ plugins: { "@nx": nxEslintPlugin } },
{
files: [
"**/*.ts",
"**/*.tsx",
"**/*.js",
"**/*.jsx"
],
rules: { rules: {
"@nx/enforce-module-boundaries": [ '@nx/enforce-module-boundaries': [
"error", 'error',
{
enforceBuildableLibDependency: true,
allow: [],
depConstraints: [
{ {
enforceBuildableLibDependency: true, sourceTag: '*',
allow: [], onlyDependOnLibsWithTags: ['*'],
depConstraints: [ },
{ ],
sourceTag: "*", },
onlyDependOnLibsWithTags: [ ],
"*" },
] },
} {
files: ['*.ts', '*.tsx'],
extends: ['plugin:@nx/typescript'],
rules: {},
},
{
files: ['*.js', '*.jsx'],
extends: ['plugin:@nx/javascript'],
rules: {},
},
{
files: [
'**/*.spec.ts',
'**/*.spec.tsx',
'**/*.spec.js',
'**/*.spec.jsx',
],
env: {
jest: true,
},
rules: {},
},
],
})
);
tree.write('.eslintignore', 'node_modules\nsomething/else');
const { content } = convertEslintJsonToFlatConfig(
tree,
'',
readJson(tree, '.eslintrc.json'),
['.eslintignore'],
'mjs'
);
expect(content).toMatchInlineSnapshot(`
"import { FlatCompat } from "@eslint/eslintrc";
import { dirname } from "path";
import { fileURLToPath } from "url";
import js from "@eslint/js";
import nxEslintPlugin from "@nx/eslint-plugin";
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
export default [
{
ignores: [
"**/dist"
]
},
{ plugins: { "@nx": nxEslintPlugin } },
{
files: [
"**/*.ts",
"**/*.tsx",
"**/*.js",
"**/*.jsx"
],
rules: {
"@nx/enforce-module-boundaries": [
"error",
{
enforceBuildableLibDependency: true,
allow: [],
depConstraints: [
{
sourceTag: "*",
onlyDependOnLibsWithTags: [
"*"
]
}
]
}
]
}
},
...compat.config({
extends: [
"plugin:@nx/typescript"
]
}).map(config => ({
...config,
files: [
"**/*.ts",
"**/*.tsx",
"**/*.cts",
"**/*.mts"
],
rules: {
...config.rules
}
})),
...compat.config({
extends: [
"plugin:@nx/javascript"
]
}).map(config => ({
...config,
files: [
"**/*.js",
"**/*.jsx",
"**/*.cjs",
"**/*.mjs"
],
rules: {
...config.rules
}
})),
...compat.config({
env: {
jest: true
}
}).map(config => ({
...config,
files: [
"**/*.spec.ts",
"**/*.spec.tsx",
"**/*.spec.js",
"**/*.spec.jsx"
],
rules: {
...config.rules
}
})),
{
ignores: [
"src/ignore/to/keep.ts"
]
},
{
ignores: [
"something/else"
]
}
];
"
`);
});
it('should convert project configs', async () => {
tree.write(
'mylib/.eslintrc.json',
JSON.stringify({
extends: [
'plugin:@nx/react-typescript',
'next',
'next/core-web-vitals',
'../../.eslintrc.json',
],
ignorePatterns: ['!**/*', '.next/**/*'],
overrides: [
{
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
rules: {
'@next/next/no-html-link-for-pages': [
'error',
'apps/test-next/pages',
],
},
},
{
files: ['*.ts', '*.tsx'],
rules: {},
},
{
files: ['*.js', '*.jsx'],
rules: {},
},
{
files: ['*.json'],
parser: 'jsonc-eslint-parser',
rules: {
'@nx/dependency-checks': 'error',
},
},
],
rules: {
'@next/next/no-html-link-for-pages': 'off',
},
env: {
jest: true,
},
})
);
tree.write('mylib/.eslintignore', 'node_modules\nsomething/else');
const { content } = convertEslintJsonToFlatConfig(
tree,
'mylib',
readJson(tree, 'mylib/.eslintrc.json'),
['mylib/.eslintignore'],
'mjs'
);
expect(content).toMatchInlineSnapshot(`
"import { FlatCompat } from "@eslint/eslintrc";
import { dirname } from "path";
import { fileURLToPath } from "url";
import js from "@eslint/js";
import baseConfig from "../../eslint.config.mjs";
import globals from "globals";
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
export default [
{
ignores: [
"**/dist"
]
},
...baseConfig,
...compat.extends("plugin:@nx/react-typescript", "next", "next/core-web-vitals"),
{ languageOptions: { globals: { ...globals.jest } } },
{
rules: {
"@next/next/no-html-link-for-pages": "off"
}
},
{
files: [
"**/*.ts",
"**/*.tsx",
"**/*.js",
"**/*.jsx"
],
rules: {
"@next/next/no-html-link-for-pages": [
"error",
"apps/test-next/pages"
] ]
} }
] },
} {
}, files: [
...compat.config({ "**/*.ts",
extends: [ "**/*.tsx"
"plugin:@nx/typescript" ],
] // Override or add rules here
}).map(config => ({ rules: {}
...config, },
files: [ {
"**/*.ts", files: [
"**/*.tsx", "**/*.js",
"**/*.cts", "**/*.jsx"
"**/*.mts" ],
], // Override or add rules here
rules: { rules: {}
...config.rules },
} {
})), files: [
...compat.config({ "**/*.json"
extends: [ ],
"plugin:@nx/javascript" rules: {
] "@nx/dependency-checks": "error"
}).map(config => ({ },
...config, languageOptions: {
files: [ parser: await import("jsonc-eslint-parser")
"**/*.js", }
"**/*.jsx", },
"**/*.cjs", {
"**/*.mjs" ignores: [
], ".next/**/*"
rules: { ]
...config.rules },
} {
})), ignores: [
...compat.config({ "something/else"
env: { ]
jest: true }
} ];
}).map(config => ({ "
...config, `);
files: [ });
"**/*.spec.ts",
"**/*.spec.tsx",
"**/*.spec.js",
"**/*.spec.jsx"
],
rules: {
...config.rules
}
})),
{
ignores: [
"src/ignore/to/keep.ts"
]
},
{
ignores: [
"something/else"
]
}
];
"
`);
}); });
it('should convert project configs', async () => { describe('CJS', () => {
tree.write( it('should convert root configs', async () => {
'mylib/.eslintrc.json', tree.write(
JSON.stringify({ '.eslintrc.json',
extends: [ JSON.stringify({
'plugin:@nx/react-typescript', root: true,
'next', ignorePatterns: ['**/*', 'src/ignore/to/keep.ts'],
'next/core-web-vitals', plugins: ['@nx'],
'../../.eslintrc.json', overrides: [
], {
ignorePatterns: ['!**/*', '.next/**/*'], files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
overrides: [
{
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
rules: {
'@next/next/no-html-link-for-pages': [
'error',
'apps/test-next/pages',
],
},
},
{
files: ['*.ts', '*.tsx'],
rules: {},
},
{
files: ['*.js', '*.jsx'],
rules: {},
},
{
files: ['*.json'],
parser: 'jsonc-eslint-parser',
rules: {
'@nx/dependency-checks': 'error',
},
},
],
rules: {
'@next/next/no-html-link-for-pages': 'off',
},
env: {
jest: true,
},
})
);
tree.write('mylib/.eslintignore', 'node_modules\nsomething/else');
const { content } = convertEslintJsonToFlatConfig(
tree,
'mylib',
readJson(tree, 'mylib/.eslintrc.json'),
['mylib/.eslintignore']
);
expect(content).toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc");
const js = require("@eslint/js");
const baseConfig = require("../../eslint.config.cjs");
const globals = require("globals");
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
});
module.exports = [
{
ignores: [
"**/dist"
]
},
...baseConfig,
...compat.extends("plugin:@nx/react-typescript", "next", "next/core-web-vitals"),
{ languageOptions: { globals: { ...globals.jest } } },
{
rules: { rules: {
"@next/next/no-html-link-for-pages": "off" '@nx/enforce-module-boundaries': [
} 'error',
}, {
{ enforceBuildableLibDependency: true,
files: [ allow: [],
"**/*.ts", depConstraints: [
"**/*.tsx", {
"**/*.js", sourceTag: '*',
"**/*.jsx" onlyDependOnLibsWithTags: ['*'],
], },
rules: { ],
"@next/next/no-html-link-for-pages": [ },
"error", ],
"apps/test-next/pages"
]
}
},
{
files: [
"**/*.ts",
"**/*.tsx"
],
// Override or add rules here
rules: {}
},
{
files: [
"**/*.js",
"**/*.jsx"
],
// Override or add rules here
rules: {}
},
{
files: [
"**/*.json"
],
rules: {
"@nx/dependency-checks": "error"
}, },
languageOptions: { },
parser: require("jsonc-eslint-parser") {
} files: ['*.ts', '*.tsx'],
extends: ['plugin:@nx/typescript'],
rules: {},
},
{
files: ['*.js', '*.jsx'],
extends: ['plugin:@nx/javascript'],
rules: {},
},
{
files: [
'**/*.spec.ts',
'**/*.spec.tsx',
'**/*.spec.js',
'**/*.spec.jsx',
],
env: {
jest: true,
},
rules: {},
},
],
})
);
tree.write('.eslintignore', 'node_modules\nsomething/else');
const { content } = convertEslintJsonToFlatConfig(
tree,
'',
readJson(tree, '.eslintrc.json'),
['.eslintignore'],
'cjs'
);
expect(content).toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc");
const js = require("@eslint/js");
const nxEslintPlugin = require("@nx/eslint-plugin");
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
});
module.exports = [
{
ignores: [
"**/dist"
]
},
{ plugins: { "@nx": nxEslintPlugin } },
{
files: [
"**/*.ts",
"**/*.tsx",
"**/*.js",
"**/*.jsx"
],
rules: {
"@nx/enforce-module-boundaries": [
"error",
{
enforceBuildableLibDependency: true,
allow: [],
depConstraints: [
{
sourceTag: "*",
onlyDependOnLibsWithTags: [
"*"
]
}
]
}
]
}
},
...compat.config({
extends: [
"plugin:@nx/typescript"
]
}).map(config => ({
...config,
files: [
"**/*.ts",
"**/*.tsx",
"**/*.cts",
"**/*.mts"
],
rules: {
...config.rules
}
})),
...compat.config({
extends: [
"plugin:@nx/javascript"
]
}).map(config => ({
...config,
files: [
"**/*.js",
"**/*.jsx",
"**/*.cjs",
"**/*.mjs"
],
rules: {
...config.rules
}
})),
...compat.config({
env: {
jest: true
}
}).map(config => ({
...config,
files: [
"**/*.spec.ts",
"**/*.spec.tsx",
"**/*.spec.js",
"**/*.spec.jsx"
],
rules: {
...config.rules
}
})),
{
ignores: [
"src/ignore/to/keep.ts"
]
},
{
ignores: [
"something/else"
]
}
];
"
`);
});
it('should convert project configs', async () => {
tree.write(
'mylib/.eslintrc.json',
JSON.stringify({
extends: [
'plugin:@nx/react-typescript',
'next',
'next/core-web-vitals',
'../../.eslintrc.json',
],
ignorePatterns: ['!**/*', '.next/**/*'],
overrides: [
{
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
rules: {
'@next/next/no-html-link-for-pages': [
'error',
'apps/test-next/pages',
],
},
},
{
files: ['*.ts', '*.tsx'],
rules: {},
},
{
files: ['*.js', '*.jsx'],
rules: {},
},
{
files: ['*.json'],
parser: 'jsonc-eslint-parser',
rules: {
'@nx/dependency-checks': 'error',
},
},
],
rules: {
'@next/next/no-html-link-for-pages': 'off',
}, },
{ env: {
ignores: [ jest: true,
".next/**/*"
]
}, },
{ })
ignores: [ );
"something/else"
] tree.write('mylib/.eslintignore', 'node_modules\nsomething/else');
}
]; const { content } = convertEslintJsonToFlatConfig(
" tree,
`); 'mylib',
readJson(tree, 'mylib/.eslintrc.json'),
['mylib/.eslintignore'],
'cjs'
);
expect(content).toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc");
const js = require("@eslint/js");
const baseConfig = require("../../eslint.config.cjs");
const globals = require("globals");
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
});
module.exports = [
{
ignores: [
"**/dist"
]
},
...baseConfig,
...compat.extends("plugin:@nx/react-typescript", "next", "next/core-web-vitals"),
{ languageOptions: { globals: { ...globals.jest } } },
{
rules: {
"@next/next/no-html-link-for-pages": "off"
}
},
{
files: [
"**/*.ts",
"**/*.tsx",
"**/*.js",
"**/*.jsx"
],
rules: {
"@next/next/no-html-link-for-pages": [
"error",
"apps/test-next/pages"
]
}
},
{
files: [
"**/*.ts",
"**/*.tsx"
],
// Override or add rules here
rules: {}
},
{
files: [
"**/*.js",
"**/*.jsx"
],
// Override or add rules here
rules: {}
},
{
files: [
"**/*.json"
],
rules: {
"@nx/dependency-checks": "error"
},
languageOptions: {
parser: require("jsonc-eslint-parser")
}
},
{
ignores: [
".next/**/*"
]
},
{
ignores: [
"something/else"
]
}
];
"
`);
});
}); });
}); });

View File

@ -21,7 +21,8 @@ export function convertEslintJsonToFlatConfig(
tree: Tree, tree: Tree,
root: string, root: string,
config: ESLint.ConfigData, config: ESLint.ConfigData,
ignorePaths: string[] ignorePaths: string[],
format: 'cjs' | 'mjs'
): { content: string; addESLintRC: boolean; addESLintJS: boolean } { ): { content: string; addESLintRC: boolean; addESLintJS: boolean } {
const importsMap = new Map<string, string>(); const importsMap = new Map<string, string>();
const exportElements: ts.Expression[] = []; const exportElements: ts.Expression[] = [];
@ -38,7 +39,12 @@ export function convertEslintJsonToFlatConfig(
); );
if (config.extends) { if (config.extends) {
const extendsResult = addExtends(importsMap, exportElements, config); const extendsResult = addExtends(
importsMap,
exportElements,
config,
format
);
isFlatCompatNeeded = extendsResult.isFlatCompatNeeded; isFlatCompatNeeded = extendsResult.isFlatCompatNeeded;
isESLintJSNeeded = extendsResult.isESLintJSNeeded; isESLintJSNeeded = extendsResult.isESLintJSNeeded;
} }
@ -156,7 +162,7 @@ export function convertEslintJsonToFlatConfig(
) { ) {
isFlatCompatNeeded = true; isFlatCompatNeeded = true;
} }
exportElements.push(generateFlatOverride(override)); exportElements.push(generateFlatOverride(override, format));
}); });
} }
@ -189,7 +195,7 @@ export function convertEslintJsonToFlatConfig(
} }
// create the node list and print it to new file // create the node list and print it to new file
const nodeList = createNodeList(importsMap, exportElements); const nodeList = createNodeList(importsMap, exportElements, format);
let content = stringifyNodeList(nodeList); let content = stringifyNodeList(nodeList);
if (isFlatCompatNeeded) { if (isFlatCompatNeeded) {
content = addFlatCompatToFlatConfig(content); content = addFlatCompatToFlatConfig(content);
@ -206,7 +212,8 @@ export function convertEslintJsonToFlatConfig(
function addExtends( function addExtends(
importsMap: Map<string, string | string[]>, importsMap: Map<string, string | string[]>,
configBlocks: ts.Expression[], configBlocks: ts.Expression[],
config: ESLint.ConfigData config: ESLint.ConfigData,
format: 'mjs' | 'cjs'
): { isFlatCompatNeeded: boolean; isESLintJSNeeded: boolean } { ): { isFlatCompatNeeded: boolean; isESLintJSNeeded: boolean } {
let isFlatCompatNeeded = false; let isFlatCompatNeeded = false;
let isESLintJSNeeded = false; let isESLintJSNeeded = false;
@ -225,7 +232,7 @@ function addExtends(
configBlocks.push(generateSpreadElement(localName)); configBlocks.push(generateSpreadElement(localName));
const newImport = imp.replace( const newImport = imp.replace(
/^(.*)\.eslintrc(.base)?\.json$/, /^(.*)\.eslintrc(.base)?\.json$/,
'$1eslint$2.config.cjs' `$1eslint$2.config.${format}`
); );
importsMap.set(newImport, localName); importsMap.set(newImport, localName);
} else { } else {

View File

@ -40,10 +40,12 @@ export async function convertToFlatConfigGenerator(
); );
} }
options.eslintConfigFormat ??= 'mjs';
const eslintIgnoreFiles = new Set<string>(['.eslintignore']); const eslintIgnoreFiles = new Set<string>(['.eslintignore']);
// convert root eslint config to eslint.config.cjs // convert root eslint config to eslint.config.cjs or eslint.base.config.mjs based on eslintConfigFormat
convertRootToFlatConfig(tree, eslintFile); convertRootToFlatConfig(tree, eslintFile, options.eslintConfigFormat);
// convert project eslint files to eslint.config.cjs // convert project eslint files to eslint.config.cjs
const projects = getProjects(tree); const projects = getProjects(tree);
@ -53,7 +55,8 @@ export async function convertToFlatConfigGenerator(
project, project,
projectConfig, projectConfig,
readNxJson(tree), readNxJson(tree),
eslintIgnoreFiles eslintIgnoreFiles,
options.eslintConfigFormat
); );
} }
@ -63,7 +66,7 @@ export async function convertToFlatConfigGenerator(
} }
// replace references in nx.json // replace references in nx.json
updateNxJsonConfig(tree); updateNxJsonConfig(tree, options.eslintConfigFormat);
// install missing packages // install missing packages
if (!options.skipFormat) { if (!options.skipFormat) {
@ -75,15 +78,26 @@ export async function convertToFlatConfigGenerator(
export default convertToFlatConfigGenerator; export default convertToFlatConfigGenerator;
function convertRootToFlatConfig(tree: Tree, eslintFile: string) { function convertRootToFlatConfig(
tree: Tree,
eslintFile: string,
format: 'cjs' | 'mjs'
) {
if (/\.base\.(js|json|yml|yaml)$/.test(eslintFile)) { if (/\.base\.(js|json|yml|yaml)$/.test(eslintFile)) {
convertConfigToFlatConfig(tree, '', eslintFile, 'eslint.base.config.cjs'); convertConfigToFlatConfig(
tree,
'',
eslintFile,
`eslint.base.config.${format}`,
format
);
} }
convertConfigToFlatConfig( convertConfigToFlatConfig(
tree, tree,
'', '',
eslintFile.replace('.base.', '.'), eslintFile.replace('.base.', '.'),
'eslint.config.cjs' `eslint.config.${format}`,
format
); );
} }
@ -92,7 +106,8 @@ function convertProjectToFlatConfig(
project: string, project: string,
projectConfig: ProjectConfiguration, projectConfig: ProjectConfiguration,
nxJson: NxJsonConfiguration, nxJson: NxJsonConfiguration,
eslintIgnoreFiles: Set<string> eslintIgnoreFiles: Set<string>,
format: 'cjs' | 'mjs'
) { ) {
const eslintFile = findEslintFile(tree, projectConfig.root); const eslintFile = findEslintFile(tree, projectConfig.root);
if (eslintFile && !eslintFile.endsWith('.js')) { if (eslintFile && !eslintFile.endsWith('.js')) {
@ -132,7 +147,8 @@ function convertProjectToFlatConfig(
tree, tree,
projectConfig.root, projectConfig.root,
eslintFile, eslintFile,
'eslint.config.cjs', `eslint.config.${format}`,
format,
ignorePath ignorePath
); );
eslintIgnoreFiles.add(`${projectConfig.root}/.eslintignore`); eslintIgnoreFiles.add(`${projectConfig.root}/.eslintignore`);
@ -146,22 +162,22 @@ function convertProjectToFlatConfig(
// update names of eslint files in nx.json // update names of eslint files in nx.json
// and remove eslintignore // and remove eslintignore
function updateNxJsonConfig(tree: Tree) { function updateNxJsonConfig(tree: Tree, format: 'cjs' | 'mjs') {
if (tree.exists('nx.json')) { if (tree.exists('nx.json')) {
updateJson(tree, 'nx.json', (json: NxJsonConfiguration) => { updateJson(tree, 'nx.json', (json: NxJsonConfiguration) => {
if (json.targetDefaults?.lint?.inputs) { if (json.targetDefaults?.lint?.inputs) {
const inputSet = new Set(json.targetDefaults.lint.inputs); const inputSet = new Set(json.targetDefaults.lint.inputs);
inputSet.add('{workspaceRoot}/eslint.config.cjs'); inputSet.add(`{workspaceRoot}/eslint.config.${format}`);
json.targetDefaults.lint.inputs = Array.from(inputSet); json.targetDefaults.lint.inputs = Array.from(inputSet);
} }
if (json.targetDefaults?.['@nx/eslint:lint']?.inputs) { if (json.targetDefaults?.['@nx/eslint:lint']?.inputs) {
const inputSet = new Set(json.targetDefaults['@nx/eslint:lint'].inputs); const inputSet = new Set(json.targetDefaults['@nx/eslint:lint'].inputs);
inputSet.add('{workspaceRoot}/eslint.config.cjs'); inputSet.add(`{workspaceRoot}/eslint.config.${format}`);
json.targetDefaults['@nx/eslint:lint'].inputs = Array.from(inputSet); json.targetDefaults['@nx/eslint:lint'].inputs = Array.from(inputSet);
} }
if (json.namedInputs?.production) { if (json.namedInputs?.production) {
const inputSet = new Set(json.namedInputs.production); const inputSet = new Set(json.namedInputs.production);
inputSet.add('!{projectRoot}/eslint.config.cjs'); inputSet.add(`!{projectRoot}/eslint.config.${format}`);
json.namedInputs.production = Array.from(inputSet); json.namedInputs.production = Array.from(inputSet);
} }
return json; return json;
@ -174,6 +190,7 @@ function convertConfigToFlatConfig(
root: string, root: string,
source: string, source: string,
target: string, target: string,
format: 'cjs' | 'mjs',
ignorePath?: string ignorePath?: string
) { ) {
const ignorePaths = ignorePath const ignorePaths = ignorePath
@ -186,7 +203,8 @@ function convertConfigToFlatConfig(
tree, tree,
root, root,
config, config,
ignorePaths ignorePaths,
format
); );
return processConvertedConfig(tree, root, source, target, conversionResult); return processConvertedConfig(tree, root, source, target, conversionResult);
} }
@ -201,7 +219,8 @@ function convertConfigToFlatConfig(
tree, tree,
root, root,
config, config,
ignorePaths ignorePaths,
format
); );
return processConvertedConfig(tree, root, source, target, conversionResult); return processConvertedConfig(tree, root, source, target, conversionResult);
} }

View File

@ -1,3 +1,5 @@
export interface ConvertToFlatConfigGeneratorSchema { export interface ConvertToFlatConfigGeneratorSchema {
skipFormat?: boolean; skipFormat?: boolean;
// Internal option
eslintConfigFormat?: 'mjs' | 'cjs';
} }

View File

@ -66,6 +66,7 @@ function postTargetTransformer(
'{workspaceRoot}/.eslintrc.json', '{workspaceRoot}/.eslintrc.json',
'{workspaceRoot}/.eslintignore', '{workspaceRoot}/.eslintignore',
'{workspaceRoot}/eslint.config.cjs', '{workspaceRoot}/eslint.config.cjs',
'{workspaceRoot}/eslint.config.mjs',
].includes(input) ].includes(input)
); );
if (inputs.length === 0) { if (inputs.length === 0) {

View File

@ -92,9 +92,10 @@ export const getGlobalEsLintConfiguration = (
}; };
export const getGlobalFlatEslintConfiguration = ( export const getGlobalFlatEslintConfiguration = (
format: 'cjs' | 'mjs',
rootProject?: boolean rootProject?: boolean
): string => { ): string => {
const nodeList = createNodeList(new Map(), []); const nodeList = createNodeList(new Map(), [], format);
let content = stringifyNodeList(nodeList); let content = stringifyNodeList(nodeList);
content = addImportToFlatConfig(content, 'nx', '@nx/eslint-plugin'); content = addImportToFlatConfig(content, 'nx', '@nx/eslint-plugin');
@ -114,49 +115,58 @@ export const getGlobalFlatEslintConfiguration = (
content = addBlockToFlatConfigExport( content = addBlockToFlatConfigExport(
content, content,
generateFlatOverride({ generateFlatOverride(
ignores: ['**/dist'], {
}) ignores: ['**/dist'],
},
format
)
); );
if (!rootProject) { if (!rootProject) {
content = addBlockToFlatConfigExport( content = addBlockToFlatConfigExport(
content, content,
generateFlatOverride({ generateFlatOverride(
files: ['*.ts', '*.tsx', '*.js', '*.jsx'], {
rules: { files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
'@nx/enforce-module-boundaries': [ rules: {
'error', '@nx/enforce-module-boundaries': [
{ 'error',
enforceBuildableLibDependency: true, {
allow: [ enforceBuildableLibDependency: true,
// This allows a root project to be present without causing lint errors allow: [
// since all projects will depend on this base file. // This allows a root project to be present without causing lint errors
'^.*/eslint(\\.base)?\\.config\\.[cm]?js$', // since all projects will depend on this base file.
], '^.*/eslint(\\.base)?\\.config\\.[cm]?js$',
depConstraints: [ ],
{ sourceTag: '*', onlyDependOnLibsWithTags: ['*'] }, depConstraints: [
], { sourceTag: '*', onlyDependOnLibsWithTags: ['*'] },
}, ],
], },
} as Linter.RulesRecord, ],
}) } as Linter.RulesRecord,
},
format
)
); );
} }
content = addBlockToFlatConfigExport( content = addBlockToFlatConfigExport(
content, content,
generateFlatOverride({ generateFlatOverride(
files: [ {
'**/*.ts', files: [
'**/*.tsx', '**/*.ts',
'**/*.js', '**/*.tsx',
'**/*.jsx', '**/*.js',
'**/*.cjs', '**/*.jsx',
'**/*.mjs', '**/*.cjs',
], '**/*.mjs',
rules: {}, ],
}) rules: {},
},
format
)
); );
return content; return content;

View File

@ -9,8 +9,12 @@ import {
updateJson, updateJson,
writeJson, writeJson,
} from '@nx/devkit'; } from '@nx/devkit';
import { dirname } from 'path'; import { dirname, extname } from 'path';
import { findEslintFile, isEslintConfigSupported } from '../utils/eslint-file'; import {
determineEslintConfigFormat,
findEslintFile,
isEslintConfigSupported,
} from '../utils/eslint-file';
import { import {
getGlobalEsLintConfiguration, getGlobalEsLintConfiguration,
getGlobalFlatEslintConfiguration, getGlobalFlatEslintConfiguration,
@ -32,10 +36,24 @@ export function migrateConfigToMonorepoStyle(
projects: ProjectConfiguration[], projects: ProjectConfiguration[],
tree: Tree, tree: Tree,
unitTestRunner: string, unitTestRunner: string,
eslintConfigFormat: 'mjs' | 'cjs',
keepExistingVersions?: boolean keepExistingVersions?: boolean
): GeneratorCallback { ): GeneratorCallback {
const rootEslintConfig = findEslintFile(tree); const rootEslintConfig = findEslintFile(tree);
let skipCleanup = false; let skipCleanup = false;
if (rootEslintConfig) {
// We do not want to mix the formats
const fileExtension = extname(rootEslintConfig);
if (fileExtension === '.mjs' || fileExtension === '.cjs') {
eslintConfigFormat = fileExtension.slice(1) as 'mjs' | 'cjs';
} else {
eslintConfigFormat = determineEslintConfigFormat(
tree.read(rootEslintConfig, 'utf-8')
);
}
}
if ( if (
rootEslintConfig?.match(/\.base\./) && rootEslintConfig?.match(/\.base\./) &&
!projects.some((p) => p.root === '.') !projects.some((p) => p.root === '.')
@ -57,10 +75,10 @@ export function migrateConfigToMonorepoStyle(
keepExistingVersions keepExistingVersions
); );
tree.write( tree.write(
tree.exists('eslint.config.cjs') tree.exists(`eslint.config.${eslintConfigFormat}`)
? 'eslint.base.config.cjs' ? `eslint.base.config.${eslintConfigFormat}`
: 'eslint.config.cjs', : `eslint.config.${eslintConfigFormat}`,
getGlobalFlatEslintConfiguration() getGlobalFlatEslintConfiguration(eslintConfigFormat)
); );
} else { } else {
const eslintFile = findEslintFile(tree, '.'); const eslintFile = findEslintFile(tree, '.');
@ -134,7 +152,9 @@ function migrateEslintFile(projectEslintPath: string, tree: Tree) {
let config = tree.read(projectEslintPath, 'utf-8'); let config = tree.read(projectEslintPath, 'utf-8');
// remove @nx plugin // remove @nx plugin
config = removePlugin(config, '@nx', '@nx/eslint-plugin-nx'); config = removePlugin(config, '@nx', '@nx/eslint-plugin-nx');
// extend eslint.base.config.cjs
// if base config is cjs, we will need to import it using async import
config = addImportToFlatConfig( config = addImportToFlatConfig(
config, config,
'baseConfig', 'baseConfig',

View File

@ -102,43 +102,103 @@ describe('@nx/eslint:init', () => {
}); });
describe('(legacy)', () => { describe('(legacy)', () => {
it('should add the root eslint config to the lint targetDefaults for lint', async () => { describe('CJS', () => {
await lintInitGenerator(tree, { ...options, addPlugin: false }); it('should add the root eslint config to the lint targetDefaults for lint', async () => {
await lintInitGenerator(tree, {
...options,
addPlugin: false,
eslintConfigFormat: 'cjs',
});
expect( expect(
readJson(tree, 'nx.json').targetDefaults['@nx/eslint:lint'] readJson(tree, 'nx.json').targetDefaults['@nx/eslint:lint']
).toEqual({ ).toEqual({
cache: true, cache: true,
inputs: [ inputs: [
'default', 'default',
'{workspaceRoot}/.eslintrc.json', '{workspaceRoot}/.eslintrc.json',
'{workspaceRoot}/.eslintignore', '{workspaceRoot}/.eslintignore',
'{workspaceRoot}/eslint.config.cjs', '{workspaceRoot}/eslint.config.cjs',
], ],
});
});
it('should setup lint target defaults', async () => {
updateJson<NxJsonConfiguration>(tree, 'nx.json', (json) => {
json.namedInputs ??= {};
json.namedInputs.production = ['default'];
return json;
});
await lintInitGenerator(tree, {
...options,
addPlugin: false,
eslintConfigFormat: 'cjs',
});
expect(
readJson<NxJsonConfiguration>(tree, 'nx.json').targetDefaults[
'@nx/eslint:lint'
]
).toEqual({
cache: true,
inputs: [
'default',
'{workspaceRoot}/.eslintrc.json',
'{workspaceRoot}/.eslintignore',
'{workspaceRoot}/eslint.config.cjs',
],
});
}); });
}); });
it('should setup lint target defaults', async () => { describe('MJS', () => {
updateJson<NxJsonConfiguration>(tree, 'nx.json', (json) => { it('should add the root eslint config to the lint targetDefaults for lint', async () => {
json.namedInputs ??= {}; await lintInitGenerator(tree, {
json.namedInputs.production = ['default']; ...options,
return json; addPlugin: false,
eslintConfigFormat: 'mjs',
});
expect(
readJson(tree, 'nx.json').targetDefaults['@nx/eslint:lint']
).toEqual({
cache: true,
inputs: [
'default',
'{workspaceRoot}/.eslintrc.json',
'{workspaceRoot}/.eslintignore',
'{workspaceRoot}/eslint.config.mjs',
],
});
}); });
await lintInitGenerator(tree, { ...options, addPlugin: false }); it('should setup lint target defaults', async () => {
updateJson<NxJsonConfiguration>(tree, 'nx.json', (json) => {
json.namedInputs ??= {};
json.namedInputs.production = ['default'];
return json;
});
expect( await lintInitGenerator(tree, {
readJson<NxJsonConfiguration>(tree, 'nx.json').targetDefaults[ ...options,
'@nx/eslint:lint' addPlugin: false,
] eslintConfigFormat: 'mjs',
).toEqual({ });
cache: true,
inputs: [ expect(
'default', readJson<NxJsonConfiguration>(tree, 'nx.json').targetDefaults[
'{workspaceRoot}/.eslintrc.json', '@nx/eslint:lint'
'{workspaceRoot}/.eslintignore', ]
'{workspaceRoot}/eslint.config.cjs', ).toEqual({
], cache: true,
inputs: [
'default',
'{workspaceRoot}/.eslintrc.json',
'{workspaceRoot}/.eslintignore',
'{workspaceRoot}/eslint.config.mjs',
],
});
}); });
}); });
}); });

View File

@ -11,31 +11,37 @@ import {
} from '@nx/devkit'; } from '@nx/devkit';
import { addPlugin } from '@nx/devkit/src/utils/add-plugin'; import { addPlugin } from '@nx/devkit/src/utils/add-plugin';
import { eslintVersion, nxVersion } from '../../utils/versions'; import { eslintVersion, nxVersion } from '../../utils/versions';
import { findEslintFile } from '../utils/eslint-file'; import {
determineEslintConfigFormat,
findEslintFile,
} from '../utils/eslint-file';
import { createNodesV2 } from '../../plugins/plugin'; import { createNodesV2 } from '../../plugins/plugin';
import { hasEslintPlugin } from '../utils/plugin'; import { hasEslintPlugin } from '../utils/plugin';
import { extname } from 'path';
export interface LinterInitOptions { export interface LinterInitOptions {
skipPackageJson?: boolean; skipPackageJson?: boolean;
keepExistingVersions?: boolean; keepExistingVersions?: boolean;
updatePackageScripts?: boolean; updatePackageScripts?: boolean;
addPlugin?: boolean; addPlugin?: boolean;
// Internal option
eslintConfigFormat?: 'mjs' | 'cjs';
} }
function updateProductionFileset(tree: Tree) { function updateProductionFileset(tree: Tree, format: 'mjs' | 'cjs' = 'mjs') {
const nxJson = readNxJson(tree); const nxJson = readNxJson(tree);
const productionFileSet = nxJson.namedInputs?.production; const productionFileSet = nxJson.namedInputs?.production;
if (productionFileSet) { if (productionFileSet) {
productionFileSet.push('!{projectRoot}/.eslintrc.json'); productionFileSet.push('!{projectRoot}/.eslintrc.json');
productionFileSet.push('!{projectRoot}/eslint.config.cjs'); productionFileSet.push(`!{projectRoot}/eslint.config.${format}`);
// Dedupe and set // Dedupe and set
nxJson.namedInputs.production = Array.from(new Set(productionFileSet)); nxJson.namedInputs.production = Array.from(new Set(productionFileSet));
} }
updateNxJson(tree, nxJson); updateNxJson(tree, nxJson);
} }
function addTargetDefaults(tree: Tree) { function addTargetDefaults(tree: Tree, format: 'mjs' | 'cjs') {
const nxJson = readNxJson(tree); const nxJson = readNxJson(tree);
nxJson.targetDefaults ??= {}; nxJson.targetDefaults ??= {};
@ -45,7 +51,7 @@ function addTargetDefaults(tree: Tree) {
'default', 'default',
`{workspaceRoot}/.eslintrc.json`, `{workspaceRoot}/.eslintrc.json`,
`{workspaceRoot}/.eslintignore`, `{workspaceRoot}/.eslintignore`,
`{workspaceRoot}/eslint.config.cjs`, `{workspaceRoot}/eslint.config.${format}`,
]; ];
updateNxJson(tree, nxJson); updateNxJson(tree, nxJson);
} }
@ -74,9 +80,21 @@ export async function initEsLint(
process.env.NX_ADD_PLUGINS !== 'false' && process.env.NX_ADD_PLUGINS !== 'false' &&
nxJson.useInferencePlugins !== false; nxJson.useInferencePlugins !== false;
options.addPlugin ??= addPluginDefault; options.addPlugin ??= addPluginDefault;
options.eslintConfigFormat ??= 'mjs';
const hasPlugin = hasEslintPlugin(tree); const hasPlugin = hasEslintPlugin(tree);
const rootEslintFile = findEslintFile(tree); const rootEslintFile = findEslintFile(tree);
if (rootEslintFile) {
const fileExtension = extname(rootEslintFile);
if (fileExtension === '.mjs' || fileExtension === '.cjs') {
options.eslintConfigFormat = fileExtension.slice(1) as 'mjs' | 'cjs';
} else {
options.eslintConfigFormat = determineEslintConfigFormat(
tree.read(rootEslintFile, 'utf-8')
);
}
}
const graph = await createProjectGraphAsync(); const graph = await createProjectGraphAsync();
const lintTargetNames = [ const lintTargetNames = [
@ -107,7 +125,7 @@ export async function initEsLint(
return () => {}; return () => {};
} }
updateProductionFileset(tree); updateProductionFileset(tree, options.eslintConfigFormat);
updateVsCodeRecommendedExtensions(tree); updateVsCodeRecommendedExtensions(tree);
@ -123,7 +141,7 @@ export async function initEsLint(
options.updatePackageScripts options.updatePackageScripts
); );
} else { } else {
addTargetDefaults(tree); addTargetDefaults(tree, options.eslintConfigFormat);
} }
const tasks: GeneratorCallback[] = []; const tasks: GeneratorCallback[] = [];

View File

@ -42,7 +42,237 @@ describe('@nx/eslint:lint-project', () => {
}); });
}); });
it('should generate a flat eslint base config', async () => { describe('Eslint base config named eslint.base.config', () => {
it('should generate a flat eslint config format based on base config (JS with CJS export)', async () => {
const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG;
process.env.ESLINT_USE_FLAT_CONFIG = 'true';
// CJS config
tree.write('eslint.base.config.js', 'module.exports = {};');
await lintProjectGenerator(tree, {
...defaultOptions,
linter: Linter.EsLint,
project: 'test-lib',
setParserOptionsProject: false,
skipFormat: true,
});
expect(tree.read('libs/test-lib/eslint.config.cjs', 'utf-8'))
.toMatchInlineSnapshot(`
"const baseConfig = require("../../eslint.base.config.js");
module.exports = [
...baseConfig
];
"
`);
process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal;
});
it('should generate a flat eslint config format based on base config (JS with MJS export)', async () => {
const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG;
process.env.ESLINT_USE_FLAT_CONFIG = 'true';
// MJS config
tree.write('eslint.base.config.js', 'export default {};');
await lintProjectGenerator(tree, {
...defaultOptions,
linter: Linter.EsLint,
project: 'test-lib',
setParserOptionsProject: false,
skipFormat: true,
});
expect(tree.read('libs/test-lib/eslint.config.mjs', 'utf-8'))
.toMatchInlineSnapshot(`
"import baseConfig from "../../eslint.base.config.js";
export default [
...baseConfig
];
"
`);
process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal;
});
it('should generate a flat eslint config format based on base config (mjs)', async () => {
const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG;
process.env.ESLINT_USE_FLAT_CONFIG = 'true';
// MJS config
tree.write('eslint.base.config.mjs', 'export default {};');
await lintProjectGenerator(tree, {
...defaultOptions,
linter: Linter.EsLint,
project: 'test-lib',
setParserOptionsProject: false,
skipFormat: true,
eslintConfigFormat: 'mjs',
});
expect(tree.read('libs/test-lib/eslint.config.mjs', 'utf-8'))
.toMatchInlineSnapshot(`
"import baseConfig from "../../eslint.base.config.mjs";
export default [
...baseConfig
];
"
`);
process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal;
});
it('should generate a flat eslint config format based on base config CJS', async () => {
const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG;
process.env.ESLINT_USE_FLAT_CONFIG = 'true';
// CJS config
tree.write('eslint.base.config.cjs', 'module.exports = {};');
await lintProjectGenerator(tree, {
...defaultOptions,
linter: Linter.EsLint,
project: 'test-lib',
setParserOptionsProject: false,
skipFormat: true,
});
expect(tree.read('libs/test-lib/eslint.config.cjs', 'utf-8'))
.toMatchInlineSnapshot(`
"const baseConfig = require("../../eslint.base.config.cjs");
module.exports = [
...baseConfig
];
"
`);
process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal;
});
});
describe('Eslint base config named eslint.config', () => {
it('should generate a flat eslint config format based on base config (JS with CJS export)', async () => {
const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG;
process.env.ESLINT_USE_FLAT_CONFIG = 'true';
// CJS config
tree.write('eslint.config.js', 'module.exports = {};');
await lintProjectGenerator(tree, {
...defaultOptions,
linter: Linter.EsLint,
project: 'test-lib',
setParserOptionsProject: false,
skipFormat: true,
});
expect(tree.read('libs/test-lib/eslint.config.cjs', 'utf-8'))
.toMatchInlineSnapshot(`
"const baseConfig = require("../../eslint.config.js");
module.exports = [
...baseConfig
];
"
`);
process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal;
});
it('should generate a flat eslint config format based on base config (JS with MJS export)', async () => {
const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG;
process.env.ESLINT_USE_FLAT_CONFIG = 'true';
// MJS config
tree.write('eslint.config.js', 'export default {};');
await lintProjectGenerator(tree, {
...defaultOptions,
linter: Linter.EsLint,
project: 'test-lib',
setParserOptionsProject: false,
skipFormat: true,
});
expect(tree.read('libs/test-lib/eslint.config.mjs', 'utf-8'))
.toMatchInlineSnapshot(`
"import baseConfig from "../../eslint.config.js";
export default [
...baseConfig
];
"
`);
process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal;
});
it('should generate a flat eslint config format based on base config (mjs)', async () => {
const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG;
process.env.ESLINT_USE_FLAT_CONFIG = 'true';
// MJS config
tree.write('eslint.config.mjs', 'export default {};');
await lintProjectGenerator(tree, {
...defaultOptions,
linter: Linter.EsLint,
project: 'test-lib',
setParserOptionsProject: false,
skipFormat: true,
eslintConfigFormat: 'mjs',
});
expect(tree.read('libs/test-lib/eslint.config.mjs', 'utf-8'))
.toMatchInlineSnapshot(`
"import baseConfig from "../../eslint.config.mjs";
export default [
...baseConfig
];
"
`);
process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal;
});
it('should generate a flat eslint config format based on base config CJS', async () => {
const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG;
process.env.ESLINT_USE_FLAT_CONFIG = 'true';
// CJS config
tree.write('eslint.config.cjs', 'module.exports = {};');
await lintProjectGenerator(tree, {
...defaultOptions,
linter: Linter.EsLint,
project: 'test-lib',
setParserOptionsProject: false,
skipFormat: true,
});
expect(tree.read('libs/test-lib/eslint.config.cjs', 'utf-8'))
.toMatchInlineSnapshot(`
"const baseConfig = require("../../eslint.config.cjs");
module.exports = [
...baseConfig
];
"
`);
process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal;
});
});
it('should generate a flat eslint base config ESM', async () => {
const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG; const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG;
process.env.ESLINT_USE_FLAT_CONFIG = 'true'; process.env.ESLINT_USE_FLAT_CONFIG = 'true';
await lintProjectGenerator(tree, { await lintProjectGenerator(tree, {
@ -51,6 +281,76 @@ describe('@nx/eslint:lint-project', () => {
project: 'test-lib', project: 'test-lib',
setParserOptionsProject: false, setParserOptionsProject: false,
skipFormat: true, skipFormat: true,
eslintConfigFormat: 'mjs',
});
expect(tree.read('eslint.config.mjs', 'utf-8')).toMatchInlineSnapshot(`
"import nx from "@nx/eslint-plugin";
export default [
...nx.configs["flat/base"],
...nx.configs["flat/typescript"],
...nx.configs["flat/javascript"],
{
ignores: [
"**/dist"
]
},
{
files: [
"**/*.ts",
"**/*.tsx",
"**/*.js",
"**/*.jsx"
],
rules: {
"@nx/enforce-module-boundaries": [
"error",
{
enforceBuildableLibDependency: true,
allow: [
"^.*/eslint(\\\\.base)?\\\\.config\\\\.[cm]?js$"
],
depConstraints: [
{
sourceTag: "*",
onlyDependOnLibsWithTags: [
"*"
]
}
]
}
]
}
},
{
files: [
"**/*.ts",
"**/*.tsx",
"**/*.js",
"**/*.jsx",
"**/*.cjs",
"**/*.mjs"
],
// Override or add rules here
rules: {}
}
];
"
`);
process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal;
});
it('should generate a flat eslint base config CJS', async () => {
const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG;
process.env.ESLINT_USE_FLAT_CONFIG = 'true';
await lintProjectGenerator(tree, {
...defaultOptions,
linter: Linter.EsLint,
project: 'test-lib',
setParserOptionsProject: false,
skipFormat: true,
eslintConfigFormat: 'cjs',
}); });
expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchInlineSnapshot(` expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchInlineSnapshot(`

View File

@ -17,8 +17,11 @@ import {
} from '@nx/devkit'; } from '@nx/devkit';
import { Linter as LinterEnum, LinterType } from '../utils/linter'; import { Linter as LinterEnum, LinterType } from '../utils/linter';
import { findEslintFile } from '../utils/eslint-file'; import {
import { join } from 'path'; determineEslintConfigFormat,
findEslintFile,
} from '../utils/eslint-file';
import { extname, join } from 'path';
import { lintInitGenerator } from '../init/init'; import { lintInitGenerator } from '../init/init';
import type { Linter } from 'eslint'; import type { Linter } from 'eslint';
import { migrateConfigToMonorepoStyle } from '../init/init-migration'; import { migrateConfigToMonorepoStyle } from '../init/init-migration';
@ -32,7 +35,7 @@ import {
} from '../utils/flat-config/ast-utils'; } from '../utils/flat-config/ast-utils';
import { import {
baseEsLintConfigFile, baseEsLintConfigFile,
baseEsLintFlatConfigFile, BASE_ESLINT_CONFIG_FILENAMES,
} from '../../utils/config-file'; } from '../../utils/config-file';
import { hasEslintPlugin } from '../utils/plugin'; import { hasEslintPlugin } from '../utils/plugin';
import { setupRootEsLint } from './setup-root-eslint'; import { setupRootEsLint } from './setup-root-eslint';
@ -49,6 +52,7 @@ interface LintProjectOptions {
rootProject?: boolean; rootProject?: boolean;
keepExistingVersions?: boolean; keepExistingVersions?: boolean;
addPlugin?: boolean; addPlugin?: boolean;
eslintConfigFormat?: 'mjs' | 'cjs';
/** /**
* @internal * @internal
@ -66,6 +70,7 @@ export async function lintProjectGeneratorInternal(
options: LintProjectOptions options: LintProjectOptions
) { ) {
const nxJson = readNxJson(tree); const nxJson = readNxJson(tree);
options.eslintConfigFormat ??= 'mjs';
const addPluginDefault = const addPluginDefault =
process.env.NX_ADD_PLUGINS !== 'false' && process.env.NX_ADD_PLUGINS !== 'false' &&
nxJson.useInferencePlugins !== false; nxJson.useInferencePlugins !== false;
@ -74,12 +79,14 @@ export async function lintProjectGeneratorInternal(
const initTask = await lintInitGenerator(tree, { const initTask = await lintInitGenerator(tree, {
skipPackageJson: options.skipPackageJson, skipPackageJson: options.skipPackageJson,
addPlugin: options.addPlugin, addPlugin: options.addPlugin,
eslintConfigFormat: options.eslintConfigFormat,
}); });
tasks.push(initTask); tasks.push(initTask);
const rootEsLintTask = setupRootEsLint(tree, { const rootEsLintTask = setupRootEsLint(tree, {
unitTestRunner: options.unitTestRunner, unitTestRunner: options.unitTestRunner,
skipPackageJson: options.skipPackageJson, skipPackageJson: options.skipPackageJson,
rootProject: options.rootProject, rootProject: options.rootProject,
eslintConfigFormat: options.eslintConfigFormat,
}); });
tasks.push(rootEsLintTask); tasks.push(rootEsLintTask);
const projectConfig = readProjectConfiguration(tree, options.project); const projectConfig = readProjectConfiguration(tree, options.project);
@ -146,6 +153,7 @@ export async function lintProjectGeneratorInternal(
filteredProjects, filteredProjects,
tree, tree,
options.unitTestRunner, options.unitTestRunner,
options.eslintConfigFormat,
options.keepExistingVersions options.keepExistingVersions
); );
tasks.push(migrateTask); tasks.push(migrateTask);
@ -199,6 +207,22 @@ function createEsLintConfiguration(
const pathToRootConfig = extendedRootConfig const pathToRootConfig = extendedRootConfig
? `${offsetFromRoot(projectConfig.root)}${extendedRootConfig}` ? `${offsetFromRoot(projectConfig.root)}${extendedRootConfig}`
: undefined; : undefined;
if (extendedRootConfig) {
// We do not want to mix the formats
// if the base file extension is `.mjs` we should use `mjs` for the new file
// or if base the file extension is `.cjs` then the format should be `cjs`
const fileExtension = extname(extendedRootConfig);
if (fileExtension === '.mjs' || fileExtension === '.cjs') {
options.eslintConfigFormat = fileExtension.slice(1) as 'mjs' | 'cjs';
} else {
options.eslintConfigFormat = determineEslintConfigFormat(
tree.read(extendedRootConfig, 'utf-8')
);
}
}
const addDependencyChecks = const addDependencyChecks =
options.addPackageJsonDependencyChecks || options.addPackageJsonDependencyChecks ||
isBuildableLibraryProject(projectConfig); isBuildableLibraryProject(projectConfig);
@ -269,11 +293,18 @@ function createEsLintConfiguration(
nodes.push(generateSpreadElement('baseConfig')); nodes.push(generateSpreadElement('baseConfig'));
} }
overrides.forEach((override) => { overrides.forEach((override) => {
nodes.push(generateFlatOverride(override)); nodes.push(generateFlatOverride(override, options.eslintConfigFormat));
}); });
const nodeList = createNodeList(importMap, nodes); const nodeList = createNodeList(
importMap,
nodes,
options.eslintConfigFormat
);
const content = stringifyNodeList(nodeList); const content = stringifyNodeList(nodeList);
tree.write(join(projectConfig.root, `eslint.config.cjs`), content); tree.write(
join(projectConfig.root, `eslint.config.${options.eslintConfigFormat}`),
content
);
} else { } else {
writeJson(tree, join(projectConfig.root, `.eslintrc.json`), { writeJson(tree, join(projectConfig.root, `.eslintrc.json`), {
extends: extendedRootConfig ? [pathToRootConfig] : undefined, extends: extendedRootConfig ? [pathToRootConfig] : undefined,
@ -313,8 +344,9 @@ function isBuildableLibraryProject(
function isMigrationToMonorepoNeeded(tree: Tree, graph: ProjectGraph): boolean { function isMigrationToMonorepoNeeded(tree: Tree, graph: ProjectGraph): boolean {
// the base config is already created, migration has been done // the base config is already created, migration has been done
if ( if (
tree.exists(baseEsLintConfigFile) || [baseEsLintConfigFile, ...BASE_ESLINT_CONFIG_FILENAMES].some((f) =>
tree.exists(baseEsLintFlatConfigFile) tree.exists(f)
)
) { ) {
return false; return false;
} }

View File

@ -22,6 +22,7 @@ export type SetupRootEsLintOptions = {
unitTestRunner?: string; unitTestRunner?: string;
skipPackageJson?: boolean; skipPackageJson?: boolean;
rootProject?: boolean; rootProject?: boolean;
eslintConfigFormat?: 'mjs' | 'cjs';
}; };
export function setupRootEsLint( export function setupRootEsLint(
@ -32,6 +33,8 @@ export function setupRootEsLint(
if (rootEslintFile) { if (rootEslintFile) {
return () => {}; return () => {};
} }
options.eslintConfigFormat ??= 'mjs';
if (!useFlatConfig(tree)) { if (!useFlatConfig(tree)) {
return setUpLegacyRootEslintRc(tree, options); return setUpLegacyRootEslintRc(tree, options);
} }
@ -71,8 +74,11 @@ function setUpLegacyRootEslintRc(tree: Tree, options: SetupRootEsLintOptions) {
function setUpRootFlatConfig(tree: Tree, options: SetupRootEsLintOptions) { function setUpRootFlatConfig(tree: Tree, options: SetupRootEsLintOptions) {
tree.write( tree.write(
'eslint.config.cjs', `eslint.config.${options.eslintConfigFormat}`,
getGlobalFlatEslintConfiguration(options.rootProject) getGlobalFlatEslintConfiguration(
options.eslintConfigFormat,
options.rootProject
)
); );
return !options.skipPackageJson return !options.skipPackageJson

View File

@ -2,8 +2,8 @@ import { readJson, type Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import * as devkitInternals from 'nx/src/devkit-internals'; import * as devkitInternals from 'nx/src/devkit-internals';
import { import {
BASE_ESLINT_CONFIG_FILENAMES,
ESLINT_CONFIG_FILENAMES, ESLINT_CONFIG_FILENAMES,
baseEsLintConfigFile,
} from '../../utils/config-file'; } from '../../utils/config-file';
import { import {
addExtendsToLintConfig, addExtendsToLintConfig,
@ -32,12 +32,11 @@ describe('@nx/eslint:lint-file', () => {
} }
); );
test.each(ESLINT_CONFIG_FILENAMES)( test.each(BASE_ESLINT_CONFIG_FILENAMES)(
'should return base file instead %p when calling findEslintFile', 'should return base file %p when calling findEslintFile',
(eslintFileName) => { (eslintFileName) => {
tree.write(baseEsLintConfigFile, '{}');
tree.write(eslintFileName, '{}'); tree.write(eslintFileName, '{}');
expect(findEslintFile(tree)).toBe(baseEsLintConfigFile); expect(findEslintFile(tree)).toBe(eslintFileName);
} }
); );
}); });

View File

@ -12,9 +12,8 @@ import type { Linter } from 'eslint';
import { gte } from 'semver'; import { gte } from 'semver';
import { import {
baseEsLintConfigFile, baseEsLintConfigFile,
baseEsLintFlatConfigFile,
ESLINT_CONFIG_FILENAMES, ESLINT_CONFIG_FILENAMES,
legacyBaseEsLintFlatConfigFile, BASE_ESLINT_CONFIG_FILENAMES,
} from '../../utils/config-file'; } from '../../utils/config-file';
import { import {
eslintFlatConfigFilenames, eslintFlatConfigFilenames,
@ -45,17 +44,15 @@ export function findEslintFile(
tree: Tree, tree: Tree,
projectRoot?: string projectRoot?: string
): string | null { ): string | null {
if (projectRoot === undefined && tree.exists(baseEsLintConfigFile)) { if (projectRoot === undefined) {
return baseEsLintConfigFile; for (const file of [
} baseEsLintConfigFile,
if (projectRoot === undefined && tree.exists(baseEsLintFlatConfigFile)) { ...BASE_ESLINT_CONFIG_FILENAMES,
return baseEsLintFlatConfigFile; ]) {
} if (tree.exists(file)) {
if ( return file;
projectRoot === undefined && }
tree.exists(legacyBaseEsLintFlatConfigFile) }
) {
return legacyBaseEsLintFlatConfigFile;
} }
projectRoot ??= ''; projectRoot ??= '';
for (const file of ESLINT_CONFIG_FILENAMES) { for (const file of ESLINT_CONFIG_FILENAMES) {
@ -75,7 +72,8 @@ export function isEslintConfigSupported(tree: Tree, projectRoot = ''): boolean {
return ( return (
eslintFile.endsWith('.json') || eslintFile.endsWith('.json') ||
eslintFile.endsWith('.config.js') || eslintFile.endsWith('.config.js') ||
eslintFile.endsWith('.config.cjs') eslintFile.endsWith('.config.cjs') ||
eslintFile.endsWith('.config.mjs')
); );
} }
@ -148,6 +146,19 @@ function replaceFlatConfigPaths(
`require('${newPath}')` + `require('${newPath}')` +
newConfig.slice(match.index + match[0].length); newConfig.slice(match.index + match[0].length);
} }
// Handle import statements
const importRegex = RegExp(/import\s+.*?\s+from\s+['"](.*)['"]/g);
while ((match = importRegex.exec(newConfig)) !== null) {
const oldPath = match[1];
const newPath = offsetFilePath(sourceRoot, oldPath, offset, tree);
// Replace the old path with the updated path
newConfig =
newConfig.slice(0, match.index + match[0].indexOf(oldPath)) +
newPath +
newConfig.slice(match.index + match[0].indexOf(oldPath) + oldPath.length);
}
// replace projects // replace projects
const projectRegex = RegExp(/project:\s?\[?['"](.*)['"]\]?/g); const projectRegex = RegExp(/project:\s?\[?['"](.*)['"]\]?/g);
while ((match = projectRegex.exec(newConfig)) !== null) { while ((match = projectRegex.exec(newConfig)) !== null) {
@ -184,6 +195,22 @@ function offsetFilePath(
return joinPathFragments(offset, projectRoot, pathToFile); return joinPathFragments(offset, projectRoot, pathToFile);
} }
export function determineEslintConfigFormat(content: string): 'mjs' | 'cjs' {
const sourceFile = ts.createSourceFile(
'',
content,
ts.ScriptTarget.Latest,
true
);
// Check if there's an `export default` in the AST
const hasExportDefault = sourceFile.statements.some(
(statement) => ts.isExportAssignment(statement) && !statement.isExportEquals
);
return hasExportDefault ? 'mjs' : 'cjs';
}
export function addOverrideToLintConfig( export function addOverrideToLintConfig(
tree: Tree, tree: Tree,
root: string, root: string,
@ -197,7 +224,12 @@ export function addOverrideToLintConfig(
if (useFlatConfig(tree)) { if (useFlatConfig(tree)) {
let fileName: string; let fileName: string;
if (isBase) { if (isBase) {
fileName = joinPathFragments(root, baseEsLintFlatConfigFile); for (const file of BASE_ESLINT_CONFIG_FILENAMES) {
if (tree.exists(joinPathFragments(root, file))) {
fileName = joinPathFragments(root, file);
break;
}
}
} else { } else {
for (const f of eslintFlatConfigFilenames) { for (const f of eslintFlatConfigFilenames) {
if (tree.exists(joinPathFragments(root, f))) { if (tree.exists(joinPathFragments(root, f))) {
@ -207,8 +239,10 @@ export function addOverrideToLintConfig(
} }
} }
const flatOverride = generateFlatOverride(override);
let content = tree.read(fileName, 'utf8'); let content = tree.read(fileName, 'utf8');
const format = content.includes('export default') ? 'mjs' : 'cjs';
const flatOverride = generateFlatOverride(override, format);
// Check if the provided override using legacy eslintrc properties or plugins, if so we need to add compat // Check if the provided override using legacy eslintrc properties or plugins, if so we need to add compat
if (overrideNeedsCompat(override)) { if (overrideNeedsCompat(override)) {
content = addFlatCompatToFlatConfig(content); content = addFlatCompatToFlatConfig(content);
@ -306,7 +340,12 @@ export function lintConfigHasOverride(
checkBaseConfig && checkBaseConfig &&
findEslintFile(tree, root).includes('.base'); findEslintFile(tree, root).includes('.base');
if (isBase) { if (isBase) {
fileName = joinPathFragments(root, baseEsLintFlatConfigFile); for (const file of BASE_ESLINT_CONFIG_FILENAMES) {
if (tree.exists(joinPathFragments(root, file))) {
fileName = joinPathFragments(root, file);
break;
}
}
} }
if (useFlatConfig(tree)) { if (useFlatConfig(tree)) {
if (!fileName) { if (!fileName) {
@ -343,13 +382,14 @@ export function replaceOverridesInLintConfig(
} }
} }
let content = tree.read(fileName, 'utf8'); let content = tree.read(fileName, 'utf8');
const format = content.includes('export default') ? 'mjs' : 'cjs';
// Check if any of the provided overrides using legacy eslintrc properties or plugins, if so we need to add compat // Check if any of the provided overrides using legacy eslintrc properties or plugins, if so we need to add compat
if (overrides.some(overrideNeedsCompat)) { if (overrides.some(overrideNeedsCompat)) {
content = addFlatCompatToFlatConfig(content); content = addFlatCompatToFlatConfig(content);
} }
content = removeOverridesFromLintConfig(content); content = removeOverridesFromLintConfig(content);
overrides.forEach((override) => { overrides.forEach((override) => {
const flatOverride = generateFlatOverride(override); const flatOverride = generateFlatOverride(override, format);
content = addBlockToFlatConfigExport(content, flatOverride); content = addBlockToFlatConfigExport(content, flatOverride);
}); });
@ -381,6 +421,14 @@ export function addExtendsToLintConfig(
break; break;
} }
} }
// Check the file extension to determine the format of the config if it is .js we look for the export
const eslintConfigFormat = fileName.endsWith('.mjs')
? 'mjs'
: fileName.endsWith('.cjs')
? 'cjs'
: tree.read(fileName, 'utf-8').includes('module.exports')
? 'cjs'
: 'mjs';
let shouldImportEslintCompat = false; let shouldImportEslintCompat = false;
// assume eslint version is 9 if not found, as it's what we'd be generating by default // assume eslint version is 9 if not found, as it's what we'd be generating by default

View File

@ -30,7 +30,7 @@ describe('ast-utils', () => {
it('should create appropriate ASTs for a flat config entries based on the provided legacy eslintrc JSON override data', () => { it('should create appropriate ASTs for a flat config entries based on the provided legacy eslintrc JSON override data', () => {
// It's easier to review the stringified result of the AST than the AST itself // It's easier to review the stringified result of the AST than the AST itself
const getOutput = (input: any) => { const getOutput = (input: any) => {
const ast = generateFlatOverride(input); const ast = generateFlatOverride(input, 'mjs');
return printTsNode(ast); return printTsNode(ast);
}; };
@ -87,13 +87,13 @@ describe('ast-utils', () => {
expect( expect(
getOutput({ getOutput({
// It should not only nest the parser in languageOptions, but also wrap it in a require call because parsers are passed by reference in flat config // It should not only nest the parser in languageOptions, but also wrap it in an import call because parsers are passed by reference in flat config
parser: 'jsonc-eslint-parser', parser: 'jsonc-eslint-parser',
}) })
).toMatchInlineSnapshot(` ).toMatchInlineSnapshot(`
"{ "{
languageOptions: { languageOptions: {
parser: require("jsonc-eslint-parser") parser: await import("jsonc-eslint-parser")
} }
}" }"
`); `);
@ -188,8 +188,8 @@ describe('ast-utils', () => {
describe('addBlockToFlatConfigExport', () => { describe('addBlockToFlatConfigExport', () => {
it('should inject block to the end of the file', () => { it('should inject block to the end of the file', () => {
const content = `const baseConfig = require("../../eslint.config.cjs"); const content = `import baseConfig from "../../eslint.config.mjs";
module.exports = [ export default [
...baseConfig, ...baseConfig,
{ {
files: [ files: [
@ -210,17 +210,18 @@ describe('ast-utils', () => {
}) })
); );
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
"const baseConfig = require("../../eslint.config.cjs"); "import baseConfig from "../../eslint.config.mjs";
module.exports = [
...baseConfig, export default [
{ ...baseConfig,
files: [ {
"my-lib/**/*.ts", files: [
"my-lib/**/*.tsx" "my-lib/**/*.ts",
], "my-lib/**/*.tsx"
rules: {} ],
}, rules: {}
{ ignores: ["my-lib/.cache/**/*"] }, },
{ ignores: ["my-lib/.cache/**/*"] },
{ {
files: [ files: [
"**/*.svg" "**/*.svg"
@ -228,14 +229,15 @@ describe('ast-utils', () => {
rules: { rules: {
"@nx/do-something-with-svg": "error" "@nx/do-something-with-svg": "error"
} }
}, }
];" ];
"
`); `);
}); });
it('should inject spread to the beginning of the file', () => { it('should inject spread to the beginning of the file', () => {
const content = `const baseConfig = require("../../eslint.config.cjs"); const content = `import baseConfig from "../../eslint.config.mjs";
module.exports = [ export default [
...baseConfig, ...baseConfig,
{ {
files: [ files: [
@ -252,28 +254,29 @@ describe('ast-utils', () => {
{ insertAtTheEnd: false } { insertAtTheEnd: false }
); );
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
"const baseConfig = require("../../eslint.config.cjs"); "import baseConfig from "../../eslint.config.mjs";
module.exports = [
...config,
...baseConfig, export default [
{ ...config,
files: [ ...baseConfig,
"my-lib/**/*.ts", {
"my-lib/**/*.tsx" files: [
], "my-lib/**/*.ts",
rules: {} "my-lib/**/*.tsx"
}, ],
{ ignores: ["my-lib/.cache/**/*"] }, rules: {}
];" },
{ ignores: ["my-lib/.cache/**/*"] }
];
"
`); `);
}); });
}); });
describe('addImportToFlatConfig', () => { describe('addImportToFlatConfig', () => {
it('should inject import if not found', () => { it('should inject import if not found', () => {
const content = `const baseConfig = require("../../eslint.config.cjs"); const content = `import baseConfig from "../../eslint.config.mjs";
module.exports = [ export default [
...baseConfig, ...baseConfig,
{ {
files: [ files: [
@ -286,30 +289,30 @@ describe('ast-utils', () => {
];`; ];`;
const result = addImportToFlatConfig( const result = addImportToFlatConfig(
content, content,
'varName', ['varName'],
'@myorg/awesome-config' '@myorg/awesome-config'
); );
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
"const varName = require("@myorg/awesome-config"); "import { varName } from "@myorg/awesome-config";
const baseConfig = require("../../eslint.config.cjs"); import baseConfig from "../../eslint.config.mjs";
module.exports = [ export default [
...baseConfig, ...baseConfig,
{ {
files: [ files: [
"my-lib/**/*.ts", "my-lib/**/*.ts",
"my-lib/**/*.tsx" "my-lib/**/*.tsx"
], ],
rules: {} rules: {}
}, },
{ ignores: ["my-lib/.cache/**/*"] }, { ignores: ["my-lib/.cache/**/*"] },
];" ];"
`); `);
}); });
it('should update import if already found', () => { it('should update import if already found', () => {
const content = `const { varName } = require("@myorg/awesome-config"); const content = `import { varName } from "@myorg/awesome-config";
const baseConfig = require("../../eslint.config.cjs"); import baseConfig from "../../eslint.config.mjs";
module.exports = [ export default [
...baseConfig, ...baseConfig,
{ {
files: [ files: [
@ -326,26 +329,27 @@ describe('ast-utils', () => {
'@myorg/awesome-config' '@myorg/awesome-config'
); );
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
"const { varName, otherName, someName } = require("@myorg/awesome-config"); "import { varName, otherName, someName } from "@myorg/awesome-config";
const baseConfig = require("../../eslint.config.cjs"); import baseConfig from "../../eslint.config.mjs";
module.exports = [ export default [
...baseConfig, ...baseConfig,
{ {
files: [ files: [
"my-lib/**/*.ts", "my-lib/**/*.ts",
"my-lib/**/*.tsx" "my-lib/**/*.tsx"
], ],
rules: {} rules: {}
}, },
{ ignores: ["my-lib/.cache/**/*"] }, { ignores: ["my-lib/.cache/**/*"] },
];" ];"
`); `);
}); });
it('should not inject import if already exists', () => { it('should not inject import if already exists', () => {
const content = `const { varName, otherName } = require("@myorg/awesome-config"); const content = `import { varName, otherName } from "@myorg/awesome-config";
const baseConfig = require("../../eslint.config.cjs"); import baseConfig from "../../eslint.config.mjs";
module.exports = [
export default [
...baseConfig, ...baseConfig,
{ {
files: [ files: [
@ -365,9 +369,10 @@ describe('ast-utils', () => {
}); });
it('should not update import if already exists', () => { it('should not update import if already exists', () => {
const content = `const varName = require("@myorg/awesome-config"); const content = `import { varName } from "@myorg/awesome-config";
const baseConfig = require("../../eslint.config.cjs"); import baseConfig from "../../eslint.config.mjs";
module.exports = [
export default [
...baseConfig, ...baseConfig,
{ {
files: [ files: [
@ -380,7 +385,7 @@ describe('ast-utils', () => {
];`; ];`;
const result = addImportToFlatConfig( const result = addImportToFlatConfig(
content, content,
'varName', ['varName'],
'@myorg/awesome-config' '@myorg/awesome-config'
); );
expect(result).toEqual(content); expect(result).toEqual(content);
@ -390,10 +395,11 @@ describe('ast-utils', () => {
describe('removeImportFromFlatConfig', () => { describe('removeImportFromFlatConfig', () => {
it('should remove existing import from config if the var name matches', () => { it('should remove existing import from config if the var name matches', () => {
const content = stripIndents` const content = stripIndents`
const nx = require("@nx/eslint-plugin"); import nx from "@nx/eslint-plugin";
const thisShouldRemain = require("@nx/eslint-plugin"); import thisShouldRemain from "@nx/eslint-plugin";
const playwright = require('eslint-plugin-playwright'); import playwright from 'eslint-plugin-playwright';
module.exports = [
export default [
playwright.configs['flat/recommended'], playwright.configs['flat/recommended'],
]; ];
`; `;
@ -404,9 +410,10 @@ describe('ast-utils', () => {
); );
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
" "
const thisShouldRemain = require("@nx/eslint-plugin"); import thisShouldRemain from "@nx/eslint-plugin";
const playwright = require('eslint-plugin-playwright'); import playwright from 'eslint-plugin-playwright';
module.exports = [
export default [
playwright.configs['flat/recommended'], playwright.configs['flat/recommended'],
];" ];"
`); `);
@ -415,8 +422,8 @@ describe('ast-utils', () => {
describe('addCompatToFlatConfig', () => { describe('addCompatToFlatConfig', () => {
it('should add compat to config', () => { it('should add compat to config', () => {
const content = `const baseConfig = require("../../eslint.config.cjs"); const content = `import baseConfig from "../../eslint.config.mjs";
module.exports = [ export default [
...baseConfig, ...baseConfig,
{ {
files: [ files: [
@ -429,15 +436,18 @@ describe('ast-utils', () => {
];`; ];`;
const result = addFlatCompatToFlatConfig(content); const result = addFlatCompatToFlatConfig(content);
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc"); "import { FlatCompat } from "@eslint/eslintrc";
const js = require("@eslint/js"); import { dirname } from "path";
const baseConfig = require("../../eslint.config.cjs"); import { fileURLToPath } from "url";
import js from "@eslint/js";
import baseConfig from "../../eslint.config.mjs";
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [
export default [
...baseConfig, ...baseConfig,
{ {
files: [ files: [
@ -452,9 +462,10 @@ describe('ast-utils', () => {
}); });
it('should add only partially compat to config if parts exist', () => { it('should add only partially compat to config if parts exist', () => {
const content = `const baseConfig = require("../../eslint.config.cjs"); const content = `import baseConfig from "../../eslint.config.mjs";
const js = require("@eslint/js"); import js from "@eslint/js";
module.exports = [
export default [
...baseConfig, ...baseConfig,
{ {
files: [ files: [
@ -467,15 +478,19 @@ describe('ast-utils', () => {
];`; ];`;
const result = addFlatCompatToFlatConfig(content); const result = addFlatCompatToFlatConfig(content);
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc"); "import { FlatCompat } from "@eslint/eslintrc";
const baseConfig = require("../../eslint.config.cjs"); import { dirname } from "path";
const js = require("@eslint/js"); import { fileURLToPath } from "url";
import baseConfig from "../../eslint.config.mjs";
import js from "@eslint/js";
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [
export default [
...baseConfig, ...baseConfig,
{ {
files: [ files: [
@ -490,16 +505,18 @@ describe('ast-utils', () => {
}); });
it('should not add compat to config if exist', () => { it('should not add compat to config if exist', () => {
const content = `const FlatCompat = require("@eslint/eslintrc"); const content = `import { FlatCompat } from "@eslint/eslintrc";
const baseConfig = require("../../eslint.config.cjs"); import baseConfig from "../../eslint.config.cjs";
const js = require("@eslint/js"); import js from "@eslint/js";
import { fileURLToPath } from "url";
import { dirname } from 'path';
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [ export default [
...baseConfig, ...baseConfig,
{ {
files: [ files: [
@ -517,16 +534,17 @@ describe('ast-utils', () => {
describe('removeOverridesFromLintConfig', () => { describe('removeOverridesFromLintConfig', () => {
it('should remove all rules from config', () => { it('should remove all rules from config', () => {
const content = `const FlatCompat = require("@eslint/eslintrc"); const content = `import { FlatCompat } from "@eslint/eslintrc";
const baseConfig = require("../../eslint.config.cjs"); import js from "@eslint/js";
const js = require("@eslint/js"); import { fileURLToPath } from "url";
import { dirname } from 'path';
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [ export default [
...baseConfig, ...baseConfig,
{ {
files: [ files: [
@ -557,26 +575,27 @@ describe('ast-utils', () => {
];`; ];`;
const result = removeOverridesFromLintConfig(content); const result = removeOverridesFromLintConfig(content);
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
"const FlatCompat = require("@eslint/eslintrc"); "import { FlatCompat } from "@eslint/eslintrc";
const baseConfig = require("../../eslint.config.cjs"); import js from "@eslint/js";
const js = require("@eslint/js"); import { fileURLToPath } from "url";
import { dirname } from 'path';
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [ export default [
...baseConfig, ...baseConfig,
{ ignores: ["my-lib/.cache/**/*"] }, { ignores: ["my-lib/.cache/**/*"] },
];" ];"
`); `);
}); });
it('should remove all rules from starting with first', () => { it('should remove all rules from starting with first', () => {
const content = `const baseConfig = require("../../eslint.config.cjs"); const content = `import baseConfig from "../../eslint.config.mjs";
module.exports = [ export default [
{ {
files: [ files: [
"my-lib/**/*.ts", "my-lib/**/*.ts",
@ -605,19 +624,19 @@ describe('ast-utils', () => {
];`; ];`;
const result = removeOverridesFromLintConfig(content); const result = removeOverridesFromLintConfig(content);
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
"const baseConfig = require("../../eslint.config.cjs"); "import baseConfig from "../../eslint.config.mjs";
module.exports = [ export default [
];" ];"
`); `);
}); });
}); });
describe('replaceOverride', () => { describe('replaceOverride', () => {
it('should find and replace rules in override', () => { it('should find and replace rules in override', () => {
const content = `const baseConfig = require("../../eslint.config.cjs"); const content = `import baseConfig from "../../eslint.config.mjs";
module.exports = [ export default [
{ {
files: [ files: [
"my-lib/**/*.ts", "my-lib/**/*.ts",
@ -657,9 +676,9 @@ module.exports = [
}) })
); );
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
"const baseConfig = require("../../eslint.config.cjs"); "import baseConfig from "../../eslint.config.mjs";
module.exports = [ export default [
{ {
"files": [ "files": [
"my-lib/**/*.ts", "my-lib/**/*.ts",
@ -692,9 +711,9 @@ module.exports = [
}); });
it('should append rules in override', () => { it('should append rules in override', () => {
const content = `const baseConfig = require("../../eslint.config.cjs"); const content = `import baseConfig from "../../eslint.config.mjs";
module.exports = [ export default [
{ {
files: [ files: [
"my-lib/**/*.ts", "my-lib/**/*.ts",
@ -728,9 +747,9 @@ module.exports = [
}) })
); );
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
"const baseConfig = require("../../eslint.config.cjs"); "import baseConfig from "../../eslint.config.mjs";
module.exports = [ export default [
{ {
"files": [ "files": [
"my-lib/**/*.ts", "my-lib/**/*.ts",
@ -755,9 +774,9 @@ module.exports = [
}); });
it('should work for compat overrides', () => { it('should work for compat overrides', () => {
const content = `const baseConfig = require("../../eslint.config.cjs"); const content = `import baseConfig from "../../eslint.config.mjs";
module.exports = [ export default [
...compat.config({ extends: ["plugin:@nx/typescript"] }).map(config => ({ ...compat.config({ extends: ["plugin:@nx/typescript"] }).map(config => ({
...config, ...config,
files: [ files: [
@ -783,9 +802,9 @@ module.exports = [
}) })
); );
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
"const baseConfig = require("../../eslint.config.cjs"); "import baseConfig from "../../eslint.config.mjs";
module.exports = [ export default [
...compat.config({ extends: ["plugin:@nx/typescript"] }).map(config => ({ ...compat.config({ extends: ["plugin:@nx/typescript"] }).map(config => ({
...config, ...config,
"files": [ "files": [
@ -804,14 +823,17 @@ module.exports = [
describe('removePlugin', () => { describe('removePlugin', () => {
it('should remove plugins from config', () => { it('should remove plugins from config', () => {
const content = `const { FlatCompat } = require("@eslint/eslintrc"); const content = `import { FlatCompat } from "@eslint/eslintrc";
const nxEslintPlugin = require("@nx/eslint-plugin"); import nxEslintPlugin from "@nx/eslint-plugin";
const js = require("@eslint/js"); import js = from ("@eslint/js");
import { fileURLToPath} from "url";
import { dirname } from 'path';
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: dirname(fileURLToPath(import.meta.url));
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [ export default [
{ plugins: { "@nx": nxEslintPlugin } }, { plugins: { "@nx": nxEslintPlugin } },
{ ignores: ["src/ignore/to/keep.ts"] }, { ignores: ["src/ignore/to/keep.ts"] },
{ ignores: ["something/else"] } { ignores: ["something/else"] }
@ -819,13 +841,16 @@ module.exports = [
const result = removePlugin(content, '@nx', '@nx/eslint-plugin'); const result = removePlugin(content, '@nx', '@nx/eslint-plugin');
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc"); "import { FlatCompat } from "@eslint/eslintrc";
const js = require("@eslint/js"); import js = from ("@eslint/js");
import { fileURLToPath} from "url";
import { dirname } from 'path';
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: dirname(fileURLToPath(import.meta.url));
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [ export default [
{ ignores: ["src/ignore/to/keep.ts"] }, { ignores: ["src/ignore/to/keep.ts"] },
{ ignores: ["something/else"] } { ignores: ["something/else"] }
];" ];"
@ -833,15 +858,19 @@ module.exports = [
}); });
it('should remove single plugin from config', () => { it('should remove single plugin from config', () => {
const content = `const { FlatCompat } = require("@eslint/eslintrc"); const content = `import { FlatCompat } from "@eslint/eslintrc";
const nxEslintPlugin = require("@nx/eslint-plugin"); import nxEslintPlugin from "@nx/eslint-plugin";
const otherPlugin = require("other/eslint-plugin"); import otherPlugin from "other/eslint-plugin";
const js = require("@eslint/js"); import js from "@eslint/js";
import { fileURLToPath } from "url";
import { dirname } from 'path';
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [
export default [
{ plugins: { "@nx": nxEslintPlugin, "@other": otherPlugin } }, { plugins: { "@nx": nxEslintPlugin, "@other": otherPlugin } },
{ ignores: ["src/ignore/to/keep.ts"] }, { ignores: ["src/ignore/to/keep.ts"] },
{ ignores: ["something/else"] } { ignores: ["something/else"] }
@ -849,14 +878,18 @@ module.exports = [
const result = removePlugin(content, '@nx', '@nx/eslint-plugin'); const result = removePlugin(content, '@nx', '@nx/eslint-plugin');
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc"); "import { FlatCompat } from "@eslint/eslintrc";
const otherPlugin = require("other/eslint-plugin"); import otherPlugin from "other/eslint-plugin";
const js = require("@eslint/js"); import js from "@eslint/js";
import { fileURLToPath } from "url";
import { dirname } from 'path';
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [
export default [
{ plugins: { "@other": otherPlugin } }, { plugins: { "@other": otherPlugin } },
{ ignores: ["src/ignore/to/keep.ts"] }, { ignores: ["src/ignore/to/keep.ts"] },
{ ignores: ["something/else"] } { ignores: ["something/else"] }
@ -865,14 +898,18 @@ module.exports = [
}); });
it('should leave other properties in config', () => { it('should leave other properties in config', () => {
const content = `const { FlatCompat } = require("@eslint/eslintrc"); const content = `import { FlatCompat } from "@eslint/eslintrc";
const nxEslintPlugin = require("@nx/eslint-plugin"); import nxEslintPlugin from "@nx/eslint-plugin";
const js = require("@eslint/js"); import js from "@eslint/js";
import { fileURLToPath } from "url";
import { dirname } from 'path';
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [
export default [
{ plugins: { "@nx": nxEslintPlugin }, rules: {} }, { plugins: { "@nx": nxEslintPlugin }, rules: {} },
{ ignores: ["src/ignore/to/keep.ts"] }, { ignores: ["src/ignore/to/keep.ts"] },
{ ignores: ["something/else"] } { ignores: ["something/else"] }
@ -880,13 +917,17 @@ module.exports = [
const result = removePlugin(content, '@nx', '@nx/eslint-plugin'); const result = removePlugin(content, '@nx', '@nx/eslint-plugin');
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc"); "import { FlatCompat } from "@eslint/eslintrc";
const js = require("@eslint/js"); import js from "@eslint/js";
import { fileURLToPath } from "url";
import { dirname } from 'path';
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [
export default [
{ rules: {} }, { rules: {} },
{ ignores: ["src/ignore/to/keep.ts"] }, { ignores: ["src/ignore/to/keep.ts"] },
{ ignores: ["something/else"] } { ignores: ["something/else"] }
@ -895,14 +936,18 @@ module.exports = [
}); });
it('should remove single plugin from config array', () => { it('should remove single plugin from config array', () => {
const content = `const { FlatCompat } = require("@eslint/eslintrc"); const content = `import { FlatCompat } from "@eslint/eslintrc";
const nxEslintPlugin = require("@nx/eslint-plugin"); import nxEslintPlugin from "@nx/eslint-plugin";
const js = require("@eslint/js"); import js from "@eslint/js";
import { fileURLToPath } from "url";
import { dirname } from 'path';
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [
export default [
{ plugins: ["@nx", "something-else"] }, { plugins: ["@nx", "something-else"] },
{ ignores: ["src/ignore/to/keep.ts"] }, { ignores: ["src/ignore/to/keep.ts"] },
{ ignores: ["something/else"] } { ignores: ["something/else"] }
@ -910,13 +955,17 @@ module.exports = [
const result = removePlugin(content, '@nx', '@nx/eslint-plugin'); const result = removePlugin(content, '@nx', '@nx/eslint-plugin');
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc"); "import { FlatCompat } from "@eslint/eslintrc";
const js = require("@eslint/js"); import js from "@eslint/js";
import { fileURLToPath } from "url";
import { dirname } from 'path';
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [
export default [
{ plugins:["something-else"] }, { plugins:["something-else"] },
{ ignores: ["src/ignore/to/keep.ts"] }, { ignores: ["src/ignore/to/keep.ts"] },
{ ignores: ["something/else"] } { ignores: ["something/else"] }
@ -925,14 +974,18 @@ module.exports = [
}); });
it('should leave other fields in the object', () => { it('should leave other fields in the object', () => {
const content = `const { FlatCompat } = require("@eslint/eslintrc"); const content = `import { FlatCompat } from "@eslint/eslintrc";
const nxEslintPlugin = require("@nx/eslint-plugin"); import nxEslintPlugin from "@nx/eslint-plugin";
const js = require("@eslint/js"); import js from "@eslint/js";
import { fileURLToPath } from "url";
import { dirname } from 'path';
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [
export default [
{ plugins: ["@nx"], rules: { } }, { plugins: ["@nx"], rules: { } },
{ ignores: ["src/ignore/to/keep.ts"] }, { ignores: ["src/ignore/to/keep.ts"] },
{ ignores: ["something/else"] } { ignores: ["something/else"] }
@ -940,13 +993,17 @@ module.exports = [
const result = removePlugin(content, '@nx', '@nx/eslint-plugin'); const result = removePlugin(content, '@nx', '@nx/eslint-plugin');
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc"); "import { FlatCompat } from "@eslint/eslintrc";
const js = require("@eslint/js"); import js from "@eslint/js";
import { fileURLToPath } from "url";
import { dirname } from 'path';
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [
export default [
{ rules: { } }, { rules: { } },
{ ignores: ["src/ignore/to/keep.ts"] }, { ignores: ["src/ignore/to/keep.ts"] },
{ ignores: ["something/else"] } { ignores: ["something/else"] }
@ -955,14 +1012,19 @@ module.exports = [
}); });
it('should remove entire plugin when array with single element', () => { it('should remove entire plugin when array with single element', () => {
const content = `const { FlatCompat } = require("@eslint/eslintrc"); const content = `import { FlatCompat } from "@eslint/eslintrc";
const nxEslintPlugin = require("@nx/eslint-plugin"); import nxEslintPlugin from "@nx/eslint-plugin";
const js = require("@eslint/js"); import js from "@eslint/js";
import { fileURLToPath } from "url";
import { dirname } from 'path';
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [
export default [
{ plugins: ["@nx"] }, { plugins: ["@nx"] },
{ ignores: ["src/ignore/to/keep.ts"] }, { ignores: ["src/ignore/to/keep.ts"] },
{ ignores: ["something/else"] } { ignores: ["something/else"] }
@ -970,13 +1032,18 @@ module.exports = [
const result = removePlugin(content, '@nx', '@nx/eslint-plugin'); const result = removePlugin(content, '@nx', '@nx/eslint-plugin');
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc"); "import { FlatCompat } from "@eslint/eslintrc";
const js = require("@eslint/js"); import js from "@eslint/js";
import { fileURLToPath } from "url";
import { dirname } from 'path';
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [
export default [
{ ignores: ["src/ignore/to/keep.ts"] }, { ignores: ["src/ignore/to/keep.ts"] },
{ ignores: ["something/else"] } { ignores: ["something/else"] }
];" ];"
@ -986,14 +1053,18 @@ module.exports = [
describe('removeCompatExtends', () => { describe('removeCompatExtends', () => {
it('should remove compat extends from config', () => { it('should remove compat extends from config', () => {
const content = `const { FlatCompat } = require("@eslint/eslintrc"); const content = `import { FlatCompat } from "@eslint/eslintrc";
const nxEslintPlugin = require("@nx/eslint-plugin"); import nxEslintPlugin from "@nx/eslint-plugin";
const js = require("@eslint/js"); import js from "@eslint/js";
import { fileURLToPath } from "url";
import { dirname } from "path";
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [
export default [
{ plugins: { "@nx": nxEslintPlugin } }, { plugins: { "@nx": nxEslintPlugin } },
...compat.config({ extends: ["plugin:@nx/typescript"] }).map(config => ({ ...compat.config({ extends: ["plugin:@nx/typescript"] }).map(config => ({
...config, ...config,
@ -1013,14 +1084,18 @@ module.exports = [
'plugin:@nx/javascript', 'plugin:@nx/javascript',
]); ]);
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc"); "import { FlatCompat } from "@eslint/eslintrc";
const nxEslintPlugin = require("@nx/eslint-plugin"); import nxEslintPlugin from "@nx/eslint-plugin";
const js = require("@eslint/js"); import js from "@eslint/js";
import { fileURLToPath } from "url";
import { dirname } from "path";
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [
export default [
{ plugins: { "@nx": nxEslintPlugin } }, { plugins: { "@nx": nxEslintPlugin } },
{ {
files: ['*.ts', '*.tsx', '*.js', '*.jsx'], files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
@ -1039,9 +1114,10 @@ module.exports = [
describe('removePredefinedConfigs', () => { describe('removePredefinedConfigs', () => {
it('should remove config objects and import', () => { it('should remove config objects and import', () => {
const content = stripIndents` const content = stripIndents`
const nx = require("@nx/eslint-plugin"); import nx from "@nx/eslint-plugin";
const playwright = require('eslint-plugin-playwright'); import playwright from 'eslint-plugin-playwright';
module.exports = [
export default [
...nx.config['flat/base'], ...nx.config['flat/base'],
...nx.config['flat/typescript'], ...nx.config['flat/typescript'],
...nx.config['flat/javascript'], ...nx.config['flat/javascript'],
@ -1058,8 +1134,9 @@ module.exports = [
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
" "
const playwright = require('eslint-plugin-playwright'); import playwright from 'eslint-plugin-playwright';
module.exports = [
export default [
playwright.configs['flat/recommended'], playwright.configs['flat/recommended'],
];" ];"
`); `);
@ -1067,9 +1144,10 @@ module.exports = [
it('should keep configs that are not in the list', () => { it('should keep configs that are not in the list', () => {
const content = stripIndents` const content = stripIndents`
const nx = require("@nx/eslint-plugin"); import nx from "@nx/eslint-plugin";
const playwright = require('eslint-plugin-playwright'); import playwright from 'eslint-plugin-playwright';
module.exports = [
export default [
...nx.config['flat/base'], ...nx.config['flat/base'],
...nx.config['flat/typescript'], ...nx.config['flat/typescript'],
...nx.config['flat/javascript'], ...nx.config['flat/javascript'],
@ -1086,9 +1164,10 @@ module.exports = [
); );
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
"const nx = require("@nx/eslint-plugin"); "import nx from "@nx/eslint-plugin";
const playwright = require('eslint-plugin-playwright'); import playwright from 'eslint-plugin-playwright';
module.exports = [
export default [
...nx.config['flat/react'], ...nx.config['flat/react'],
playwright.configs['flat/recommended'], playwright.configs['flat/recommended'],
];" ];"

View File

@ -26,7 +26,10 @@ export function removeOverridesFromLintConfig(content: string): string {
ts.ScriptKind.JS ts.ScriptKind.JS
); );
const exportsArray = findAllBlocks(source); const format = content.includes('export default') ? 'mjs' : 'cjs';
const exportsArray =
format === 'mjs' ? findExportDefault(source) : findModuleExports(source);
if (!exportsArray) { if (!exportsArray) {
return content; return content;
} }
@ -47,7 +50,19 @@ export function removeOverridesFromLintConfig(content: string): string {
return applyChangesToString(content, changes); return applyChangesToString(content, changes);
} }
function findAllBlocks(source: ts.SourceFile): ts.NodeArray<ts.Node> { // TODO Change name
function findExportDefault(source: ts.SourceFile): ts.NodeArray<ts.Node> {
return ts.forEachChild(source, function analyze(node) {
if (
ts.isExportAssignment(node) &&
ts.isArrayLiteralExpression(node.expression)
) {
return node.expression.elements;
}
});
}
function findModuleExports(source: ts.SourceFile): ts.NodeArray<ts.Node> {
return ts.forEachChild(source, function analyze(node) { return ts.forEachChild(source, function analyze(node) {
if ( if (
ts.isExpressionStatement(node) && ts.isExpressionStatement(node) &&
@ -86,7 +101,9 @@ export function hasOverride(
true, true,
ts.ScriptKind.JS ts.ScriptKind.JS
); );
const exportsArray = findAllBlocks(source); const format = content.includes('export default') ? 'mjs' : 'cjs';
const exportsArray =
format === 'mjs' ? findExportDefault(source) : findModuleExports(source);
if (!exportsArray) { if (!exportsArray) {
return false; return false;
} }
@ -120,6 +137,7 @@ function parseTextToJson(text: string): any {
.replace(/\s([a-zA-Z0-9_]+)\s*:/g, ' "$1": ') .replace(/\s([a-zA-Z0-9_]+)\s*:/g, ' "$1": ')
// stringify any require calls to avoid JSON parsing errors, turn them into just the string value being required // stringify any require calls to avoid JSON parsing errors, turn them into just the string value being required
.replace(/require\(['"]([^'"]+)['"]\)/g, '"$1"') .replace(/require\(['"]([^'"]+)['"]\)/g, '"$1"')
.replace(/\(?await import\(['"]([^'"]+)['"]\)\)?/g, '"$1"')
); );
} }
@ -141,7 +159,9 @@ export function replaceOverride(
true, true,
ts.ScriptKind.JS ts.ScriptKind.JS
); );
const exportsArray = findAllBlocks(source); const format = content.includes('export default') ? 'mjs' : 'cjs';
const exportsArray =
format === 'mjs' ? findExportDefault(source) : findModuleExports(source);
if (!exportsArray) { if (!exportsArray) {
return content; return content;
} }
@ -174,20 +194,24 @@ export function replaceOverride(
let updatedData = update(data); let updatedData = update(data);
if (updatedData) { if (updatedData) {
updatedData = mapFilePaths(updatedData); updatedData = mapFilePaths(updatedData);
const parserReplacement =
format === 'mjs'
? (parser: string) => `(await import('${parser}'))`
: (parser: string) => `require('${parser}')`;
changes.push({ changes.push({
type: ChangeType.Insert, type: ChangeType.Insert,
index: start, index: start,
// NOTE: Indentation added to format without formatting tools like Prettier.
text: text:
' ' + ' ' +
JSON.stringify(updatedData, null, 2) JSON.stringify(updatedData, null, 2)
// restore any parser require calls that were stripped during JSON parsing .replace(
.replace(/"parser": "([^"]+)"/g, (_, parser) => { /"parser": "([^"]+)"/g,
return `"parser": require('${parser}')`; (_, parser) => `"parser": ${parserReplacement(parser)}`
}) )
.slice(2, -2) // remove curly braces and start/end line breaks since we are injecting just properties .slice(2, -2) // Remove curly braces and start/end line breaks
// Append indentation so file is formatted without Prettier .replaceAll(/\n/g, '\n '), // Maintain indentation
.replaceAll(/\n/g, '\n '),
}); });
} }
} }
@ -198,7 +222,12 @@ export function replaceOverride(
} }
/** /**
* Adding require statement to the top of the file * Adding import statement to the top of the file
* The imports are added based on a few rules:
* 1. If it's a default import and matches the variable, return content unchanged.
* 2. If it's a named import and the variables are not part of the import object, add them.
* 3. If no existing import and variable is a string, add a default import.
* 4. If no existing import and variable is an array, add it as an object import.
*/ */
export function addImportToFlatConfig( export function addImportToFlatConfig(
content: string, content: string,
@ -214,6 +243,159 @@ export function addImportToFlatConfig(
ts.ScriptKind.JS ts.ScriptKind.JS
); );
const format = content.includes('export default') ? 'mjs' : 'cjs';
if (format === 'mjs') {
return addESMImportToFlatConfig(source, printer, content, variable, imp);
}
return addCJSImportToFlatConfig(source, printer, content, variable, imp);
}
function addESMImportToFlatConfig(
source: ts.SourceFile,
printer: ts.Printer,
content: string,
variable: string | string[],
imp: string
): string {
let existingImport: ts.ImportDeclaration | undefined;
ts.forEachChild(source, (node) => {
if (
ts.isImportDeclaration(node) &&
ts.isStringLiteral(node.moduleSpecifier) &&
node.moduleSpecifier.text === imp
) {
existingImport = node;
}
});
// Rule 1:
if (
existingImport &&
typeof variable === 'string' &&
existingImport.importClause?.name?.getText() === variable
) {
return content;
}
// Rule 2:
if (
existingImport &&
existingImport.importClause?.namedBindings &&
Array.isArray(variable)
) {
const namedImports = existingImport.importClause
.namedBindings as ts.NamedImports;
const existingElements = namedImports.elements;
// Filter out variables that are already imported
const newVariables = variable.filter(
(v) => !existingElements.some((e) => e.name.getText() === v)
);
if (newVariables.length === 0) {
return content;
}
const newImportSpecifiers = newVariables.map((v) =>
ts.factory.createImportSpecifier(
false,
undefined,
ts.factory.createIdentifier(v)
)
);
const lastElement = existingElements[existingElements.length - 1];
const insertIndex = lastElement
? lastElement.getEnd()
: namedImports.getEnd();
const insertText = printer.printList(
ts.ListFormat.NamedImportsOrExportsElements,
ts.factory.createNodeArray(newImportSpecifiers),
source
);
return applyChangesToString(content, [
{
type: ChangeType.Insert,
index: insertIndex,
text: `, ${insertText}`,
},
]);
}
// Rule 3:
if (!existingImport && typeof variable === 'string') {
const defaultImport = ts.factory.createImportDeclaration(
undefined,
ts.factory.createImportClause(
false,
ts.factory.createIdentifier(variable),
undefined
),
ts.factory.createStringLiteral(imp)
);
const insert = printer.printNode(
ts.EmitHint.Unspecified,
defaultImport,
source
);
return applyChangesToString(content, [
{
type: ChangeType.Insert,
index: 0,
text: `${insert}\n`,
},
]);
}
// Rule 4:
if (!existingImport && Array.isArray(variable)) {
const objectImport = ts.factory.createImportDeclaration(
undefined,
ts.factory.createImportClause(
false,
undefined,
ts.factory.createNamedImports(
variable.map((v) =>
ts.factory.createImportSpecifier(
false,
undefined,
ts.factory.createIdentifier(v)
)
)
)
),
ts.factory.createStringLiteral(imp)
);
const insert = printer.printNode(
ts.EmitHint.Unspecified,
objectImport,
source
);
return applyChangesToString(content, [
{
type: ChangeType.Insert,
index: 0,
text: `${insert}\n`,
},
]);
}
}
function addCJSImportToFlatConfig(
source: ts.SourceFile,
printer: ts.Printer,
content: string,
variable: string | string[],
imp: string
): string {
const foundBindingVars: ts.NodeArray<ts.BindingElement> = ts.forEachChild( const foundBindingVars: ts.NodeArray<ts.BindingElement> = ts.forEachChild(
source, source,
function analyze(node) { function analyze(node) {
@ -322,6 +504,22 @@ export function addImportToFlatConfig(
]); ]);
} }
function existsAsNamedOrDefaultImport(
node: ts.ImportDeclaration,
variable: string | string[]
) {
const isNamed =
node.importClause.namedBindings &&
ts.isNamedImports(node.importClause.namedBindings);
if (Array.isArray(variable)) {
return isNamed || variable.includes(node.importClause?.name?.getText());
}
return (
(node.importClause.namedBindings &&
ts.isNamedImports(node.importClause.namedBindings)) ||
node.importClause?.name?.getText() === variable
);
}
/** /**
* Remove an import from flat config * Remove an import from flat config
*/ */
@ -338,8 +536,49 @@ export function removeImportFromFlatConfig(
ts.ScriptKind.JS ts.ScriptKind.JS
); );
const format = content.includes('export default') ? 'mjs' : 'cjs';
if (format === 'mjs') {
return removeImportFromFlatConfigESM(source, content, variable, imp);
} else {
return removeImportFromFlatConfigCJS(source, content, variable, imp);
}
}
function removeImportFromFlatConfigESM(
source: ts.SourceFile,
content: string,
variable: string,
imp: string
): string {
const changes: StringChange[] = []; const changes: StringChange[] = [];
ts.forEachChild(source, (node) => {
// we can only combine object binding patterns
if (
ts.isImportDeclaration(node) &&
ts.isStringLiteral(node.moduleSpecifier) &&
node.moduleSpecifier.text === imp &&
node.importClause &&
existsAsNamedOrDefaultImport(node, variable)
) {
changes.push({
type: ChangeType.Delete,
start: node.pos,
length: node.end - node.pos,
});
}
});
return applyChangesToString(content, changes);
}
function removeImportFromFlatConfigCJS(
source: ts.SourceFile,
content: string,
variable: string,
imp: string
): string {
const changes: StringChange[] = [];
ts.forEachChild(source, (node) => { ts.forEachChild(source, (node) => {
// we can only combine object binding patterns // we can only combine object binding patterns
if ( if (
@ -367,7 +606,7 @@ export function removeImportFromFlatConfig(
} }
/** /**
* Injects new ts.expression to the end of the module.exports array. * Injects new ts.expression to the end of the module.exports or export default array.
*/ */
export function addBlockToFlatConfigExport( export function addBlockToFlatConfigExport(
content: string, content: string,
@ -385,6 +624,79 @@ export function addBlockToFlatConfigExport(
ts.ScriptKind.JS ts.ScriptKind.JS
); );
const format = content.includes('export default') ? 'mjs' : 'cjs';
// find the export default array statement
if (format === 'mjs') {
return addBlockToFlatConfigExportESM(
content,
config,
source,
printer,
options
);
} else {
return addBlockToFlatConfigExportCJS(
content,
config,
source,
printer,
options
);
}
}
function addBlockToFlatConfigExportESM(
content: string,
config: ts.Expression | ts.SpreadElement,
source: ts.SourceFile,
printer: ts.Printer,
options: { insertAtTheEnd?: boolean; checkBaseConfig?: boolean } = {
insertAtTheEnd: true,
}
): string {
const exportDefaultStatement = source.statements.find(
(statement): statement is ts.ExportAssignment =>
ts.isExportAssignment(statement) &&
ts.isArrayLiteralExpression(statement.expression)
);
if (!exportDefaultStatement) return content;
const exportArrayLiteral =
exportDefaultStatement.expression as ts.ArrayLiteralExpression;
const updatedArrayElements = options.insertAtTheEnd
? [...exportArrayLiteral.elements, config]
: [config, ...exportArrayLiteral.elements];
const updatedExportDefault = ts.factory.createExportAssignment(
undefined,
false,
ts.factory.createArrayLiteralExpression(updatedArrayElements, true)
);
// update the existing export default array
const updatedStatements = source.statements.map((statement) =>
statement === exportDefaultStatement ? updatedExportDefault : statement
);
const updatedSource = ts.factory.updateSourceFile(source, updatedStatements);
return printer
.printFile(updatedSource)
.replace(/export default/, '\nexport default');
}
function addBlockToFlatConfigExportCJS(
content: string,
config: ts.Expression | ts.SpreadElement,
source: ts.SourceFile,
printer: ts.Printer,
options: { insertAtTheEnd?: boolean; checkBaseConfig?: boolean } = {
insertAtTheEnd: true,
}
): string {
const exportsArray = ts.forEachChild(source, function analyze(node) { const exportsArray = ts.forEachChild(source, function analyze(node) {
if ( if (
ts.isExpressionStatement(node) && ts.isExpressionStatement(node) &&
@ -443,34 +755,57 @@ export function removePlugin(
true, true,
ts.ScriptKind.JS ts.ScriptKind.JS
); );
const format = content.includes('export default') ? 'mjs' : 'cjs';
const changes: StringChange[] = []; const changes: StringChange[] = [];
if (format === 'mjs') {
ts.forEachChild(source, function analyze(node) {
if (
ts.isImportDeclaration(node) &&
ts.isStringLiteral(node.moduleSpecifier) &&
node.moduleSpecifier.text === pluginImport
) {
const importClause = node.importClause;
if (
(importClause && importClause.name) ||
(importClause.namedBindings &&
ts.isNamedImports(importClause.namedBindings))
) {
changes.push({
type: ChangeType.Delete,
start: node.pos,
length: node.end - node.pos,
});
}
}
});
} else {
ts.forEachChild(source, function analyze(node) {
if (
ts.isVariableStatement(node) &&
ts.isVariableDeclaration(node.declarationList.declarations[0]) &&
ts.isCallExpression(node.declarationList.declarations[0].initializer) &&
node.declarationList.declarations[0].initializer.arguments.length &&
ts.isStringLiteral(
node.declarationList.declarations[0].initializer.arguments[0]
) &&
node.declarationList.declarations[0].initializer.arguments[0].text ===
pluginImport
) {
changes.push({
type: ChangeType.Delete,
start: node.pos,
length: node.end - node.pos,
});
}
});
}
ts.forEachChild(source, function analyze(node) { ts.forEachChild(source, function analyze(node) {
if ( if (
ts.isVariableStatement(node) && ts.isExportAssignment(node) &&
ts.isVariableDeclaration(node.declarationList.declarations[0]) && ts.isArrayLiteralExpression(node.expression)
ts.isCallExpression(node.declarationList.declarations[0].initializer) &&
node.declarationList.declarations[0].initializer.arguments.length &&
ts.isStringLiteral(
node.declarationList.declarations[0].initializer.arguments[0]
) &&
node.declarationList.declarations[0].initializer.arguments[0].text ===
pluginImport
) { ) {
changes.push({ const blockElements = node.expression.elements;
type: ChangeType.Delete,
start: node.pos,
length: node.end - node.pos,
});
}
});
ts.forEachChild(source, function analyze(node) {
if (
ts.isExpressionStatement(node) &&
ts.isBinaryExpression(node.expression) &&
node.expression.left.getText() === 'module.exports' &&
ts.isArrayLiteralExpression(node.expression.right)
) {
const blockElements = node.expression.right.elements;
blockElements.forEach((element) => { blockElements.forEach((element) => {
if (ts.isObjectLiteralExpression(element)) { if (ts.isObjectLiteralExpression(element)) {
const pluginsElem = element.properties.find( const pluginsElem = element.properties.find(
@ -583,7 +918,15 @@ export function removeCompatExtends(
ts.ScriptKind.JS ts.ScriptKind.JS
); );
const changes: StringChange[] = []; const changes: StringChange[] = [];
findAllBlocks(source)?.forEach((node) => { const format = content.includes('export default') ? 'mjs' : 'cjs';
const exportsArray =
format === 'mjs' ? findExportDefault(source) : findModuleExports(source);
if (!exportsArray) {
return content;
}
exportsArray.forEach((node) => {
if ( if (
ts.isSpreadElement(node) && ts.isSpreadElement(node) &&
ts.isCallExpression(node.expression) && ts.isCallExpression(node.expression) &&
@ -644,9 +987,16 @@ export function removePredefinedConfigs(
true, true,
ts.ScriptKind.JS ts.ScriptKind.JS
); );
const format = content.includes('export default') ? 'mjs' : 'cjs';
const changes: StringChange[] = []; const changes: StringChange[] = [];
let removeImport = true; let removeImport = true;
findAllBlocks(source)?.forEach((node) => { const exportsArray =
format === 'mjs' ? findExportDefault(source) : findModuleExports(source);
if (!exportsArray) {
return content;
}
exportsArray.forEach((node) => {
if ( if (
ts.isSpreadElement(node) && ts.isSpreadElement(node) &&
ts.isElementAccessExpression(node.expression) && ts.isElementAccessExpression(node.expression) &&
@ -709,14 +1059,23 @@ export function addPluginsToExportsBlock(
* Adds compat if missing to flat config * Adds compat if missing to flat config
*/ */
export function addFlatCompatToFlatConfig(content: string) { export function addFlatCompatToFlatConfig(content: string) {
let result = content; const result = addImportToFlatConfig(content, 'js', '@eslint/js');
result = addImportToFlatConfig(result, 'js', '@eslint/js'); const format = content.includes('export default') ? 'mjs' : 'cjs';
if (result.includes('const compat = new FlatCompat')) { if (result.includes('const compat = new FlatCompat')) {
return result; return result;
} }
result = addImportToFlatConfig(result, ['FlatCompat'], '@eslint/eslintrc');
const index = result.indexOf('module.exports'); if (format === 'mjs') {
return applyChangesToString(result, [ return addFlatCompatToFlatConfigESM(result);
} else {
return addFlatCompatToFlatConfigCJS(result);
}
}
function addFlatCompatToFlatConfigCJS(content: string) {
content = addImportToFlatConfig(content, ['FlatCompat'], '@eslint/eslintrc');
const index = content.indexOf('module.exports');
return applyChangesToString(content, [
{ {
type: ChangeType.Insert, type: ChangeType.Insert,
index: index - 1, index: index - 1,
@ -729,6 +1088,32 @@ const compat = new FlatCompat({
}, },
]); ]);
} }
function addFlatCompatToFlatConfigESM(content: string) {
const importsToAdd = [
{ variable: 'js', module: '@eslint/js' },
{ variable: ['fileURLToPath'], module: 'url' },
{ variable: ['dirname'], module: 'path' },
{ variable: ['FlatCompat'], module: '@eslint/eslintrc' },
];
for (const { variable, module } of importsToAdd) {
content = addImportToFlatConfig(content, variable, module);
}
const index = content.indexOf('export default');
return applyChangesToString(content, [
{
type: ChangeType.Insert,
index: index - 1,
text: `
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});\n
`,
},
]);
}
/** /**
* Generate node list representing the imports and the exports blocks * Generate node list representing the imports and the exports blocks
@ -736,17 +1121,26 @@ const compat = new FlatCompat({
*/ */
export function createNodeList( export function createNodeList(
importsMap: Map<string, string>, importsMap: Map<string, string>,
exportElements: ts.Expression[] exportElements: ts.Expression[],
format: 'mjs' | 'cjs'
): ts.NodeArray< ): ts.NodeArray<
ts.VariableStatement | ts.Identifier | ts.ExpressionStatement | ts.SourceFile ts.VariableStatement | ts.Identifier | ts.ExpressionStatement | ts.SourceFile
> { > {
const importsList = []; const importsList = [];
// generateRequire(varName, imp, ts.factory);
Array.from(importsMap.entries()).forEach(([imp, varName]) => { Array.from(importsMap.entries()).forEach(([imp, varName]) => {
importsList.push(generateRequire(varName, imp)); if (format === 'mjs') {
importsList.push(generateESMImport(varName, imp));
} else {
importsList.push(generateRequire(varName, imp));
}
}); });
const exports =
format === 'mjs'
? generateESMExport(exportElements)
: generateCJSExport(exportElements);
return ts.factory.createNodeArray([ return ts.factory.createNodeArray([
// add plugin imports // add plugin imports
...importsList, ...importsList,
@ -757,21 +1151,33 @@ export function createNodeList(
false, false,
ts.ScriptKind.JS ts.ScriptKind.JS
), ),
// creates: exports,
// module.exports = [ ... ];
ts.factory.createExpressionStatement(
ts.factory.createBinaryExpression(
ts.factory.createPropertyAccessExpression(
ts.factory.createIdentifier('module'),
ts.factory.createIdentifier('exports')
),
ts.factory.createToken(ts.SyntaxKind.EqualsToken),
ts.factory.createArrayLiteralExpression(exportElements, true)
)
),
]); ]);
} }
function generateESMExport(elements: ts.Expression[]): ts.ExportAssignment {
// creates: export default = [...]
return ts.factory.createExportAssignment(
undefined,
false,
ts.factory.createArrayLiteralExpression(elements, true)
);
}
function generateCJSExport(elements: ts.Expression[]): ts.ExpressionStatement {
// creates: module.exports = [...]
return ts.factory.createExpressionStatement(
ts.factory.createBinaryExpression(
ts.factory.createPropertyAccessExpression(
ts.factory.createIdentifier('module'),
ts.factory.createIdentifier('exports')
),
ts.factory.createToken(ts.SyntaxKind.EqualsToken),
ts.factory.createArrayLiteralExpression(elements, true)
)
);
}
export function generateSpreadElement(name: string): ts.SpreadElement { export function generateSpreadElement(name: string): ts.SpreadElement {
return ts.factory.createSpreadElement(ts.factory.createIdentifier(name)); return ts.factory.createSpreadElement(ts.factory.createIdentifier(name));
} }
@ -831,17 +1237,20 @@ export function stringifyNodeList(
true, true,
ts.ScriptKind.JS ts.ScriptKind.JS
); );
return ( const result = printer
printer .printList(ts.ListFormat.MultiLine, nodes, resultFile)
.printList(ts.ListFormat.MultiLine, nodes, resultFile) // add new line before compat initialization
// add new line before compat initialization .replace(
.replace( /const compat = new FlatCompat/,
/const compat = new FlatCompat/, '\nconst compat = new FlatCompat'
'\nconst compat = new FlatCompat' );
)
// add new line before module.exports = ... if (result.includes('export default')) {
.replace(/module\.exports/, '\nmodule.exports') return result // add new line before export default = ...
); .replace(/export default/, '\nexport default');
} else {
return result.replace(/module.exports/, '\nmodule.exports');
}
} }
/** /**
@ -871,6 +1280,50 @@ export function generateRequire(
); );
} }
// Top level imports
export function generateESMImport(
variableName: string | ts.ObjectBindingPattern,
imp: string
): ts.ImportDeclaration {
let importClause;
if (typeof variableName === 'string') {
// For single variable import e.g import foo from 'module';
importClause = ts.factory.createImportClause(
false,
ts.factory.createIdentifier(variableName),
undefined
);
} else {
// For object binding pattern import e.g import { a, b, c } from 'module';
importClause = ts.factory.createImportClause(
false,
undefined,
ts.factory.createNamedImports(
variableName.elements.map((element) => {
const propertyName = element.propertyName
? ts.isIdentifier(element.propertyName)
? element.propertyName
: ts.factory.createIdentifier(element.propertyName.getText())
: undefined;
return ts.factory.createImportSpecifier(
false,
propertyName,
element.name as ts.Identifier
);
})
)
);
}
return ts.factory.createImportDeclaration(
undefined,
importClause,
ts.factory.createStringLiteral(imp)
);
}
/** /**
* FROM: https://github.com/eslint/rewrite/blob/e2a7ec809db20e638abbad250d105ddbde88a8d5/packages/migrate-config/src/migrate-config.js#L222 * FROM: https://github.com/eslint/rewrite/blob/e2a7ec809db20e638abbad250d105ddbde88a8d5/packages/migrate-config/src/migrate-config.js#L222
* *
@ -904,7 +1357,8 @@ export function overrideNeedsCompat(
export function generateFlatOverride( export function generateFlatOverride(
_override: Partial<Linter.ConfigOverride<Linter.RulesRecord>> & { _override: Partial<Linter.ConfigOverride<Linter.RulesRecord>> & {
ignores?: Linter.FlatConfig['ignores']; ignores?: Linter.FlatConfig['ignores'];
} },
format: 'mjs' | 'cjs'
): ts.ObjectLiteralExpression | ts.SpreadElement { ): ts.ObjectLiteralExpression | ts.SpreadElement {
const override = mapFilePaths(_override); const override = mapFilePaths(_override);
@ -981,21 +1435,10 @@ export function generateFlatOverride(
} }
return propertyAssignment; return propertyAssignment;
} else { } else {
// Change parser to require statement. // Change parser to import statement.
return ts.factory.createPropertyAssignment( return format === 'mjs'
'parser', ? generateESMParserImport(override)
ts.factory.createCallExpression( : generateCJSParserImport(override);
ts.factory.createIdentifier('require'),
undefined,
[
ts.factory.createStringLiteral(
override['languageOptions']?.['parserOptions']?.parser ??
override['languageOptions']?.parser ??
override.parser
),
]
)
);
} }
}, },
}); });
@ -1103,6 +1546,50 @@ export function generateFlatOverride(
); );
} }
function generateESMParserImport(
override: Partial<Linter.ConfigOverride<Linter.RulesRecord>> & {
ignores?: Linter.FlatConfig['ignores'];
}
): ts.PropertyAssignment {
return ts.factory.createPropertyAssignment(
'parser',
ts.factory.createAwaitExpression(
ts.factory.createCallExpression(
ts.factory.createIdentifier('import'),
undefined,
[
ts.factory.createStringLiteral(
override['languageOptions']?.['parserOptions']?.parser ??
override['languageOptions']?.parser ??
override.parser
),
]
)
)
);
}
function generateCJSParserImport(
override: Partial<Linter.ConfigOverride<Linter.RulesRecord>> & {
ignores?: Linter.FlatConfig['ignores'];
}
): ts.PropertyAssignment {
return ts.factory.createPropertyAssignment(
'parser',
ts.factory.createCallExpression(
ts.factory.createIdentifier('require'),
undefined,
[
ts.factory.createStringLiteral(
override['languageOptions']?.['parserOptions']?.parser ??
override['languageOptions']?.parser ??
override.parser
),
]
)
);
}
export function generateFlatPredefinedConfig( export function generateFlatPredefinedConfig(
predefinedConfigName: string, predefinedConfigName: string,
moduleName = 'nx', moduleName = 'nx',

View File

@ -21,7 +21,7 @@ import { globWithWorkspaceContext } from 'nx/src/utils/workspace-context';
import { gte } from 'semver'; import { gte } from 'semver';
import { import {
baseEsLintConfigFile, baseEsLintConfigFile,
baseEsLintFlatConfigFile, BASE_ESLINT_CONFIG_FILENAMES,
ESLINT_CONFIG_FILENAMES, ESLINT_CONFIG_FILENAMES,
isFlatConfig, isFlatConfig,
} from '../utils/config-file'; } from '../utils/config-file';
@ -405,7 +405,7 @@ function getProjectUsingESLintConfig(
): CreateNodesResult['projects'][string] | null { ): CreateNodesResult['projects'][string] | null {
const rootEslintConfig = [ const rootEslintConfig = [
baseEsLintConfigFile, baseEsLintConfigFile,
baseEsLintFlatConfigFile, ...BASE_ESLINT_CONFIG_FILENAMES,
...ESLINT_CONFIG_FILENAMES, ...ESLINT_CONFIG_FILENAMES,
].find((f) => existsSync(join(context.workspaceRoot, f))); ].find((f) => existsSync(join(context.workspaceRoot, f)));

View File

@ -1,6 +1,9 @@
import { existsSync, statSync } from 'fs'; import { existsSync, statSync } from 'fs';
import { basename, dirname, join, resolve } from 'path'; import { basename, dirname, join, resolve } from 'path';
import { eslintFlatConfigFilenames } from './flat-config'; import {
baseEslintConfigFilenames,
eslintFlatConfigFilenames,
} from './flat-config';
export const ESLINT_FLAT_CONFIG_FILENAMES = eslintFlatConfigFilenames; export const ESLINT_FLAT_CONFIG_FILENAMES = eslintFlatConfigFilenames;
@ -18,8 +21,10 @@ export const ESLINT_CONFIG_FILENAMES = [
...ESLINT_FLAT_CONFIG_FILENAMES, ...ESLINT_FLAT_CONFIG_FILENAMES,
]; ];
export const BASE_ESLINT_CONFIG_FILENAMES = baseEslintConfigFilenames;
export const baseEsLintConfigFile = '.eslintrc.base.json'; export const baseEsLintConfigFile = '.eslintrc.base.json';
export const baseEsLintFlatConfigFile = 'eslint.base.config.cjs'; export const baseEsLintFlatConfigFile = 'eslint.base.config.mjs';
// Make sure we can handle previous file extension as well for migrations or custom generators. // Make sure we can handle previous file extension as well for migrations or custom generators.
export const legacyBaseEsLintFlatConfigFile = 'eslint.base.config.js'; export const legacyBaseEsLintFlatConfigFile = 'eslint.base.config.js';

View File

@ -1,10 +1,17 @@
import { Tree } from '@nx/devkit'; import { Tree } from '@nx/devkit';
import { gte } from 'semver'; import { gte } from 'semver';
// todo: add support for eslint.config.mjs,
export const eslintFlatConfigFilenames = [ export const eslintFlatConfigFilenames = [
'eslint.config.cjs', 'eslint.config.cjs',
'eslint.config.js', 'eslint.config.js',
'eslint.config.mjs',
];
export const baseEslintConfigFilenames = [
'eslint.base.js',
'eslint.base.config.cjs',
'eslint.base.config.js',
'eslint.base.config.mjs',
]; ];
export function getRootESLintFlatConfigFilename(tree: Tree): string { export function getRootESLintFlatConfigFilename(tree: Tree): string {

View File

@ -623,7 +623,41 @@ describe('app', () => {
describe('--linter', () => { describe('--linter', () => {
describe('default (eslint)', () => { describe('default (eslint)', () => {
it('should add flat config as needed', async () => { it('should add flat config as needed MJS', async () => {
tree.write('eslint.config.mjs', 'export default {};');
const name = uniq();
await applicationGenerator(tree, {
directory: name,
style: 'css',
});
expect(tree.read(`${name}/eslint.config.mjs`, 'utf-8'))
.toMatchInlineSnapshot(`
"import { FlatCompat } from '@eslint/eslintrc';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import js from '@eslint/js';
import nx from '@nx/eslint-plugin';
import baseConfig from '../eslint.config.mjs';
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
export default [
...compat.extends('next', 'next/core-web-vitals'),
...baseConfig,
...nx.configs['flat/react-typescript'],
{
ignores: ['.next/**/*'],
},
];
"
`);
});
it('should add flat config as needed CJS', async () => {
tree.write('eslint.config.cjs', ''); tree.write('eslint.config.cjs', '');
const name = uniq(); const name = uniq();

View File

@ -33,7 +33,7 @@ exports[`app generated files content - as-provided - my-app general application
" "
`; `;
exports[`app generated files content - as-provided - my-app general application should configure eslint correctly (flat config) 1`] = ` exports[`app generated files content - as-provided - my-app general application should configure eslint correctly (flat config CJS) 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc'); "const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js'); const js = require('@eslint/js');
const baseConfig = require('../eslint.config.cjs'); const baseConfig = require('../eslint.config.cjs');
@ -66,6 +66,40 @@ module.exports = [
" "
`; `;
exports[`app generated files content - as-provided - my-app general application should configure eslint correctly (flat config ESM) 1`] = `
"import { FlatCompat } from '@eslint/eslintrc';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import js from '@eslint/js';
import baseConfig from '../eslint.config.mjs';
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
export default [
...baseConfig,
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.vue'],
// Override or add rules here
rules: {},
},
...compat.extends('@nuxt/eslint-config'),
{
files: ['**/*.vue'],
languageOptions: {
parserOptions: {
parser: await import('@typescript-eslint/parser'),
},
},
},
{
ignores: ['.nuxt/**', '.output/**', 'node_modules'],
},
];
"
`;
exports[`app generated files content - as-provided - my-app general application should configure nuxt correctly 1`] = ` exports[`app generated files content - as-provided - my-app general application should configure nuxt correctly 1`] = `
"import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; "import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
import { defineNuxtConfig } from 'nuxt/config'; import { defineNuxtConfig } from 'nuxt/config';
@ -393,7 +427,7 @@ exports[`app generated files content - as-provided - myApp general application s
" "
`; `;
exports[`app generated files content - as-provided - myApp general application should configure eslint correctly (flat config) 1`] = ` exports[`app generated files content - as-provided - myApp general application should configure eslint correctly (flat config CJS) 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc'); "const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js'); const js = require('@eslint/js');
const baseConfig = require('../eslint.config.cjs'); const baseConfig = require('../eslint.config.cjs');
@ -426,6 +460,40 @@ module.exports = [
" "
`; `;
exports[`app generated files content - as-provided - myApp general application should configure eslint correctly (flat config ESM) 1`] = `
"import { FlatCompat } from '@eslint/eslintrc';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import js from '@eslint/js';
import baseConfig from '../eslint.config.mjs';
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
export default [
...baseConfig,
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.vue'],
// Override or add rules here
rules: {},
},
...compat.extends('@nuxt/eslint-config'),
{
files: ['**/*.vue'],
languageOptions: {
parserOptions: {
parser: await import('@typescript-eslint/parser'),
},
},
},
{
ignores: ['.nuxt/**', '.output/**', 'node_modules'],
},
];
"
`;
exports[`app generated files content - as-provided - myApp general application should configure nuxt correctly 1`] = ` exports[`app generated files content - as-provided - myApp general application should configure nuxt correctly 1`] = `
"import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; "import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
import { defineNuxtConfig } from 'nuxt/config'; import { defineNuxtConfig } from 'nuxt/config';

View File

@ -65,8 +65,21 @@ describe('app', () => {
).toMatchSnapshot(); ).toMatchSnapshot();
}); });
it('should configure eslint correctly (flat config)', async () => { it('should configure eslint correctly (flat config ESM)', async () => {
tree.write('eslint.config.cjs', ''); tree.write('eslint.config.mjs', 'export default {};');
await applicationGenerator(tree, {
directory: name,
unitTestRunner: 'vitest',
});
expect(
tree.read(`${name}/eslint.config.mjs`, 'utf-8')
).toMatchSnapshot();
});
it('should configure eslint correctly (flat config CJS)', async () => {
tree.write('eslint.config.cjs', 'module.exports = {};');
await applicationGenerator(tree, { await applicationGenerator(tree, {
directory: name, directory: name,

View File

@ -277,7 +277,7 @@ exports[`library should ignore test files in tsconfig.lib.json 1`] = `
] ]
`; `;
exports[`library should support eslint flat config 1`] = ` exports[`library should support eslint flat config CJS 1`] = `
"const vue = require('eslint-plugin-vue'); "const vue = require('eslint-plugin-vue');
const baseConfig = require('../eslint.config.cjs'); const baseConfig = require('../eslint.config.cjs');
@ -301,3 +301,28 @@ module.exports = [
]; ];
" "
`; `;
exports[`library should support eslint flat config ESM 1`] = `
"import vue from 'eslint-plugin-vue';
import baseConfig from '../eslint.config.mjs';
export default [
...baseConfig,
...vue.configs['flat/recommended'],
{
files: ['**/*.vue'],
languageOptions: {
parserOptions: {
parser: await import('@typescript-eslint/parser'),
},
},
},
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.vue'],
rules: {
'vue/multi-word-component-names': 'off',
},
},
];
"
`;

View File

@ -149,7 +149,7 @@ describe('library', () => {
expect(eslintJson).toMatchSnapshot(); expect(eslintJson).toMatchSnapshot();
}); });
it('should support eslint flat config', async () => { it('should support eslint flat config CJS', async () => {
tree.write( tree.write(
'eslint.config.cjs', 'eslint.config.cjs',
`const { FlatCompat } = require('@eslint/eslintrc'); `const { FlatCompat } = require('@eslint/eslintrc');
@ -217,6 +217,76 @@ module.exports = [
); );
}); });
it('should support eslint flat config ESM', async () => {
tree.write(
'eslint.config.mjs',
`import { FlatCompat } from '@eslint/eslintrc';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import js from '@eslint/js';
import nx from '@nx/eslint-plugin';
import baseConfig from '../eslint.config.mjs';
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
export default [
{ plugins: { '@nx': nxEslintPlugin } },
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
rules: {
'@nx/enforce-module-boundaries': [
'error',
{
enforceBuildableLibDependency: true,
allow: [],
depConstraints: [
{
sourceTag: '*',
onlyDependOnLibsWithTags: ['*'],
},
],
},
],
},
},
...compat.config({ extends: ['plugin:@nx/typescript'] }).map((config) => ({
...config,
files: ['**/*.ts', '**/*.tsx'],
rules: {
...config.rules,
},
})),
...compat.config({ extends: ['plugin:@nx/javascript'] }).map((config) => ({
...config,
files: ['**/*.js', '**/*.jsx'],
rules: {
...config.rules,
},
})),
...compat.config({ env: { jest: true } }).map((config) => ({
...config,
files: ['**/*.spec.ts', '**/*.spec.tsx', '**/*.spec.js', '**/*.spec.jsx'],
rules: {
...config.rules,
},
})),
]`
);
await libraryGenerator(tree, defaultSchema);
const eslintJson = tree.read('my-lib/eslint.config.mjs', 'utf-8');
expect(eslintJson).toMatchSnapshot();
// assert **/*.vue was added to override in base eslint config
const eslintBaseJson = tree.read('eslint.config.mjs', 'utf-8');
expect(eslintBaseJson).toContain(
`files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.vue'],`
);
});
describe('nested', () => { describe('nested', () => {
it('should update tags and implicitDependencies', async () => { it('should update tags and implicitDependencies', async () => {
await libraryGenerator(tree, { await libraryGenerator(tree, {

View File

@ -16,9 +16,11 @@ export function updateEslintConfig(
!tree.exists('.eslintrc.json') && !tree.exists('.eslintrc.json') &&
!tree.exists('eslint.config.js') && !tree.exists('eslint.config.js') &&
!tree.exists('eslint.config.cjs') && !tree.exists('eslint.config.cjs') &&
!tree.exists('eslint.config.mjs') &&
!tree.exists('.eslintrc.base.json') && !tree.exists('.eslintrc.base.json') &&
!tree.exists('eslint.base.config.js') && !tree.exists('eslint.base.config.js') &&
!tree.exists('eslint.base.config.cjs') !tree.exists('eslint.base.config.cjs') &&
!tree.exists('eslint.base.config.mjs')
) { ) {
return; return;
} }