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`
1277 lines
40 KiB
TypeScript
1277 lines
40 KiB
TypeScript
import 'nx/src/internal-testing-utils/mock-project-graph';
|
|
|
|
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
|
import {
|
|
NxJsonConfiguration,
|
|
ProjectConfiguration,
|
|
Tree,
|
|
addProjectConfiguration,
|
|
readJson,
|
|
updateJson,
|
|
} from '@nx/devkit';
|
|
|
|
import { convertToFlatConfigGenerator } from './generator';
|
|
import { ConvertToFlatConfigGeneratorSchema } from './schema';
|
|
import { lintProjectGenerator } from '../lint-project/lint-project';
|
|
import { Linter } from '../utils/linter';
|
|
import { eslintrcVersion } from '../../utils/versions';
|
|
import { dump } from '@zkochan/js-yaml';
|
|
|
|
describe('convert-to-flat-config generator', () => {
|
|
let tree: Tree;
|
|
|
|
// TODO(@meeroslav): add plugin in these tests
|
|
|
|
beforeEach(() => {
|
|
tree = createTreeWithEmptyWorkspace();
|
|
addProjectConfiguration(tree, 'test-lib', {
|
|
root: 'libs/test-lib',
|
|
targets: {},
|
|
});
|
|
updateJson(tree, 'nx.json', (json: NxJsonConfiguration) => {
|
|
json.targetDefaults = {
|
|
lint: {
|
|
inputs: ['default'],
|
|
},
|
|
};
|
|
json.namedInputs = {
|
|
default: ['{projectRoot}/**/*', 'sharedGlobals'],
|
|
production: [
|
|
'default',
|
|
'!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)',
|
|
],
|
|
sharedGlobals: [],
|
|
};
|
|
return json;
|
|
});
|
|
});
|
|
|
|
describe('CJS', () => {
|
|
const options: ConvertToFlatConfigGeneratorSchema = {
|
|
skipFormat: false,
|
|
eslintConfigFormat: 'cjs',
|
|
};
|
|
it('should update dependencies', async () => {
|
|
await lintProjectGenerator(tree, {
|
|
skipFormat: false,
|
|
linter: Linter.EsLint,
|
|
project: 'test-lib',
|
|
setParserOptionsProject: false,
|
|
});
|
|
await convertToFlatConfigGenerator(tree, options);
|
|
|
|
expect(tree.read('package.json', 'utf-8')).toMatchInlineSnapshot(`
|
|
"{
|
|
"name": "@proj/source",
|
|
"dependencies": {},
|
|
"devDependencies": {
|
|
"@eslint/eslintrc": "^2.1.1",
|
|
"@nx/eslint": "0.0.1",
|
|
"@nx/eslint-plugin": "0.0.1",
|
|
"eslint": "^9.8.0",
|
|
"eslint-config-prettier": "^9.0.0",
|
|
"typescript-eslint": "^8.19.0"
|
|
}
|
|
}
|
|
"
|
|
`);
|
|
});
|
|
|
|
it('should convert json successfully', async () => {
|
|
await lintProjectGenerator(tree, {
|
|
skipFormat: false,
|
|
linter: Linter.EsLint,
|
|
project: 'test-lib',
|
|
setParserOptionsProject: false,
|
|
eslintConfigFormat: 'cjs',
|
|
});
|
|
await convertToFlatConfigGenerator(tree, options);
|
|
|
|
expect(tree.exists('eslint.config.cjs')).toBeTruthy();
|
|
expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchSnapshot();
|
|
expect(tree.exists('libs/test-lib/eslint.config.cjs')).toBeTruthy();
|
|
expect(
|
|
tree.read('libs/test-lib/eslint.config.cjs', 'utf-8')
|
|
).toMatchSnapshot();
|
|
// check nx.json changes
|
|
const nxJson = readJson(tree, 'nx.json');
|
|
expect(nxJson.targetDefaults.lint.inputs).toContain(
|
|
'{workspaceRoot}/eslint.config.cjs'
|
|
);
|
|
expect(nxJson.namedInputs.production).toContain(
|
|
'!{projectRoot}/eslint.config.cjs'
|
|
);
|
|
});
|
|
|
|
it('should convert yaml successfully', async () => {
|
|
await lintProjectGenerator(tree, {
|
|
skipFormat: false,
|
|
linter: Linter.EsLint,
|
|
eslintFilePatterns: ['**/*.ts'],
|
|
project: 'test-lib',
|
|
setParserOptionsProject: false,
|
|
eslintConfigFormat: 'cjs',
|
|
});
|
|
const yamlContent = dump(readJson(tree, 'libs/test-lib/.eslintrc.json'));
|
|
tree.delete('libs/test-lib/.eslintrc.json');
|
|
tree.write('libs/test-lib/.eslintrc.yaml', yamlContent);
|
|
|
|
await convertToFlatConfigGenerator(tree, options);
|
|
|
|
expect(tree.exists('eslint.config.cjs')).toBeTruthy();
|
|
expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchSnapshot();
|
|
expect(tree.exists('libs/test-lib/eslint.config.cjs')).toBeTruthy();
|
|
expect(
|
|
tree.read('libs/test-lib/eslint.config.cjs', 'utf-8')
|
|
).toMatchSnapshot();
|
|
// check nx.json changes
|
|
const nxJson = readJson(tree, 'nx.json');
|
|
expect(nxJson.targetDefaults.lint.inputs).toContain(
|
|
'{workspaceRoot}/eslint.config.cjs'
|
|
);
|
|
expect(nxJson.namedInputs.production).toContain(
|
|
'!{projectRoot}/eslint.config.cjs'
|
|
);
|
|
});
|
|
|
|
it('should convert yml successfully', async () => {
|
|
await lintProjectGenerator(tree, {
|
|
skipFormat: false,
|
|
linter: Linter.EsLint,
|
|
eslintFilePatterns: ['**/*.ts'],
|
|
project: 'test-lib',
|
|
setParserOptionsProject: false,
|
|
eslintConfigFormat: 'cjs',
|
|
});
|
|
const yamlContent = dump(readJson(tree, 'libs/test-lib/.eslintrc.json'));
|
|
tree.delete('libs/test-lib/.eslintrc.json');
|
|
tree.write('libs/test-lib/.eslintrc.yml', yamlContent);
|
|
|
|
await convertToFlatConfigGenerator(tree, options);
|
|
|
|
expect(tree.exists('eslint.config.cjs')).toBeTruthy();
|
|
expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchSnapshot();
|
|
expect(tree.exists('libs/test-lib/eslint.config.cjs')).toBeTruthy();
|
|
expect(
|
|
tree.read('libs/test-lib/eslint.config.cjs', 'utf-8')
|
|
).toMatchSnapshot();
|
|
// check nx.json changes
|
|
const nxJson = readJson(tree, 'nx.json');
|
|
expect(nxJson.targetDefaults.lint.inputs).toContain(
|
|
'{workspaceRoot}/eslint.config.cjs'
|
|
);
|
|
expect(nxJson.namedInputs.production).toContain(
|
|
'!{projectRoot}/eslint.config.cjs'
|
|
);
|
|
});
|
|
|
|
it('should add plugin extends', async () => {
|
|
await lintProjectGenerator(tree, {
|
|
skipFormat: false,
|
|
linter: Linter.EsLint,
|
|
project: 'test-lib',
|
|
setParserOptionsProject: false,
|
|
eslintConfigFormat: 'cjs',
|
|
});
|
|
updateJson(tree, '.eslintrc.json', (json) => {
|
|
json.extends = ['plugin:storybook/recommended'];
|
|
return json;
|
|
});
|
|
await convertToFlatConfigGenerator(tree, options);
|
|
|
|
expect(tree.read('eslint.config.cjs', 'utf-8')).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'],
|
|
},
|
|
...compat.extends('plugin:storybook/recommended'),
|
|
{ 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,
|
|
},
|
|
})),
|
|
];
|
|
"
|
|
`);
|
|
expect(tree.read('libs/test-lib/eslint.config.cjs', 'utf-8'))
|
|
.toMatchInlineSnapshot(`
|
|
"const baseConfig = require('../../eslint.config.cjs');
|
|
|
|
module.exports = [
|
|
{
|
|
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: {},
|
|
},
|
|
];
|
|
"
|
|
`);
|
|
expect(
|
|
readJson(tree, 'package.json').devDependencies['@eslint/eslintrc']
|
|
).toEqual(eslintrcVersion);
|
|
});
|
|
|
|
it('should add global eslintignores', async () => {
|
|
await lintProjectGenerator(tree, {
|
|
skipFormat: false,
|
|
linter: Linter.EsLint,
|
|
project: 'test-lib',
|
|
setParserOptionsProject: false,
|
|
});
|
|
tree.write('.eslintignore', 'ignore/me');
|
|
await convertToFlatConfigGenerator(tree, options);
|
|
|
|
const config = tree.read('eslint.config.cjs', 'utf-8');
|
|
expect(config).toContain('ignore/me');
|
|
expect(config).toMatchSnapshot();
|
|
expect(tree.exists('.eslintignore')).toBeFalsy();
|
|
});
|
|
|
|
it('should handle custom eslintignores', async () => {
|
|
await lintProjectGenerator(tree, {
|
|
skipFormat: false,
|
|
linter: Linter.EsLint,
|
|
project: 'test-lib',
|
|
setParserOptionsProject: false,
|
|
eslintConfigFormat: 'cjs',
|
|
});
|
|
tree.write('another-folder/.myeslintignore', 'ignore/me');
|
|
updateJson(tree, 'libs/test-lib/project.json', (json) => {
|
|
json.targets.lint.options = json.targets.lint.options || {};
|
|
json.targets.lint.options.ignorePath = 'another-folder/.myeslintignore';
|
|
return json;
|
|
});
|
|
tree.write('libs/test-lib/.eslintignore', 'ignore/me/as/well');
|
|
|
|
await convertToFlatConfigGenerator(tree, options);
|
|
|
|
expect(
|
|
tree.read('libs/test-lib/eslint.config.cjs', 'utf-8')
|
|
).toMatchSnapshot();
|
|
expect(tree.exists('another-folder/.myeslintignore')).toBeFalsy();
|
|
expect(tree.exists('libs/test-lib/.eslintignore')).toBeFalsy();
|
|
|
|
expect(
|
|
readJson(tree, 'libs/test-lib/project.json').targets.lint.options
|
|
.ignorePath
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it('should add settings', async () => {
|
|
await lintProjectGenerator(tree, {
|
|
skipFormat: false,
|
|
linter: Linter.EsLint,
|
|
project: 'test-lib',
|
|
setParserOptionsProject: false,
|
|
eslintConfigFormat: 'cjs',
|
|
});
|
|
updateJson(tree, '.eslintrc.json', (json) => {
|
|
json.settings = {
|
|
sharedData: 'Hello',
|
|
};
|
|
return json;
|
|
});
|
|
await convertToFlatConfigGenerator(tree, options);
|
|
|
|
expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchSnapshot();
|
|
});
|
|
|
|
it('should add env configuration', async () => {
|
|
await lintProjectGenerator(tree, {
|
|
skipFormat: false,
|
|
linter: Linter.EsLint,
|
|
project: 'test-lib',
|
|
setParserOptionsProject: false,
|
|
eslintConfigFormat: 'cjs',
|
|
});
|
|
updateJson(tree, '.eslintrc.json', (json) => {
|
|
json.env = {
|
|
browser: true,
|
|
node: true,
|
|
};
|
|
return json;
|
|
});
|
|
await convertToFlatConfigGenerator(tree, options);
|
|
|
|
expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchSnapshot();
|
|
});
|
|
|
|
it('should add global configuration', async () => {
|
|
await lintProjectGenerator(tree, {
|
|
skipFormat: false,
|
|
linter: Linter.EsLint,
|
|
project: 'test-lib',
|
|
setParserOptionsProject: false,
|
|
eslintConfigFormat: 'cjs',
|
|
});
|
|
updateJson(tree, '.eslintrc.json', (json) => {
|
|
json.globals = {
|
|
myCustomGlobal: 'readonly',
|
|
};
|
|
return json;
|
|
});
|
|
await convertToFlatConfigGenerator(tree, options);
|
|
|
|
expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchSnapshot();
|
|
});
|
|
|
|
it('should add global and env configuration', async () => {
|
|
await lintProjectGenerator(tree, {
|
|
skipFormat: false,
|
|
linter: Linter.EsLint,
|
|
project: 'test-lib',
|
|
setParserOptionsProject: false,
|
|
eslintConfigFormat: 'cjs',
|
|
});
|
|
updateJson(tree, '.eslintrc.json', (json) => {
|
|
json.globals = {
|
|
myCustomGlobal: 'readonly',
|
|
};
|
|
json.env = {
|
|
browser: true,
|
|
};
|
|
return json;
|
|
});
|
|
await convertToFlatConfigGenerator(tree, options);
|
|
|
|
expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchSnapshot();
|
|
});
|
|
|
|
it('should add plugins', async () => {
|
|
await lintProjectGenerator(tree, {
|
|
skipFormat: false,
|
|
linter: Linter.EsLint,
|
|
project: 'test-lib',
|
|
setParserOptionsProject: false,
|
|
eslintConfigFormat: 'cjs',
|
|
});
|
|
updateJson(tree, '.eslintrc.json', (json) => {
|
|
json.plugins = [
|
|
'eslint-plugin-import',
|
|
'single-name',
|
|
'@scope/with-name',
|
|
'@just-scope',
|
|
];
|
|
return json;
|
|
});
|
|
await convertToFlatConfigGenerator(tree, options);
|
|
|
|
expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchSnapshot();
|
|
});
|
|
|
|
it('should add parser', async () => {
|
|
await lintProjectGenerator(tree, {
|
|
skipFormat: false,
|
|
linter: Linter.EsLint,
|
|
project: 'test-lib',
|
|
setParserOptionsProject: false,
|
|
eslintConfigFormat: 'cjs',
|
|
});
|
|
updateJson(tree, '.eslintrc.json', (json) => {
|
|
json.parser = '@typescript-eslint/parser';
|
|
return json;
|
|
});
|
|
await convertToFlatConfigGenerator(tree, options);
|
|
|
|
expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchSnapshot();
|
|
});
|
|
|
|
it('should add linter options', async () => {
|
|
await lintProjectGenerator(tree, {
|
|
skipFormat: false,
|
|
linter: Linter.EsLint,
|
|
project: 'test-lib',
|
|
setParserOptionsProject: false,
|
|
eslintConfigFormat: 'cjs',
|
|
});
|
|
updateJson(tree, '.eslintrc.json', (json) => {
|
|
json.noInlineConfig = true;
|
|
return json;
|
|
});
|
|
await convertToFlatConfigGenerator(tree, options);
|
|
|
|
expect(tree.read('eslint.config.cjs', 'utf-8')).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 } },
|
|
{
|
|
linterOptions: {
|
|
noInlineConfig: true,
|
|
},
|
|
},
|
|
{
|
|
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,
|
|
},
|
|
})),
|
|
];
|
|
"
|
|
`);
|
|
});
|
|
|
|
it('should convert project if target is defined via plugin as string', async () => {
|
|
await lintProjectGenerator(tree, {
|
|
skipFormat: false,
|
|
linter: Linter.EsLint,
|
|
project: 'test-lib',
|
|
setParserOptionsProject: false,
|
|
eslintConfigFormat: 'cjs',
|
|
});
|
|
updateJson(tree, 'nx.json', (json: NxJsonConfiguration) => {
|
|
delete json.targetDefaults;
|
|
json.plugins = ['@nx/eslint/plugin'];
|
|
return json;
|
|
});
|
|
updateJson(
|
|
tree,
|
|
'libs/test-lib/project.json',
|
|
(json: ProjectConfiguration) => {
|
|
delete json.targets.lint;
|
|
return json;
|
|
}
|
|
);
|
|
|
|
expect(tree.exists('eslint.config.cjs')).toBeFalsy();
|
|
expect(tree.exists('libs/test-lib/eslint.config.cjs')).toBeFalsy();
|
|
await convertToFlatConfigGenerator(tree, options);
|
|
expect(tree.exists('eslint.config.cjs')).toBeTruthy();
|
|
expect(tree.exists('libs/test-lib/eslint.config.cjs')).toBeTruthy();
|
|
});
|
|
|
|
it('should convert project if target is defined via plugin as object', async () => {
|
|
await lintProjectGenerator(tree, {
|
|
skipFormat: false,
|
|
linter: Linter.EsLint,
|
|
project: 'test-lib',
|
|
setParserOptionsProject: false,
|
|
eslintConfigFormat: 'cjs',
|
|
});
|
|
updateJson(tree, 'nx.json', (json: NxJsonConfiguration) => {
|
|
delete json.targetDefaults;
|
|
json.plugins = [
|
|
{
|
|
plugin: '@nx/eslint/plugin',
|
|
options: {
|
|
targetName: 'lint',
|
|
},
|
|
},
|
|
];
|
|
return json;
|
|
});
|
|
updateJson(
|
|
tree,
|
|
'libs/test-lib/project.json',
|
|
(json: ProjectConfiguration) => {
|
|
delete json.targets.lint;
|
|
return json;
|
|
}
|
|
);
|
|
|
|
expect(tree.exists('eslint.config.cjs')).toBeFalsy();
|
|
expect(tree.exists('libs/test-lib/eslint.config.cjs')).toBeFalsy();
|
|
await convertToFlatConfigGenerator(tree, options);
|
|
expect(tree.exists('eslint.config.cjs')).toBeTruthy();
|
|
expect(tree.exists('libs/test-lib/eslint.config.cjs')).toBeTruthy();
|
|
});
|
|
|
|
it('should handle parser options even if parser is extended', async () => {
|
|
addProjectConfiguration(tree, 'dx-assets-ui', {
|
|
root: 'apps/dx-assets-ui',
|
|
targets: {},
|
|
});
|
|
await lintProjectGenerator(tree, {
|
|
skipFormat: false,
|
|
linter: Linter.EsLint,
|
|
|
|
project: 'dx-assets-ui',
|
|
setParserOptionsProject: false,
|
|
eslintConfigFormat: 'cjs',
|
|
});
|
|
updateJson(tree, 'apps/dx-assets-ui/.eslintrc.json', () => {
|
|
return {
|
|
extends: ['../../.eslintrc.json'],
|
|
ignorePatterns: ['!**/*', '__fixtures__/**/*'],
|
|
overrides: [
|
|
{
|
|
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
|
|
parserOptions: {
|
|
project: ['apps/dx-assets-ui/tsconfig.*?.json'],
|
|
},
|
|
rules: {},
|
|
},
|
|
{
|
|
files: ['*.ts', '*.tsx'],
|
|
rules: {},
|
|
},
|
|
{
|
|
files: ['*.js', '*.jsx'],
|
|
rules: {},
|
|
},
|
|
],
|
|
};
|
|
});
|
|
|
|
await convertToFlatConfigGenerator(tree, options);
|
|
expect(tree.exists('apps/dx-assets-ui/eslint.config.cjs')).toBeTruthy();
|
|
expect(tree.exists('eslint.config.cjs')).toBeTruthy();
|
|
expect(tree.read('apps/dx-assets-ui/eslint.config.cjs', 'utf-8'))
|
|
.toMatchInlineSnapshot(`
|
|
"const baseConfig = require('../../eslint.config.cjs');
|
|
|
|
module.exports = [
|
|
{
|
|
ignores: ['**/dist'],
|
|
},
|
|
...baseConfig,
|
|
{
|
|
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
|
// Override or add rules here
|
|
rules: {},
|
|
languageOptions: {
|
|
parserOptions: {
|
|
project: ['apps/dx-assets-ui/tsconfig.*?.json'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
files: ['**/*.ts', '**/*.tsx'],
|
|
// Override or add rules here
|
|
rules: {},
|
|
},
|
|
{
|
|
files: ['**/*.js', '**/*.jsx'],
|
|
// Override or add rules here
|
|
rules: {},
|
|
},
|
|
{
|
|
ignores: ['__fixtures__/**/*'],
|
|
},
|
|
];
|
|
"
|
|
`);
|
|
});
|
|
});
|
|
|
|
describe('MJS', () => {
|
|
const options: ConvertToFlatConfigGeneratorSchema = {
|
|
skipFormat: false,
|
|
eslintConfigFormat: 'mjs',
|
|
};
|
|
|
|
it('should update dependencies', async () => {
|
|
await lintProjectGenerator(tree, {
|
|
skipFormat: false,
|
|
linter: Linter.EsLint,
|
|
project: 'test-lib',
|
|
setParserOptionsProject: false,
|
|
eslintConfigFormat: 'mjs',
|
|
});
|
|
await convertToFlatConfigGenerator(tree, options);
|
|
|
|
expect(tree.read('package.json', 'utf-8')).toMatchInlineSnapshot(`
|
|
"{
|
|
"name": "@proj/source",
|
|
"dependencies": {},
|
|
"devDependencies": {
|
|
"@eslint/eslintrc": "^2.1.1",
|
|
"@nx/eslint": "0.0.1",
|
|
"@nx/eslint-plugin": "0.0.1",
|
|
"eslint": "^9.8.0",
|
|
"eslint-config-prettier": "^9.0.0",
|
|
"typescript-eslint": "^8.19.0"
|
|
}
|
|
}
|
|
"
|
|
`);
|
|
});
|
|
|
|
it('should convert json successfully', async () => {
|
|
await lintProjectGenerator(tree, {
|
|
skipFormat: false,
|
|
linter: Linter.EsLint,
|
|
project: 'test-lib',
|
|
setParserOptionsProject: false,
|
|
eslintConfigFormat: 'mjs',
|
|
});
|
|
await convertToFlatConfigGenerator(tree, options);
|
|
|
|
expect(tree.exists('eslint.config.mjs')).toBeTruthy();
|
|
expect(tree.read('eslint.config.mjs', 'utf-8')).toMatchSnapshot();
|
|
expect(tree.exists('libs/test-lib/eslint.config.mjs')).toBeTruthy();
|
|
expect(
|
|
tree.read('libs/test-lib/eslint.config.mjs', 'utf-8')
|
|
).toMatchSnapshot();
|
|
// check nx.json changes
|
|
const nxJson = readJson(tree, 'nx.json');
|
|
expect(nxJson.targetDefaults.lint.inputs).toContain(
|
|
'{workspaceRoot}/eslint.config.mjs'
|
|
);
|
|
expect(nxJson.namedInputs.production).toContain(
|
|
'!{projectRoot}/eslint.config.mjs'
|
|
);
|
|
});
|
|
|
|
it('should convert yaml successfully', async () => {
|
|
await lintProjectGenerator(tree, {
|
|
skipFormat: false,
|
|
linter: Linter.EsLint,
|
|
eslintFilePatterns: ['**/*.ts'],
|
|
project: 'test-lib',
|
|
setParserOptionsProject: false,
|
|
eslintConfigFormat: 'mjs',
|
|
});
|
|
const yamlContent = dump(readJson(tree, 'libs/test-lib/.eslintrc.json'));
|
|
tree.delete('libs/test-lib/.eslintrc.json');
|
|
tree.write('libs/test-lib/.eslintrc.yaml', yamlContent);
|
|
|
|
await convertToFlatConfigGenerator(tree, options);
|
|
|
|
expect(tree.exists('eslint.config.mjs')).toBeTruthy();
|
|
expect(tree.read('eslint.config.mjs', 'utf-8')).toMatchSnapshot();
|
|
expect(tree.exists('libs/test-lib/eslint.config.mjs')).toBeTruthy();
|
|
expect(
|
|
tree.read('libs/test-lib/eslint.config.mjs', 'utf-8')
|
|
).toMatchSnapshot();
|
|
// check nx.json changes
|
|
const nxJson = readJson(tree, 'nx.json');
|
|
expect(nxJson.targetDefaults.lint.inputs).toContain(
|
|
'{workspaceRoot}/eslint.config.mjs'
|
|
);
|
|
expect(nxJson.namedInputs.production).toContain(
|
|
'!{projectRoot}/eslint.config.mjs'
|
|
);
|
|
});
|
|
|
|
it('should convert yml successfully', async () => {
|
|
await lintProjectGenerator(tree, {
|
|
skipFormat: false,
|
|
linter: Linter.EsLint,
|
|
eslintFilePatterns: ['**/*.ts'],
|
|
project: 'test-lib',
|
|
setParserOptionsProject: false,
|
|
eslintConfigFormat: 'mjs',
|
|
});
|
|
const yamlContent = dump(readJson(tree, 'libs/test-lib/.eslintrc.json'));
|
|
tree.delete('libs/test-lib/.eslintrc.json');
|
|
tree.write('libs/test-lib/.eslintrc.yml', yamlContent);
|
|
|
|
await convertToFlatConfigGenerator(tree, options);
|
|
|
|
expect(tree.exists('eslint.config.mjs')).toBeTruthy();
|
|
expect(tree.read('eslint.config.mjs', 'utf-8')).toMatchSnapshot();
|
|
expect(tree.exists('libs/test-lib/eslint.config.mjs')).toBeTruthy();
|
|
expect(
|
|
tree.read('libs/test-lib/eslint.config.mjs', 'utf-8')
|
|
).toMatchSnapshot();
|
|
// check nx.json changes
|
|
const nxJson = readJson(tree, 'nx.json');
|
|
expect(nxJson.targetDefaults.lint.inputs).toContain(
|
|
'{workspaceRoot}/eslint.config.mjs'
|
|
);
|
|
expect(nxJson.namedInputs.production).toContain(
|
|
'!{projectRoot}/eslint.config.mjs'
|
|
);
|
|
});
|
|
|
|
it('should add plugin extends', async () => {
|
|
await lintProjectGenerator(tree, {
|
|
skipFormat: false,
|
|
linter: Linter.EsLint,
|
|
project: 'test-lib',
|
|
setParserOptionsProject: false,
|
|
eslintConfigFormat: 'mjs',
|
|
});
|
|
updateJson(tree, '.eslintrc.json', (json) => {
|
|
json.extends = ['plugin:storybook/recommended'];
|
|
return json;
|
|
});
|
|
await convertToFlatConfigGenerator(tree, options);
|
|
|
|
expect(tree.read('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 nxEslintPlugin from '@nx/eslint-plugin';
|
|
|
|
const compat = new FlatCompat({
|
|
baseDirectory: dirname(fileURLToPath(import.meta.url)),
|
|
recommendedConfig: js.configs.recommended,
|
|
});
|
|
|
|
export default [
|
|
{
|
|
ignores: ['**/dist'],
|
|
},
|
|
...compat.extends('plugin:storybook/recommended'),
|
|
{ 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,
|
|
},
|
|
})),
|
|
];
|
|
"
|
|
`);
|
|
expect(tree.read('libs/test-lib/eslint.config.mjs', 'utf-8'))
|
|
.toMatchInlineSnapshot(`
|
|
"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: {},
|
|
},
|
|
];
|
|
"
|
|
`);
|
|
expect(
|
|
readJson(tree, 'package.json').devDependencies['@eslint/eslintrc']
|
|
).toEqual(eslintrcVersion);
|
|
});
|
|
|
|
it('should add global eslintignores', async () => {
|
|
await lintProjectGenerator(tree, {
|
|
skipFormat: false,
|
|
linter: Linter.EsLint,
|
|
project: 'test-lib',
|
|
setParserOptionsProject: false,
|
|
});
|
|
tree.write('.eslintignore', 'ignore/me');
|
|
await convertToFlatConfigGenerator(tree, options);
|
|
|
|
const config = tree.read('eslint.config.mjs', 'utf-8');
|
|
expect(config).toContain('ignore/me');
|
|
expect(config).toMatchSnapshot();
|
|
expect(tree.exists('.eslintignore')).toBeFalsy();
|
|
});
|
|
|
|
it('should handle custom eslintignores', async () => {
|
|
await lintProjectGenerator(tree, {
|
|
skipFormat: false,
|
|
linter: Linter.EsLint,
|
|
project: 'test-lib',
|
|
setParserOptionsProject: false,
|
|
eslintConfigFormat: 'mjs',
|
|
});
|
|
tree.write('another-folder/.myeslintignore', 'ignore/me');
|
|
updateJson(tree, 'libs/test-lib/project.json', (json) => {
|
|
json.targets.lint.options = json.targets.lint.options || {};
|
|
json.targets.lint.options.ignorePath = 'another-folder/.myeslintignore';
|
|
return json;
|
|
});
|
|
tree.write('libs/test-lib/.eslintignore', 'ignore/me/as/well');
|
|
|
|
await convertToFlatConfigGenerator(tree, options);
|
|
|
|
expect(
|
|
tree.read('libs/test-lib/eslint.config.mjs', 'utf-8')
|
|
).toMatchSnapshot();
|
|
expect(tree.exists('another-folder/.myeslintignore')).toBeFalsy();
|
|
expect(tree.exists('libs/test-lib/.eslintignore')).toBeFalsy();
|
|
|
|
expect(
|
|
readJson(tree, 'libs/test-lib/project.json').targets.lint.options
|
|
.ignorePath
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it('should add settings', async () => {
|
|
await lintProjectGenerator(tree, {
|
|
skipFormat: false,
|
|
linter: Linter.EsLint,
|
|
project: 'test-lib',
|
|
setParserOptionsProject: false,
|
|
eslintConfigFormat: 'mjs',
|
|
});
|
|
updateJson(tree, '.eslintrc.json', (json) => {
|
|
json.settings = {
|
|
sharedData: 'Hello',
|
|
};
|
|
return json;
|
|
});
|
|
await convertToFlatConfigGenerator(tree, options);
|
|
|
|
expect(tree.read('eslint.config.mjs', 'utf-8')).toMatchSnapshot();
|
|
});
|
|
|
|
it('should add env configuration', async () => {
|
|
await lintProjectGenerator(tree, {
|
|
skipFormat: false,
|
|
linter: Linter.EsLint,
|
|
project: 'test-lib',
|
|
setParserOptionsProject: false,
|
|
eslintConfigFormat: 'mjs',
|
|
});
|
|
updateJson(tree, '.eslintrc.json', (json) => {
|
|
json.env = {
|
|
browser: true,
|
|
node: true,
|
|
};
|
|
return json;
|
|
});
|
|
await convertToFlatConfigGenerator(tree, options);
|
|
|
|
expect(tree.read('eslint.config.mjs', 'utf-8')).toMatchSnapshot();
|
|
});
|
|
|
|
it('should add global configuration', async () => {
|
|
await lintProjectGenerator(tree, {
|
|
skipFormat: false,
|
|
linter: Linter.EsLint,
|
|
project: 'test-lib',
|
|
setParserOptionsProject: false,
|
|
eslintConfigFormat: 'mjs',
|
|
});
|
|
updateJson(tree, '.eslintrc.json', (json) => {
|
|
json.globals = {
|
|
myCustomGlobal: 'readonly',
|
|
};
|
|
return json;
|
|
});
|
|
await convertToFlatConfigGenerator(tree, options);
|
|
|
|
expect(tree.read('eslint.config.mjs', 'utf-8')).toMatchSnapshot();
|
|
});
|
|
|
|
it('should add global and env configuration', async () => {
|
|
await lintProjectGenerator(tree, {
|
|
skipFormat: false,
|
|
linter: Linter.EsLint,
|
|
project: 'test-lib',
|
|
setParserOptionsProject: false,
|
|
eslintConfigFormat: 'mjs',
|
|
});
|
|
updateJson(tree, '.eslintrc.json', (json) => {
|
|
json.globals = {
|
|
myCustomGlobal: 'readonly',
|
|
};
|
|
json.env = {
|
|
browser: true,
|
|
};
|
|
return json;
|
|
});
|
|
await convertToFlatConfigGenerator(tree, options);
|
|
|
|
expect(tree.read('eslint.config.mjs', 'utf-8')).toMatchSnapshot();
|
|
});
|
|
|
|
it('should add plugins', async () => {
|
|
await lintProjectGenerator(tree, {
|
|
skipFormat: false,
|
|
linter: Linter.EsLint,
|
|
project: 'test-lib',
|
|
setParserOptionsProject: false,
|
|
eslintConfigFormat: 'mjs',
|
|
});
|
|
updateJson(tree, '.eslintrc.json', (json) => {
|
|
json.plugins = [
|
|
'eslint-plugin-import',
|
|
'single-name',
|
|
'@scope/with-name',
|
|
'@just-scope',
|
|
];
|
|
return json;
|
|
});
|
|
await convertToFlatConfigGenerator(tree, options);
|
|
|
|
expect(tree.read('eslint.config.mjs', 'utf-8')).toMatchSnapshot();
|
|
});
|
|
|
|
it('should add parser', async () => {
|
|
await lintProjectGenerator(tree, {
|
|
skipFormat: false,
|
|
linter: Linter.EsLint,
|
|
project: 'test-lib',
|
|
setParserOptionsProject: false,
|
|
eslintConfigFormat: 'mjs',
|
|
});
|
|
updateJson(tree, '.eslintrc.json', (json) => {
|
|
json.parser = '@typescript-eslint/parser';
|
|
return json;
|
|
});
|
|
await convertToFlatConfigGenerator(tree, options);
|
|
|
|
expect(tree.read('eslint.config.mjs', 'utf-8')).toMatchSnapshot();
|
|
});
|
|
|
|
it('should add linter options', async () => {
|
|
await lintProjectGenerator(tree, {
|
|
skipFormat: false,
|
|
linter: Linter.EsLint,
|
|
project: 'test-lib',
|
|
setParserOptionsProject: false,
|
|
eslintConfigFormat: 'mjs',
|
|
});
|
|
updateJson(tree, '.eslintrc.json', (json) => {
|
|
json.noInlineConfig = true;
|
|
return json;
|
|
});
|
|
await convertToFlatConfigGenerator(tree, options);
|
|
|
|
expect(tree.read('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 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 } },
|
|
{
|
|
linterOptions: {
|
|
noInlineConfig: true,
|
|
},
|
|
},
|
|
{
|
|
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,
|
|
},
|
|
})),
|
|
];
|
|
"
|
|
`);
|
|
});
|
|
|
|
it('should convert project if target is defined via plugin as string', async () => {
|
|
await lintProjectGenerator(tree, {
|
|
skipFormat: false,
|
|
linter: Linter.EsLint,
|
|
project: 'test-lib',
|
|
setParserOptionsProject: false,
|
|
eslintConfigFormat: 'mjs',
|
|
});
|
|
updateJson(tree, 'nx.json', (json: NxJsonConfiguration) => {
|
|
delete json.targetDefaults;
|
|
json.plugins = ['@nx/eslint/plugin'];
|
|
return json;
|
|
});
|
|
updateJson(
|
|
tree,
|
|
'libs/test-lib/project.json',
|
|
(json: ProjectConfiguration) => {
|
|
delete json.targets.lint;
|
|
return json;
|
|
}
|
|
);
|
|
|
|
expect(tree.exists('eslint.config.mjs')).toBeFalsy();
|
|
expect(tree.exists('libs/test-lib/eslint.config.mjs')).toBeFalsy();
|
|
await convertToFlatConfigGenerator(tree, options);
|
|
expect(tree.exists('eslint.config.mjs')).toBeTruthy();
|
|
expect(tree.exists('libs/test-lib/eslint.config.mjs')).toBeTruthy();
|
|
});
|
|
|
|
it('should convert project if target is defined via plugin as object', async () => {
|
|
await lintProjectGenerator(tree, {
|
|
skipFormat: false,
|
|
linter: Linter.EsLint,
|
|
project: 'test-lib',
|
|
setParserOptionsProject: false,
|
|
eslintConfigFormat: 'mjs',
|
|
});
|
|
updateJson(tree, 'nx.json', (json: NxJsonConfiguration) => {
|
|
delete json.targetDefaults;
|
|
json.plugins = [
|
|
{
|
|
plugin: '@nx/eslint/plugin',
|
|
options: {
|
|
targetName: 'lint',
|
|
},
|
|
},
|
|
];
|
|
return json;
|
|
});
|
|
updateJson(
|
|
tree,
|
|
'libs/test-lib/project.json',
|
|
(json: ProjectConfiguration) => {
|
|
delete json.targets.lint;
|
|
return json;
|
|
}
|
|
);
|
|
|
|
expect(tree.exists('eslint.config.mjs')).toBeFalsy();
|
|
expect(tree.exists('libs/test-lib/eslint.config.mjs')).toBeFalsy();
|
|
await convertToFlatConfigGenerator(tree, options);
|
|
expect(tree.exists('eslint.config.mjs')).toBeTruthy();
|
|
expect(tree.exists('libs/test-lib/eslint.config.mjs')).toBeTruthy();
|
|
});
|
|
|
|
it('should handle parser options even if parser is extended', async () => {
|
|
addProjectConfiguration(tree, 'dx-assets-ui', {
|
|
root: 'apps/dx-assets-ui',
|
|
targets: {},
|
|
});
|
|
await lintProjectGenerator(tree, {
|
|
skipFormat: false,
|
|
linter: Linter.EsLint,
|
|
|
|
project: 'dx-assets-ui',
|
|
setParserOptionsProject: false,
|
|
eslintConfigFormat: 'mjs',
|
|
});
|
|
updateJson(tree, 'apps/dx-assets-ui/.eslintrc.json', () => {
|
|
return {
|
|
extends: ['../../.eslintrc.json'],
|
|
ignorePatterns: ['!**/*', '__fixtures__/**/*'],
|
|
overrides: [
|
|
{
|
|
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
|
|
parserOptions: {
|
|
project: ['apps/dx-assets-ui/tsconfig.*?.json'],
|
|
},
|
|
rules: {},
|
|
},
|
|
{
|
|
files: ['*.ts', '*.tsx'],
|
|
rules: {},
|
|
},
|
|
{
|
|
files: ['*.js', '*.jsx'],
|
|
rules: {},
|
|
},
|
|
],
|
|
};
|
|
});
|
|
|
|
await convertToFlatConfigGenerator(tree, options);
|
|
expect(tree.exists('apps/dx-assets-ui/eslint.config.mjs')).toBeTruthy();
|
|
expect(tree.exists('eslint.config.mjs')).toBeTruthy();
|
|
expect(tree.read('apps/dx-assets-ui/eslint.config.mjs', 'utf-8'))
|
|
.toMatchInlineSnapshot(`
|
|
"import baseConfig from '../../eslint.config.mjs';
|
|
|
|
export default [
|
|
{
|
|
ignores: ['**/dist'],
|
|
},
|
|
...baseConfig,
|
|
{
|
|
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
|
// Override or add rules here
|
|
rules: {},
|
|
languageOptions: {
|
|
parserOptions: {
|
|
project: ['apps/dx-assets-ui/tsconfig.*?.json'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
files: ['**/*.ts', '**/*.tsx'],
|
|
// Override or add rules here
|
|
rules: {},
|
|
},
|
|
{
|
|
files: ['**/*.js', '**/*.jsx'],
|
|
// Override or add rules here
|
|
rules: {},
|
|
},
|
|
{
|
|
ignores: ['__fixtures__/**/*'],
|
|
},
|
|
];
|
|
"
|
|
`);
|
|
});
|
|
});
|
|
});
|