Revert "Revert "feat(linter): add generator for converting to flat co… (#18631)
This commit is contained in:
parent
e2ac4e38e7
commit
968bd38218
@ -6384,6 +6384,14 @@
|
||||
"children": [],
|
||||
"isExternal": false,
|
||||
"disableCollapsible": false
|
||||
},
|
||||
{
|
||||
"id": "convert-to-flat-config",
|
||||
"path": "/packages/linter/generators/convert-to-flat-config",
|
||||
"name": "convert-to-flat-config",
|
||||
"children": [],
|
||||
"isExternal": false,
|
||||
"disableCollapsible": false
|
||||
}
|
||||
],
|
||||
"isExternal": false,
|
||||
|
||||
@ -1140,6 +1140,15 @@
|
||||
"originalFilePath": "/packages/linter/src/generators/workspace-rule/schema.json",
|
||||
"path": "/packages/linter/generators/workspace-rule",
|
||||
"type": "generator"
|
||||
},
|
||||
"/packages/linter/generators/convert-to-flat-config": {
|
||||
"description": "Convert an Nx workspace to a Flat ESLint config.",
|
||||
"file": "generated/packages/linter/generators/convert-to-flat-config.json",
|
||||
"hidden": false,
|
||||
"name": "convert-to-flat-config",
|
||||
"originalFilePath": "/packages/linter/src/generators/convert-to-flat-config/schema.json",
|
||||
"path": "/packages/linter/generators/convert-to-flat-config",
|
||||
"type": "generator"
|
||||
}
|
||||
},
|
||||
"path": "/packages/linter"
|
||||
|
||||
@ -1123,6 +1123,15 @@
|
||||
"originalFilePath": "/packages/linter/src/generators/workspace-rule/schema.json",
|
||||
"path": "linter/generators/workspace-rule",
|
||||
"type": "generator"
|
||||
},
|
||||
{
|
||||
"description": "Convert an Nx workspace to a Flat ESLint config.",
|
||||
"file": "generated/packages/linter/generators/convert-to-flat-config.json",
|
||||
"hidden": false,
|
||||
"name": "convert-to-flat-config",
|
||||
"originalFilePath": "/packages/linter/src/generators/convert-to-flat-config/schema.json",
|
||||
"path": "linter/generators/convert-to-flat-config",
|
||||
"type": "generator"
|
||||
}
|
||||
],
|
||||
"githubRoot": "https://github.com/nrwl/nx/blob/master",
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "convert-to-flat-config",
|
||||
"factory": "./src/generators/convert-to-flat-config/generator",
|
||||
"schema": {
|
||||
"$schema": "http://json-schema.org/schema",
|
||||
"$id": "ConvertToFlatConfig",
|
||||
"cli": "nx",
|
||||
"description": "Convert an Nx workspace to a Flat ESLint config.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"skipFormat": {
|
||||
"type": "boolean",
|
||||
"description": "Skip formatting files.",
|
||||
"default": false,
|
||||
"x-priority": "internal"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [],
|
||||
"presets": []
|
||||
},
|
||||
"description": "Convert an Nx workspace to a Flat ESLint config.",
|
||||
"implementation": "/packages/linter/src/generators/convert-to-flat-config/generator.ts",
|
||||
"aliases": [],
|
||||
"hidden": false,
|
||||
"path": "/packages/linter/src/generators/convert-to-flat-config/schema.json",
|
||||
"type": "generator"
|
||||
}
|
||||
@ -413,6 +413,7 @@
|
||||
- [generators](/packages/linter/generators)
|
||||
- [workspace-rules-project](/packages/linter/generators/workspace-rules-project)
|
||||
- [workspace-rule](/packages/linter/generators/workspace-rule)
|
||||
- [convert-to-flat-config](/packages/linter/generators/convert-to-flat-config)
|
||||
- [nest](/packages/nest)
|
||||
- [documents](/packages/nest/documents)
|
||||
- [Overview](/packages/nest/documents/overview)
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
import * as path from 'path';
|
||||
import {
|
||||
checkFilesDoNotExist,
|
||||
checkFilesExist,
|
||||
cleanupProject,
|
||||
createFile,
|
||||
getSelectedPackageManager,
|
||||
newProject,
|
||||
readFile,
|
||||
readJson,
|
||||
runCLI,
|
||||
runCreateWorkspace,
|
||||
uniq,
|
||||
updateFile,
|
||||
updateJson,
|
||||
@ -537,6 +540,78 @@ describe('Linter', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Flat config', () => {
|
||||
const packageManager = getSelectedPackageManager() || 'pnpm';
|
||||
|
||||
afterEach(() => cleanupProject());
|
||||
|
||||
it('should convert integrated to flat config', () => {
|
||||
const myapp = uniq('myapp');
|
||||
const mylib = uniq('mylib');
|
||||
|
||||
runCreateWorkspace(myapp, {
|
||||
preset: 'react-monorepo',
|
||||
appName: myapp,
|
||||
style: 'css',
|
||||
packageManager,
|
||||
bundler: 'vite',
|
||||
e2eTestRunner: 'none',
|
||||
});
|
||||
runCLI(`generate @nx/js:lib ${mylib}`);
|
||||
|
||||
// migrate to flat structure
|
||||
runCLI(`generate @nx/linter:convert-to-flat-config`);
|
||||
checkFilesExist(
|
||||
'eslint.config.js',
|
||||
`apps/${myapp}/eslint.config.js`,
|
||||
`libs/${mylib}/eslint.config.js`
|
||||
);
|
||||
checkFilesDoNotExist(
|
||||
'.eslintrc.json',
|
||||
`apps/${myapp}/.eslintrc.json`,
|
||||
`libs/${mylib}/.eslintrc.json`
|
||||
);
|
||||
|
||||
const outFlat = runCLI(`affected -t lint`, {
|
||||
silenceError: true,
|
||||
});
|
||||
expect(outFlat).toContain('All files pass linting');
|
||||
}, 1000000);
|
||||
|
||||
it('should convert standalone to flat config', () => {
|
||||
const myapp = uniq('myapp');
|
||||
const mylib = uniq('mylib');
|
||||
|
||||
runCreateWorkspace(myapp, {
|
||||
preset: 'react-standalone',
|
||||
appName: myapp,
|
||||
style: 'css',
|
||||
packageManager,
|
||||
bundler: 'vite',
|
||||
e2eTestRunner: 'none',
|
||||
});
|
||||
runCLI(`generate @nx/js:lib ${mylib}`);
|
||||
|
||||
// migrate to flat structure
|
||||
runCLI(`generate @nx/linter:convert-to-flat-config`);
|
||||
checkFilesExist(
|
||||
'eslint.config.js',
|
||||
`${mylib}/eslint.config.js`,
|
||||
'eslint.base.config.js'
|
||||
);
|
||||
checkFilesDoNotExist(
|
||||
'.eslintrc.json',
|
||||
`${mylib}/.eslintrc.json`,
|
||||
'.eslintrc.base.json'
|
||||
);
|
||||
|
||||
const outFlat = runCLI(`affected -t lint`, {
|
||||
silenceError: true,
|
||||
});
|
||||
expect(outFlat).toContain('All files pass linting');
|
||||
}, 1000000);
|
||||
});
|
||||
|
||||
describe('Root projects migration', () => {
|
||||
beforeEach(() => newProject());
|
||||
afterEach(() => cleanupProject());
|
||||
|
||||
@ -46,6 +46,8 @@
|
||||
"@babel/preset-react": "^7.22.5",
|
||||
"@babel/preset-typescript": "^7.22.5",
|
||||
"@babel/runtime": "^7.22.6",
|
||||
"@eslint/eslintrc": "^2.1.1",
|
||||
"@eslint/js": "^8.46.0",
|
||||
"@floating-ui/react": "0.19.2",
|
||||
"@jest/reporters": "^29.4.1",
|
||||
"@jest/test-result": "^29.4.1",
|
||||
|
||||
@ -95,6 +95,7 @@ exports[`workspace move to nx layout should create nx.json 1`] = `
|
||||
"!{projectRoot}/**/*.spec.[jt]s",
|
||||
"!{projectRoot}/karma.conf.js",
|
||||
"!{projectRoot}/.eslintrc.json",
|
||||
"!{projectRoot}/eslint.config.js",
|
||||
],
|
||||
"sharedGlobals": [],
|
||||
},
|
||||
@ -118,6 +119,7 @@ exports[`workspace move to nx layout should create nx.json 1`] = `
|
||||
"inputs": [
|
||||
"default",
|
||||
"{workspaceRoot}/.eslintrc.json",
|
||||
"{workspaceRoot}/eslint.config.js",
|
||||
],
|
||||
},
|
||||
"test": {
|
||||
|
||||
@ -76,7 +76,9 @@ export function createNxJson(
|
||||
'!{projectRoot}/karma.conf.js',
|
||||
]
|
||||
: []),
|
||||
targets.lint ? '!{projectRoot}/.eslintrc.json' : undefined,
|
||||
...(targets.lint
|
||||
? ['!{projectRoot}/.eslintrc.json', '!{projectRoot}/eslint.config.js']
|
||||
: []),
|
||||
].filter(Boolean),
|
||||
},
|
||||
targetDefaults: {
|
||||
@ -91,7 +93,11 @@ export function createNxJson(
|
||||
: undefined,
|
||||
lint: targets.lint
|
||||
? {
|
||||
inputs: ['default', '{workspaceRoot}/.eslintrc.json'],
|
||||
inputs: [
|
||||
'default',
|
||||
'{workspaceRoot}/.eslintrc.json',
|
||||
'{workspaceRoot}/eslint.config.js',
|
||||
],
|
||||
}
|
||||
: undefined,
|
||||
e2e: targets.e2e
|
||||
|
||||
@ -25,6 +25,11 @@
|
||||
"factory": "./src/generators/workspace-rule/workspace-rule#lintWorkspaceRuleGenerator",
|
||||
"schema": "./src/generators/workspace-rule/schema.json",
|
||||
"description": "Create a new Workspace ESLint rule."
|
||||
},
|
||||
"convert-to-flat-config": {
|
||||
"factory": "./src/generators/convert-to-flat-config/generator",
|
||||
"schema": "./src/generators/convert-to-flat-config/schema.json",
|
||||
"description": "Convert an Nx workspace to a Flat ESLint config."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,7 +37,8 @@
|
||||
"tmp": "~0.2.1",
|
||||
"tslib": "^2.3.0",
|
||||
"@nx/devkit": "file:../devkit",
|
||||
"@nx/js": "file:../js"
|
||||
"@nx/js": "file:../js",
|
||||
"typescript": "~5.1.3"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"eslint": {
|
||||
|
||||
@ -130,10 +130,13 @@ Please see https://nx.dev/guides/eslint for full guidance on how to resolve this
|
||||
.filter((pattern) => !!pattern)
|
||||
.map((pattern) => `- '${pattern}'`);
|
||||
if (ignoredPatterns.length) {
|
||||
const ignoreSection = useFlatConfig
|
||||
? `'ignores' configuration`
|
||||
: `'.eslintignore' file`;
|
||||
throw new Error(
|
||||
`All files matching the following patterns are ignored:\n${ignoredPatterns.join(
|
||||
'\n'
|
||||
)}\n\nPlease check your '.eslintignore' file.`
|
||||
)}\n\nPlease check your ${ignoreSection}.`
|
||||
);
|
||||
}
|
||||
throw new Error(
|
||||
|
||||
@ -0,0 +1,385 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`convert-to-flat-config generator should add env configuration 1`] = `
|
||||
"const { FlatCompat } = require('@eslint/eslintrc');
|
||||
const nxEslintPlugin = require('@nx/eslint-plugin');
|
||||
const globals = require('globals');
|
||||
const js = require('@eslint/js');
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
module.exports = [
|
||||
{ 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'],
|
||||
rules: {},
|
||||
})),
|
||||
...compat.config({ extends: ['plugin:@nx/javascript'] }).map((config) => ({
|
||||
...config,
|
||||
files: ['**/*.js', '**/*.jsx'],
|
||||
rules: {},
|
||||
})),
|
||||
];
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`convert-to-flat-config generator should add global and env configuration 1`] = `
|
||||
"const { FlatCompat } = require('@eslint/eslintrc');
|
||||
const nxEslintPlugin = require('@nx/eslint-plugin');
|
||||
const globals = require('globals');
|
||||
const js = require('@eslint/js');
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
module.exports = [
|
||||
{ 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'],
|
||||
rules: {},
|
||||
})),
|
||||
...compat.config({ extends: ['plugin:@nx/javascript'] }).map((config) => ({
|
||||
...config,
|
||||
files: ['**/*.js', '**/*.jsx'],
|
||||
rules: {},
|
||||
})),
|
||||
];
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`convert-to-flat-config generator should add global configuration 1`] = `
|
||||
"const { FlatCompat } = require('@eslint/eslintrc');
|
||||
const nxEslintPlugin = require('@nx/eslint-plugin');
|
||||
const js = require('@eslint/js');
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
module.exports = [
|
||||
{ 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'],
|
||||
rules: {},
|
||||
})),
|
||||
...compat.config({ extends: ['plugin:@nx/javascript'] }).map((config) => ({
|
||||
...config,
|
||||
files: ['**/*.js', '**/*.jsx'],
|
||||
rules: {},
|
||||
})),
|
||||
];
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`convert-to-flat-config generator should add global gitignores 1`] = `
|
||||
"const { FlatCompat } = require('@eslint/eslintrc');
|
||||
const nxEslintPlugin = require('@nx/eslint-plugin');
|
||||
const js = require('@eslint/js');
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
module.exports = [
|
||||
{ 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: {},
|
||||
})),
|
||||
...compat.config({ extends: ['plugin:@nx/javascript'] }).map((config) => ({
|
||||
...config,
|
||||
files: ['**/*.js', '**/*.jsx'],
|
||||
rules: {},
|
||||
})),
|
||||
{ ignores: ['ignore/me'] },
|
||||
];
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`convert-to-flat-config generator should add parser 1`] = `
|
||||
"const { FlatCompat } = require('@eslint/eslintrc');
|
||||
const nxEslintPlugin = require('@nx/eslint-plugin');
|
||||
const typescriptEslintParser = require('@typescript-eslint/parser');
|
||||
const js = require('@eslint/js');
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
module.exports = [
|
||||
{ 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'],
|
||||
rules: {},
|
||||
})),
|
||||
...compat.config({ extends: ['plugin:@nx/javascript'] }).map((config) => ({
|
||||
...config,
|
||||
files: ['**/*.js', '**/*.jsx'],
|
||||
rules: {},
|
||||
})),
|
||||
];
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`convert-to-flat-config generator should add plugins 1`] = `
|
||||
"const { FlatCompat } = require('@eslint/eslintrc');
|
||||
const eslintPluginImport = require('eslint-plugin-import');
|
||||
const eslintPluginSingleName = require('eslint-plugin-single-name');
|
||||
const scopeEslintPluginWithName = require('@scope/eslint-plugin-with-name');
|
||||
const justScopeEslintPlugin = require('@just-scope/eslint-plugin');
|
||||
const js = require('@eslint/js');
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
module.exports = [
|
||||
{
|
||||
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'],
|
||||
rules: {},
|
||||
})),
|
||||
...compat.config({ extends: ['plugin:@nx/javascript'] }).map((config) => ({
|
||||
...config,
|
||||
files: ['**/*.js', '**/*.jsx'],
|
||||
rules: {},
|
||||
})),
|
||||
];
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`convert-to-flat-config generator should add settings 1`] = `
|
||||
"const { FlatCompat } = require('@eslint/eslintrc');
|
||||
const nxEslintPlugin = require('@nx/eslint-plugin');
|
||||
const js = require('@eslint/js');
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
module.exports = [
|
||||
{ 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'],
|
||||
rules: {},
|
||||
})),
|
||||
...compat.config({ extends: ['plugin:@nx/javascript'] }).map((config) => ({
|
||||
...config,
|
||||
files: ['**/*.js', '**/*.jsx'],
|
||||
rules: {},
|
||||
})),
|
||||
];
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`convert-to-flat-config generator should run successfully 1`] = `
|
||||
"const { FlatCompat } = require('@eslint/eslintrc');
|
||||
const nxEslintPlugin = require('@nx/eslint-plugin');
|
||||
const js = require('@eslint/js');
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
module.exports = [
|
||||
{ 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: {},
|
||||
})),
|
||||
...compat.config({ extends: ['plugin:@nx/javascript'] }).map((config) => ({
|
||||
...config,
|
||||
files: ['**/*.js', '**/*.jsx'],
|
||||
rules: {},
|
||||
})),
|
||||
];
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`convert-to-flat-config generator should run successfully 2`] = `
|
||||
"const baseConfig = require('../../eslint.config.js');
|
||||
module.exports = [
|
||||
...baseConfig,
|
||||
{
|
||||
files: [
|
||||
'libs/test-lib/**/*.ts',
|
||||
'libs/test-lib/**/*.tsx',
|
||||
'libs/test-lib/**/*.js',
|
||||
'libs/test-lib/**/*.jsx',
|
||||
],
|
||||
rules: {},
|
||||
},
|
||||
{
|
||||
files: ['libs/test-lib/**/*.ts', 'libs/test-lib/**/*.tsx'],
|
||||
rules: {},
|
||||
},
|
||||
{
|
||||
files: ['libs/test-lib/**/*.js', 'libs/test-lib/**/*.jsx'],
|
||||
rules: {},
|
||||
},
|
||||
];
|
||||
"
|
||||
`;
|
||||
@ -0,0 +1,68 @@
|
||||
import * as ts from 'typescript';
|
||||
|
||||
/**
|
||||
* Generates an AST from a JSON-type input
|
||||
*/
|
||||
export function generateAst<T>(input: unknown): T {
|
||||
if (Array.isArray(input)) {
|
||||
return ts.factory.createArrayLiteralExpression(
|
||||
input.map((item) => generateAst<ts.Expression>(item)),
|
||||
input.length > 1 // multiline only if more than one item
|
||||
) as T;
|
||||
}
|
||||
if (input === null) {
|
||||
return ts.factory.createNull() as T;
|
||||
}
|
||||
if (typeof input === 'object') {
|
||||
return ts.factory.createObjectLiteralExpression(
|
||||
Object.entries(input)
|
||||
.filter(([_, value]) => value !== undefined)
|
||||
.map(([key, value]) =>
|
||||
ts.factory.createPropertyAssignment(
|
||||
isValidKey(key) ? key : ts.factory.createStringLiteral(key),
|
||||
generateAst<ts.Expression>(value)
|
||||
)
|
||||
),
|
||||
Object.keys(input).length > 1 // multiline only if more than one property
|
||||
) as T;
|
||||
}
|
||||
if (typeof input === 'string') {
|
||||
return ts.factory.createStringLiteral(input) as T;
|
||||
}
|
||||
if (typeof input === 'number') {
|
||||
return ts.factory.createNumericLiteral(input) as T;
|
||||
}
|
||||
if (typeof input === 'boolean') {
|
||||
return (input ? ts.factory.createTrue() : ts.factory.createFalse()) as T;
|
||||
}
|
||||
// since we are parsing JSON, this should never happen
|
||||
throw new Error(`Unknown type: ${typeof input}`);
|
||||
}
|
||||
|
||||
export function generateRequire(
|
||||
variableName: string | ts.ObjectBindingPattern,
|
||||
imp: string
|
||||
): ts.VariableStatement {
|
||||
return ts.factory.createVariableStatement(
|
||||
undefined,
|
||||
ts.factory.createVariableDeclarationList(
|
||||
[
|
||||
ts.factory.createVariableDeclaration(
|
||||
variableName,
|
||||
undefined,
|
||||
undefined,
|
||||
ts.factory.createCallExpression(
|
||||
ts.factory.createIdentifier('require'),
|
||||
undefined,
|
||||
[ts.factory.createStringLiteral(imp)]
|
||||
)
|
||||
),
|
||||
],
|
||||
ts.NodeFlags.Const
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function isValidKey(key: string): boolean {
|
||||
return /^[a-zA-Z0-9_]+$/.test(key);
|
||||
}
|
||||
@ -0,0 +1,234 @@
|
||||
import { Tree } from '@nx/devkit';
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
import { convertEslintJsonToFlatConfig } from './json-converter';
|
||||
|
||||
describe('convertEslintJsonToFlatConfig', () => {
|
||||
let tree: Tree;
|
||||
|
||||
beforeEach(() => {
|
||||
tree = createTreeWithEmptyWorkspace();
|
||||
});
|
||||
|
||||
it('should convert root configs', async () => {
|
||||
tree.write(
|
||||
'.eslintrc.json',
|
||||
JSON.stringify({
|
||||
root: true,
|
||||
ignorePatterns: ['**/*', 'src/ignore/to/keep.ts'],
|
||||
plugins: ['@nx'],
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
|
||||
rules: {
|
||||
'@nx/enforce-module-boundaries': [
|
||||
'error',
|
||||
{
|
||||
enforceBuildableLibDependency: true,
|
||||
allow: [],
|
||||
depConstraints: [
|
||||
{
|
||||
sourceTag: '*',
|
||||
onlyDependOnLibsWithTags: ['*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.ts', '*.tsx'],
|
||||
extends: ['plugin:@nx/typescript'],
|
||||
rules: {},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'**/*.spec.ts',
|
||||
'**/*.spec.tsx',
|
||||
'**/*.spec.js',
|
||||
'**/*.spec.jsx',
|
||||
],
|
||||
env: {
|
||||
jest: true,
|
||||
},
|
||||
rules: {},
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
tree.write('.eslintignore', 'node_modules\nsomething/else');
|
||||
|
||||
convertEslintJsonToFlatConfig(
|
||||
tree,
|
||||
'',
|
||||
'.eslintrc.json',
|
||||
'eslint.config.js'
|
||||
);
|
||||
|
||||
expect(tree.read('eslint.config.js', 'utf-8')).toMatchInlineSnapshot(`
|
||||
"const { FlatCompat } = require("@eslint/eslintrc");
|
||||
const nxEslintPlugin = require("@nx/eslint-plugin");
|
||||
const js = require("@eslint/js");
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
module.exports = [
|
||||
{ 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: {}
|
||||
})),
|
||||
...compat.config({ env: { jest: true } }).map(config => ({
|
||||
...config,
|
||||
files: [
|
||||
"**/*.spec.ts",
|
||||
"**/*.spec.tsx",
|
||||
"**/*.spec.js",
|
||||
"**/*.spec.jsx"
|
||||
],
|
||||
rules: {}
|
||||
})),
|
||||
{ ignores: ["src/ignore/to/keep.ts"] },
|
||||
{ ignores: ["something/else"] }
|
||||
];
|
||||
"
|
||||
`);
|
||||
|
||||
expect(tree.exists('.eslintrc.json')).toBeFalsy();
|
||||
expect(tree.exists('.eslintignore')).toBeFalsy();
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
convertEslintJsonToFlatConfig(
|
||||
tree,
|
||||
'mylib',
|
||||
'.eslintrc.json',
|
||||
'eslint.config.js'
|
||||
);
|
||||
|
||||
expect(tree.read('mylib/eslint.config.js', 'utf-8')).toMatchInlineSnapshot(`
|
||||
"const { FlatCompat } = require("@eslint/eslintrc");
|
||||
const baseConfig = require("../../eslint.config.js");
|
||||
const globals = require("globals");
|
||||
const js = require("@eslint/js");
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
module.exports = [
|
||||
...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: [
|
||||
"mylib/**/*.ts",
|
||||
"mylib/**/*.tsx",
|
||||
"mylib/**/*.js",
|
||||
"mylib/**/*.jsx"
|
||||
],
|
||||
rules: { "@next/next/no-html-link-for-pages": [
|
||||
"error",
|
||||
"apps/test-next/pages"
|
||||
] }
|
||||
},
|
||||
{
|
||||
files: [
|
||||
"mylib/**/*.ts",
|
||||
"mylib/**/*.tsx"
|
||||
],
|
||||
rules: {}
|
||||
},
|
||||
{
|
||||
files: [
|
||||
"mylib/**/*.js",
|
||||
"mylib/**/*.jsx"
|
||||
],
|
||||
rules: {}
|
||||
},
|
||||
...compat.config({ parser: "jsonc-eslint-parser" }).map(config => ({
|
||||
...config,
|
||||
files: ["mylib/**/*.json"],
|
||||
rules: { "@nx/dependency-checks": "error" }
|
||||
})),
|
||||
{ ignores: ["mylib/.next/**/*"] },
|
||||
{ ignores: ["mylib/something/else"] }
|
||||
];
|
||||
"
|
||||
`);
|
||||
|
||||
expect(tree.exists('mylib/.eslintrc.json')).toBeFalsy();
|
||||
expect(tree.exists('mylib/.eslintignore')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,524 @@
|
||||
import {
|
||||
Tree,
|
||||
addDependenciesToPackageJson,
|
||||
names,
|
||||
readJson,
|
||||
} from '@nx/devkit';
|
||||
import { join } from 'path';
|
||||
import { ESLint, Linter } from 'eslint';
|
||||
import * as ts from 'typescript';
|
||||
import { generateAst, generateRequire } from './generate-ast';
|
||||
import { eslintrcVersion } from '../../../utils/versions';
|
||||
|
||||
/**
|
||||
* Converts an ESLint JSON config to a flat config.
|
||||
* Deletes the original file along with .eslintignore if it exists.
|
||||
*/
|
||||
export function convertEslintJsonToFlatConfig(
|
||||
tree: Tree,
|
||||
root: string,
|
||||
sourceFile: string,
|
||||
destinationFile: string
|
||||
) {
|
||||
const importsMap = new Map<string, string>();
|
||||
const exportElements: ts.Expression[] = [];
|
||||
let isFlatCompatNeeded = false;
|
||||
let combinedConfig: ts.PropertyAssignment[] = [];
|
||||
let languageOptions: ts.PropertyAssignment[] = [];
|
||||
|
||||
// read original config
|
||||
const config: ESLint.ConfigData = readJson(tree, `${root}/${sourceFile}`);
|
||||
|
||||
if (config.extends) {
|
||||
isFlatCompatNeeded = addExtends(importsMap, exportElements, config, tree);
|
||||
}
|
||||
|
||||
if (config.plugins) {
|
||||
addPlugins(importsMap, exportElements, config);
|
||||
}
|
||||
|
||||
if (config.parser) {
|
||||
languageOptions.push(addParser(importsMap, config));
|
||||
}
|
||||
|
||||
if (config.parserOptions) {
|
||||
languageOptions.push(
|
||||
ts.factory.createPropertyAssignment(
|
||||
'parserOptions',
|
||||
generateAst(config.parserOptions)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (config.globals || config.env) {
|
||||
if (config.env) {
|
||||
importsMap.set('globals', 'globals');
|
||||
}
|
||||
|
||||
languageOptions.push(
|
||||
ts.factory.createPropertyAssignment(
|
||||
'globals',
|
||||
ts.factory.createObjectLiteralExpression([
|
||||
...Object.keys(config.env || {}).map((env) =>
|
||||
ts.factory.createSpreadAssignment(
|
||||
ts.factory.createPropertyAccessExpression(
|
||||
ts.factory.createIdentifier('globals'),
|
||||
ts.factory.createIdentifier(env)
|
||||
)
|
||||
)
|
||||
),
|
||||
...Object.keys(config.globals || {}).map((key) =>
|
||||
ts.factory.createPropertyAssignment(
|
||||
key,
|
||||
generateAst(config.globals[key])
|
||||
)
|
||||
),
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (config.settings) {
|
||||
combinedConfig.push(
|
||||
ts.factory.createPropertyAssignment(
|
||||
'settings',
|
||||
generateAst(config.settings)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
config.noInlineConfig !== undefined ||
|
||||
config.reportUnusedDisableDirectives !== undefined
|
||||
) {
|
||||
combinedConfig.push(
|
||||
ts.factory.createPropertyAssignment(
|
||||
'linterOptions',
|
||||
generateAst({
|
||||
noInlineConfig: config.noInlineConfig,
|
||||
reportUnusedDisableDirectives: config.reportUnusedDisableDirectives,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (languageOptions.length > 0) {
|
||||
combinedConfig.push(
|
||||
ts.factory.createPropertyAssignment(
|
||||
'languageOptions',
|
||||
ts.factory.createObjectLiteralExpression(
|
||||
languageOptions,
|
||||
languageOptions.length > 1
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (combinedConfig.length > 0) {
|
||||
exportElements.push(
|
||||
ts.factory.createObjectLiteralExpression(
|
||||
combinedConfig,
|
||||
combinedConfig.length > 1
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (config.rules) {
|
||||
exportElements.push(generateAst({ rules: config.rules }));
|
||||
}
|
||||
|
||||
if (config.overrides) {
|
||||
config.overrides.forEach((override) => {
|
||||
updateFiles(override, root);
|
||||
if (
|
||||
override.env ||
|
||||
override.extends ||
|
||||
override.plugins ||
|
||||
override.parser
|
||||
) {
|
||||
isFlatCompatNeeded = true;
|
||||
addFlattenedOverride(override, exportElements);
|
||||
} else {
|
||||
exportElements.push(generateAst(override));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (config.ignorePatterns) {
|
||||
const patterns = (
|
||||
Array.isArray(config.ignorePatterns)
|
||||
? config.ignorePatterns
|
||||
: [config.ignorePatterns]
|
||||
).filter((pattern) => !['**/*', '!**/*', 'node_modules'].includes(pattern)); // these are useless in a flat config
|
||||
if (patterns.length > 0) {
|
||||
exportElements.push(
|
||||
generateAst({
|
||||
ignores: patterns.map((path) => mapFilePath(path, root)),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (tree.exists(`${root}/.eslintignore`)) {
|
||||
const patterns = tree
|
||||
.read(`${root}/.eslintignore`, 'utf-8')
|
||||
.split('\n')
|
||||
.filter((line) => line.length > 0 && line !== 'node_modules')
|
||||
.map((path) => mapFilePath(path, root));
|
||||
if (patterns.length > 0) {
|
||||
exportElements.push(generateAst({ ignores: patterns }));
|
||||
}
|
||||
}
|
||||
|
||||
tree.delete(join(root, sourceFile));
|
||||
tree.delete(join(root, '.eslintignore'));
|
||||
|
||||
// create the node list and print it to new file
|
||||
const nodeList = createNodeList(
|
||||
importsMap,
|
||||
exportElements,
|
||||
isFlatCompatNeeded
|
||||
);
|
||||
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
||||
const resultFile = ts.createSourceFile(
|
||||
join(root, destinationFile),
|
||||
'',
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
ts.ScriptKind.JS
|
||||
);
|
||||
const result = printer.printList(
|
||||
ts.ListFormat.MultiLine,
|
||||
nodeList,
|
||||
resultFile
|
||||
);
|
||||
tree.write(join(root, destinationFile), result);
|
||||
|
||||
if (isFlatCompatNeeded) {
|
||||
addDependenciesToPackageJson(
|
||||
tree,
|
||||
{},
|
||||
{
|
||||
'@eslint/eslintrc': eslintrcVersion,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function updateFiles(
|
||||
override: Linter.ConfigOverride<Linter.RulesRecord>,
|
||||
root: string
|
||||
) {
|
||||
if (override.files) {
|
||||
override.files = Array.isArray(override.files)
|
||||
? override.files
|
||||
: [override.files];
|
||||
override.files = override.files.map((file) => mapFilePath(file, root));
|
||||
}
|
||||
return override;
|
||||
}
|
||||
|
||||
function mapFilePath(filePath: string, root: string) {
|
||||
if (filePath.startsWith('!')) {
|
||||
const fileWithoutBang = filePath.slice(1);
|
||||
if (fileWithoutBang.startsWith('*.')) {
|
||||
return `!${join(root, '**', fileWithoutBang)}`;
|
||||
} else {
|
||||
return `!${join(root, fileWithoutBang)}`;
|
||||
}
|
||||
}
|
||||
if (filePath.startsWith('*.')) {
|
||||
return join(root, '**', filePath);
|
||||
} else {
|
||||
return join(root, filePath);
|
||||
}
|
||||
}
|
||||
|
||||
// add parsed extends to export blocks and add import statements
|
||||
function addExtends(
|
||||
importsMap: Map<string, string | string[]>,
|
||||
configBlocks: ts.Expression[],
|
||||
config: ESLint.ConfigData,
|
||||
tree: Tree
|
||||
): boolean {
|
||||
let isFlatCompatNeeded = false;
|
||||
const extendsConfig = Array.isArray(config.extends)
|
||||
? config.extends
|
||||
: [config.extends];
|
||||
|
||||
const eslintrcConfigs = [];
|
||||
|
||||
// add base extends
|
||||
extendsConfig
|
||||
.filter((imp) => imp.match(/^\.?(\.\/)/))
|
||||
.forEach((imp, index) => {
|
||||
if (imp.match(/\.eslintrc(.base)?\.json$/)) {
|
||||
const localName = index ? `baseConfig${index}` : 'baseConfig';
|
||||
configBlocks.push(
|
||||
ts.factory.createSpreadElement(ts.factory.createIdentifier(localName))
|
||||
);
|
||||
const newImport = imp.replace(
|
||||
/^(.*)\.eslintrc(.base)?\.json$/,
|
||||
'$1eslint$2.config.js'
|
||||
);
|
||||
importsMap.set(newImport, localName);
|
||||
} else {
|
||||
eslintrcConfigs.push(imp);
|
||||
}
|
||||
});
|
||||
// add plugin extends
|
||||
const pluginExtends = extendsConfig.filter((imp) => !imp.match(/^\.?(\.\/)/));
|
||||
if (pluginExtends.length) {
|
||||
const eslintPluginExtends = pluginExtends.filter((imp) =>
|
||||
imp.startsWith('eslint:')
|
||||
);
|
||||
pluginExtends.forEach((imp) => {
|
||||
if (!imp.startsWith('eslint:')) {
|
||||
eslintrcConfigs.push(imp);
|
||||
}
|
||||
});
|
||||
|
||||
if (eslintPluginExtends.length) {
|
||||
addDependenciesToPackageJson(
|
||||
tree,
|
||||
{},
|
||||
{
|
||||
'@eslint/js': eslintrcVersion,
|
||||
}
|
||||
);
|
||||
|
||||
importsMap.set('@eslint/js', 'js');
|
||||
eslintPluginExtends.forEach((plugin) => {
|
||||
configBlocks.push(
|
||||
ts.factory.createPropertyAccessExpression(
|
||||
ts.factory.createPropertyAccessExpression(
|
||||
ts.factory.createIdentifier('js'),
|
||||
ts.factory.createIdentifier('configs')
|
||||
),
|
||||
ts.factory.createIdentifier(plugin.slice(7)) // strip 'eslint:' prefix
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (eslintrcConfigs.length) {
|
||||
isFlatCompatNeeded = true;
|
||||
addDependenciesToPackageJson(
|
||||
tree,
|
||||
{},
|
||||
{
|
||||
'@eslint/js': eslintrcVersion,
|
||||
}
|
||||
);
|
||||
|
||||
const pluginExtendsSpread = ts.factory.createSpreadElement(
|
||||
ts.factory.createCallExpression(
|
||||
ts.factory.createPropertyAccessExpression(
|
||||
ts.factory.createIdentifier('compat'),
|
||||
ts.factory.createIdentifier('extends')
|
||||
),
|
||||
undefined,
|
||||
eslintrcConfigs.map((plugin) => ts.factory.createStringLiteral(plugin))
|
||||
)
|
||||
);
|
||||
configBlocks.push(pluginExtendsSpread);
|
||||
}
|
||||
|
||||
return isFlatCompatNeeded;
|
||||
}
|
||||
|
||||
function getPluginImport(pluginName: string): string {
|
||||
if (pluginName.includes('eslint-plugin-')) {
|
||||
return pluginName;
|
||||
}
|
||||
if (!pluginName.startsWith('@')) {
|
||||
return `eslint-plugin-${pluginName}`;
|
||||
}
|
||||
if (!pluginName.includes('/')) {
|
||||
return `${pluginName}/eslint-plugin`;
|
||||
}
|
||||
const [scope, name] = pluginName.split('/');
|
||||
return `${scope}/eslint-plugin-${name}`;
|
||||
}
|
||||
|
||||
function addPlugins(
|
||||
importsMap: Map<string, string | string[]>,
|
||||
configBlocks: ts.Expression[],
|
||||
config: ESLint.ConfigData
|
||||
) {
|
||||
const mappedPlugins: { name: string; varName: string; imp: string }[] = [];
|
||||
config.plugins.forEach((name) => {
|
||||
const imp = getPluginImport(name);
|
||||
const varName = names(imp).propertyName;
|
||||
mappedPlugins.push({ name, varName, imp });
|
||||
});
|
||||
mappedPlugins.forEach(({ varName, imp }) => {
|
||||
importsMap.set(imp, varName);
|
||||
});
|
||||
const pluginsAst = ts.factory.createObjectLiteralExpression(
|
||||
[
|
||||
ts.factory.createPropertyAssignment(
|
||||
'plugins',
|
||||
ts.factory.createObjectLiteralExpression(
|
||||
mappedPlugins.map(({ name, varName }) => {
|
||||
return ts.factory.createPropertyAssignment(
|
||||
ts.factory.createStringLiteral(name),
|
||||
ts.factory.createIdentifier(varName)
|
||||
);
|
||||
}),
|
||||
mappedPlugins.length > 1
|
||||
)
|
||||
),
|
||||
...(config.processor
|
||||
? [
|
||||
ts.factory.createPropertyAssignment(
|
||||
'processor',
|
||||
ts.factory.createStringLiteral(config.processor)
|
||||
),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
false
|
||||
);
|
||||
configBlocks.push(pluginsAst);
|
||||
}
|
||||
|
||||
function addParser(
|
||||
importsMap: Map<string, string>,
|
||||
config: ESLint.ConfigData
|
||||
): ts.PropertyAssignment {
|
||||
const imp = config.parser;
|
||||
const parserName = names(imp).propertyName;
|
||||
importsMap.set(imp, parserName);
|
||||
|
||||
return ts.factory.createPropertyAssignment(
|
||||
'parser',
|
||||
ts.factory.createIdentifier(parserName)
|
||||
);
|
||||
}
|
||||
|
||||
function addFlattenedOverride(
|
||||
override: Linter.ConfigOverride<Linter.RulesRecord>,
|
||||
configBlocks: ts.Expression[]
|
||||
) {
|
||||
const { files, excludedFiles, rules, ...rest } = override;
|
||||
|
||||
const objectLiteralElements: ts.ObjectLiteralElementLike[] = [
|
||||
ts.factory.createSpreadAssignment(ts.factory.createIdentifier('config')),
|
||||
];
|
||||
if (files) {
|
||||
objectLiteralElements.push(
|
||||
ts.factory.createPropertyAssignment('files', generateAst(files))
|
||||
);
|
||||
}
|
||||
if (excludedFiles) {
|
||||
objectLiteralElements.push(
|
||||
ts.factory.createPropertyAssignment(
|
||||
'excludedFiles',
|
||||
generateAst(excludedFiles)
|
||||
)
|
||||
);
|
||||
}
|
||||
if (rules) {
|
||||
objectLiteralElements.push(
|
||||
ts.factory.createPropertyAssignment('rules', generateAst(rules))
|
||||
);
|
||||
}
|
||||
|
||||
const overrideSpread = ts.factory.createSpreadElement(
|
||||
ts.factory.createCallExpression(
|
||||
ts.factory.createPropertyAccessExpression(
|
||||
ts.factory.createCallExpression(
|
||||
ts.factory.createPropertyAccessExpression(
|
||||
ts.factory.createIdentifier('compat'),
|
||||
ts.factory.createIdentifier('config')
|
||||
),
|
||||
undefined,
|
||||
[generateAst(rest)]
|
||||
),
|
||||
ts.factory.createIdentifier('map')
|
||||
),
|
||||
undefined,
|
||||
[
|
||||
ts.factory.createArrowFunction(
|
||||
undefined,
|
||||
undefined,
|
||||
[
|
||||
ts.factory.createParameterDeclaration(
|
||||
undefined,
|
||||
undefined,
|
||||
'config'
|
||||
),
|
||||
],
|
||||
undefined,
|
||||
ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
|
||||
ts.factory.createParenthesizedExpression(
|
||||
ts.factory.createObjectLiteralExpression(
|
||||
objectLiteralElements,
|
||||
true
|
||||
)
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
);
|
||||
configBlocks.push(overrideSpread);
|
||||
}
|
||||
|
||||
const DEFAULT_FLAT_CONFIG = `
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
`;
|
||||
|
||||
function createNodeList(
|
||||
importsMap: Map<string, string>,
|
||||
exportElements: ts.Expression[],
|
||||
isFlatCompatNeeded: boolean
|
||||
): ts.NodeArray<
|
||||
ts.VariableStatement | ts.Identifier | ts.ExpressionStatement | ts.SourceFile
|
||||
> {
|
||||
const importsList = [];
|
||||
if (isFlatCompatNeeded) {
|
||||
importsMap.set('@eslint/js', 'js');
|
||||
|
||||
importsList.push(
|
||||
generateRequire(
|
||||
ts.factory.createObjectBindingPattern([
|
||||
ts.factory.createBindingElement(undefined, undefined, 'FlatCompat'),
|
||||
]),
|
||||
'@eslint/eslintrc'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// generateRequire(varName, imp, ts.factory);
|
||||
Array.from(importsMap.entries()).forEach(([imp, varName]) => {
|
||||
importsList.push(generateRequire(varName, imp));
|
||||
});
|
||||
|
||||
return ts.factory.createNodeArray([
|
||||
// add plugin imports
|
||||
...importsList,
|
||||
ts.createSourceFile(
|
||||
'',
|
||||
isFlatCompatNeeded ? DEFAULT_FLAT_CONFIG : '',
|
||||
ts.ScriptTarget.Latest,
|
||||
false,
|
||||
ts.ScriptKind.JS
|
||||
),
|
||||
// creates:
|
||||
// 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)
|
||||
)
|
||||
),
|
||||
]);
|
||||
}
|
||||
@ -0,0 +1,350 @@
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
import {
|
||||
NxJsonConfiguration,
|
||||
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';
|
||||
|
||||
describe('convert-to-flat-config generator', () => {
|
||||
let tree: Tree;
|
||||
const options: ConvertToFlatConfigGeneratorSchema = { skipFormat: false };
|
||||
|
||||
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;
|
||||
});
|
||||
});
|
||||
|
||||
it('should run successfully', async () => {
|
||||
await lintProjectGenerator(tree, {
|
||||
skipFormat: false,
|
||||
linter: Linter.EsLint,
|
||||
eslintFilePatterns: ['**/*.ts'],
|
||||
project: 'test-lib',
|
||||
setParserOptionsProject: false,
|
||||
});
|
||||
await convertToFlatConfigGenerator(tree, options);
|
||||
|
||||
expect(tree.exists('eslint.config.js')).toBeTruthy();
|
||||
expect(tree.read('eslint.config.js', 'utf-8')).toMatchSnapshot();
|
||||
expect(tree.exists('libs/test-lib/eslint.config.js')).toBeTruthy();
|
||||
expect(
|
||||
tree.read('libs/test-lib/eslint.config.js', 'utf-8')
|
||||
).toMatchSnapshot();
|
||||
// check nx.json changes
|
||||
const nxJson = readJson(tree, 'nx.json');
|
||||
expect(nxJson.targetDefaults.lint.inputs).toContain(
|
||||
'{workspaceRoot}/eslint.config.js'
|
||||
);
|
||||
expect(nxJson.namedInputs.production).toContain(
|
||||
'!{projectRoot}/eslint.config.js'
|
||||
);
|
||||
});
|
||||
|
||||
it('should add plugin extends', async () => {
|
||||
await lintProjectGenerator(tree, {
|
||||
skipFormat: false,
|
||||
linter: Linter.EsLint,
|
||||
eslintFilePatterns: ['**/*.ts'],
|
||||
project: 'test-lib',
|
||||
setParserOptionsProject: false,
|
||||
});
|
||||
updateJson(tree, '.eslintrc.json', (json) => {
|
||||
json.extends = ['plugin:storybook/recommended'];
|
||||
return json;
|
||||
});
|
||||
await convertToFlatConfigGenerator(tree, options);
|
||||
|
||||
expect(tree.read('eslint.config.js', 'utf-8')).toMatchInlineSnapshot(`
|
||||
"const { FlatCompat } = require('@eslint/eslintrc');
|
||||
const nxEslintPlugin = require('@nx/eslint-plugin');
|
||||
const js = require('@eslint/js');
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
module.exports = [
|
||||
...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'],
|
||||
rules: {},
|
||||
})),
|
||||
...compat.config({ extends: ['plugin:@nx/javascript'] }).map((config) => ({
|
||||
...config,
|
||||
files: ['**/*.js', '**/*.jsx'],
|
||||
rules: {},
|
||||
})),
|
||||
];
|
||||
"
|
||||
`);
|
||||
expect(tree.read('libs/test-lib/eslint.config.js', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"const baseConfig = require('../../eslint.config.js');
|
||||
module.exports = [
|
||||
...baseConfig,
|
||||
{
|
||||
files: [
|
||||
'libs/test-lib/**/*.ts',
|
||||
'libs/test-lib/**/*.tsx',
|
||||
'libs/test-lib/**/*.js',
|
||||
'libs/test-lib/**/*.jsx',
|
||||
],
|
||||
rules: {},
|
||||
},
|
||||
{
|
||||
files: ['libs/test-lib/**/*.ts', 'libs/test-lib/**/*.tsx'],
|
||||
rules: {},
|
||||
},
|
||||
{
|
||||
files: ['libs/test-lib/**/*.js', 'libs/test-lib/**/*.jsx'],
|
||||
rules: {},
|
||||
},
|
||||
];
|
||||
"
|
||||
`);
|
||||
expect(
|
||||
readJson(tree, 'package.json').devDependencies['@eslint/eslintrc']
|
||||
).toEqual(eslintrcVersion);
|
||||
});
|
||||
|
||||
it('should add global gitignores', async () => {
|
||||
await lintProjectGenerator(tree, {
|
||||
skipFormat: false,
|
||||
linter: Linter.EsLint,
|
||||
eslintFilePatterns: ['**/*.ts'],
|
||||
project: 'test-lib',
|
||||
setParserOptionsProject: false,
|
||||
});
|
||||
tree.write('.eslintignore', 'ignore/me');
|
||||
await convertToFlatConfigGenerator(tree, options);
|
||||
|
||||
expect(tree.read('eslint.config.js', 'utf-8')).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should add settings', async () => {
|
||||
await lintProjectGenerator(tree, {
|
||||
skipFormat: false,
|
||||
linter: Linter.EsLint,
|
||||
eslintFilePatterns: ['**/*.ts'],
|
||||
project: 'test-lib',
|
||||
setParserOptionsProject: false,
|
||||
});
|
||||
updateJson(tree, '.eslintrc.json', (json) => {
|
||||
json.settings = {
|
||||
sharedData: 'Hello',
|
||||
};
|
||||
return json;
|
||||
});
|
||||
await convertToFlatConfigGenerator(tree, options);
|
||||
|
||||
expect(tree.read('eslint.config.js', 'utf-8')).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should add env configuration', async () => {
|
||||
await lintProjectGenerator(tree, {
|
||||
skipFormat: false,
|
||||
linter: Linter.EsLint,
|
||||
eslintFilePatterns: ['**/*.ts'],
|
||||
project: 'test-lib',
|
||||
setParserOptionsProject: false,
|
||||
});
|
||||
updateJson(tree, '.eslintrc.json', (json) => {
|
||||
json.env = {
|
||||
browser: true,
|
||||
node: true,
|
||||
};
|
||||
return json;
|
||||
});
|
||||
await convertToFlatConfigGenerator(tree, options);
|
||||
|
||||
expect(tree.read('eslint.config.js', 'utf-8')).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should add global configuration', async () => {
|
||||
await lintProjectGenerator(tree, {
|
||||
skipFormat: false,
|
||||
linter: Linter.EsLint,
|
||||
eslintFilePatterns: ['**/*.ts'],
|
||||
project: 'test-lib',
|
||||
setParserOptionsProject: false,
|
||||
});
|
||||
updateJson(tree, '.eslintrc.json', (json) => {
|
||||
json.globals = {
|
||||
myCustomGlobal: 'readonly',
|
||||
};
|
||||
return json;
|
||||
});
|
||||
await convertToFlatConfigGenerator(tree, options);
|
||||
|
||||
expect(tree.read('eslint.config.js', 'utf-8')).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should add global and env configuration', async () => {
|
||||
await lintProjectGenerator(tree, {
|
||||
skipFormat: false,
|
||||
linter: Linter.EsLint,
|
||||
eslintFilePatterns: ['**/*.ts'],
|
||||
project: 'test-lib',
|
||||
setParserOptionsProject: false,
|
||||
});
|
||||
updateJson(tree, '.eslintrc.json', (json) => {
|
||||
json.globals = {
|
||||
myCustomGlobal: 'readonly',
|
||||
};
|
||||
json.env = {
|
||||
browser: true,
|
||||
};
|
||||
return json;
|
||||
});
|
||||
await convertToFlatConfigGenerator(tree, options);
|
||||
|
||||
expect(tree.read('eslint.config.js', 'utf-8')).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should add plugins', async () => {
|
||||
await lintProjectGenerator(tree, {
|
||||
skipFormat: false,
|
||||
linter: Linter.EsLint,
|
||||
eslintFilePatterns: ['**/*.ts'],
|
||||
project: 'test-lib',
|
||||
setParserOptionsProject: false,
|
||||
});
|
||||
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.js', 'utf-8')).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should add parser', async () => {
|
||||
await lintProjectGenerator(tree, {
|
||||
skipFormat: false,
|
||||
linter: Linter.EsLint,
|
||||
eslintFilePatterns: ['**/*.ts'],
|
||||
project: 'test-lib',
|
||||
setParserOptionsProject: false,
|
||||
});
|
||||
updateJson(tree, '.eslintrc.json', (json) => {
|
||||
json.parser = '@typescript-eslint/parser';
|
||||
return json;
|
||||
});
|
||||
await convertToFlatConfigGenerator(tree, options);
|
||||
|
||||
expect(tree.read('eslint.config.js', 'utf-8')).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should add linter options', async () => {
|
||||
await lintProjectGenerator(tree, {
|
||||
skipFormat: false,
|
||||
linter: Linter.EsLint,
|
||||
eslintFilePatterns: ['**/*.ts'],
|
||||
project: 'test-lib',
|
||||
setParserOptionsProject: false,
|
||||
});
|
||||
updateJson(tree, '.eslintrc.json', (json) => {
|
||||
json.noInlineConfig = true;
|
||||
return json;
|
||||
});
|
||||
await convertToFlatConfigGenerator(tree, options);
|
||||
|
||||
expect(tree.read('eslint.config.js', 'utf-8')).toMatchInlineSnapshot(`
|
||||
"const { FlatCompat } = require('@eslint/eslintrc');
|
||||
const nxEslintPlugin = require('@nx/eslint-plugin');
|
||||
const js = require('@eslint/js');
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
module.exports = [
|
||||
{ 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'],
|
||||
rules: {},
|
||||
})),
|
||||
...compat.config({ extends: ['plugin:@nx/javascript'] }).map((config) => ({
|
||||
...config,
|
||||
files: ['**/*.js', '**/*.jsx'],
|
||||
rules: {},
|
||||
})),
|
||||
];
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,129 @@
|
||||
import {
|
||||
formatFiles,
|
||||
getProjects,
|
||||
NxJsonConfiguration,
|
||||
ProjectConfiguration,
|
||||
readNxJson,
|
||||
Tree,
|
||||
updateJson,
|
||||
updateProjectConfiguration,
|
||||
} from '@nx/devkit';
|
||||
import { ConvertToFlatConfigGeneratorSchema } from './schema';
|
||||
import { findEslintFile } from '../utils/eslint-file';
|
||||
import { convertEslintJsonToFlatConfig } from './converters/json-converter';
|
||||
|
||||
export async function convertToFlatConfigGenerator(
|
||||
tree: Tree,
|
||||
options: ConvertToFlatConfigGeneratorSchema
|
||||
) {
|
||||
const eslintFile = findEslintFile(tree);
|
||||
if (!eslintFile) {
|
||||
throw new Error('Could not find root eslint file');
|
||||
}
|
||||
if (!eslintFile.endsWith('.json')) {
|
||||
throw new Error(
|
||||
'Only json eslint config files are supported for conversion'
|
||||
);
|
||||
}
|
||||
|
||||
// rename root eslint config to eslint.config.js
|
||||
convertRootToFlatConfig(tree, eslintFile);
|
||||
// rename and map files
|
||||
const projects = getProjects(tree);
|
||||
for (const [project, projectConfig] of projects) {
|
||||
convertProjectToFlatConfig(tree, project, projectConfig, readNxJson(tree));
|
||||
}
|
||||
// replace references in nx.json
|
||||
updateNxJsonConfig(tree);
|
||||
// install missing packages
|
||||
|
||||
if (!options.skipFormat) {
|
||||
await formatFiles(tree);
|
||||
}
|
||||
}
|
||||
|
||||
export default convertToFlatConfigGenerator;
|
||||
|
||||
function convertRootToFlatConfig(tree: Tree, eslintFile: string) {
|
||||
if (eslintFile.endsWith('.base.json')) {
|
||||
convertConfigToFlatConfig(
|
||||
tree,
|
||||
'',
|
||||
'.eslintrc.base.json',
|
||||
'eslint.base.config.js'
|
||||
);
|
||||
}
|
||||
convertConfigToFlatConfig(tree, '', '.eslintrc.json', 'eslint.config.js');
|
||||
}
|
||||
|
||||
function convertProjectToFlatConfig(
|
||||
tree: Tree,
|
||||
project: string,
|
||||
projectConfig: ProjectConfiguration,
|
||||
nxJson: NxJsonConfiguration
|
||||
) {
|
||||
if (tree.exists(`${projectConfig.root}/.eslintrc.json`)) {
|
||||
if (projectConfig.targets) {
|
||||
const eslintTargets = Object.keys(projectConfig.targets || {}).filter(
|
||||
(t) => projectConfig.targets[t].executor === '@nx/linter:eslint'
|
||||
);
|
||||
for (const target of eslintTargets) {
|
||||
// remove any obsolete `eslintConfig` options pointing to the old config file
|
||||
if (projectConfig.targets[target].options?.eslintConfig) {
|
||||
delete projectConfig.targets[target].options.eslintConfig;
|
||||
}
|
||||
updateProjectConfiguration(tree, project, projectConfig);
|
||||
}
|
||||
const nxHasLintTargets = Object.keys(nxJson.targetDefaults || {}).some(
|
||||
(t) =>
|
||||
(t === '@nx/linter:eslint' ||
|
||||
nxJson.targetDefaults[t].executor === '@nx/linter:eslint') &&
|
||||
projectConfig.targets?.[t]
|
||||
);
|
||||
if (nxHasLintTargets || eslintTargets.length > 0) {
|
||||
convertConfigToFlatConfig(
|
||||
tree,
|
||||
projectConfig.root,
|
||||
'.eslintrc.json',
|
||||
'eslint.config.js'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update names of eslint files in nx.json
|
||||
// and remove eslintignore
|
||||
function updateNxJsonConfig(tree: Tree) {
|
||||
if (tree.exists('nx.json')) {
|
||||
updateJson(tree, 'nx.json', (json: NxJsonConfiguration) => {
|
||||
if (json.targetDefaults?.lint?.inputs) {
|
||||
const inputSet = new Set(json.targetDefaults.lint.inputs);
|
||||
inputSet.add('{workspaceRoot}/eslint.config.js');
|
||||
json.targetDefaults.lint.inputs = Array.from(inputSet);
|
||||
}
|
||||
if (json.targetDefaults?.['@nx/linter:eslint']?.inputs) {
|
||||
const inputSet = new Set(
|
||||
json.targetDefaults['@nx/linter:eslint'].inputs
|
||||
);
|
||||
inputSet.add('{workspaceRoot}/eslint.config.js');
|
||||
json.targetDefaults['@nx/linter:eslint'].inputs = Array.from(inputSet);
|
||||
}
|
||||
if (json.namedInputs?.production) {
|
||||
const inputSet = new Set(json.namedInputs.production);
|
||||
inputSet.add('!{projectRoot}/eslint.config.js');
|
||||
json.namedInputs.production = Array.from(inputSet);
|
||||
}
|
||||
return json;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function convertConfigToFlatConfig(
|
||||
tree: Tree,
|
||||
root: string,
|
||||
source: string,
|
||||
target: string
|
||||
) {
|
||||
convertEslintJsonToFlatConfig(tree, root, source, target);
|
||||
}
|
||||
3
packages/linter/src/generators/convert-to-flat-config/schema.d.ts
vendored
Normal file
3
packages/linter/src/generators/convert-to-flat-config/schema.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
export interface ConvertToFlatConfigGeneratorSchema {
|
||||
skipFormat?: boolean;
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/schema",
|
||||
"$id": "ConvertToFlatConfig",
|
||||
"cli": "nx",
|
||||
"description": "Convert an Nx workspace to a Flat ESLint config.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"skipFormat": {
|
||||
"type": "boolean",
|
||||
"description": "Skip formatting files.",
|
||||
"default": false,
|
||||
"x-priority": "internal"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": []
|
||||
}
|
||||
@ -34,6 +34,7 @@ describe('@nx/linter:init', () => {
|
||||
'default',
|
||||
'{workspaceRoot}/.eslintrc.json',
|
||||
'{workspaceRoot}/.eslintignore',
|
||||
'{workspaceRoot}/eslint.config.js',
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
@ -32,6 +32,7 @@ function addTargetDefaults(tree: Tree) {
|
||||
if (productionFileSet) {
|
||||
// Remove .eslintrc.json
|
||||
productionFileSet.push('!{projectRoot}/.eslintrc.json');
|
||||
productionFileSet.push('!{projectRoot}/eslint.config.js');
|
||||
// Dedupe and set
|
||||
nxJson.namedInputs.production = Array.from(new Set(productionFileSet));
|
||||
}
|
||||
@ -43,6 +44,7 @@ function addTargetDefaults(tree: Tree) {
|
||||
'default',
|
||||
`{workspaceRoot}/.eslintrc.json`,
|
||||
`{workspaceRoot}/.eslintignore`,
|
||||
`{workspaceRoot}/eslint.config.js`,
|
||||
];
|
||||
updateNxJson(tree, nxJson);
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
export const nxVersion = require('../../package.json').version;
|
||||
|
||||
export const eslintVersion = '~8.46.0';
|
||||
export const eslintrcVersion = '^2.1.1';
|
||||
export const eslintConfigPrettierVersion = '8.1.0';
|
||||
/** @deprecated This will be removed in v17 */
|
||||
export const tslintToEslintConfigVersion = '^2.13.0';
|
||||
|
||||
@ -90,7 +90,9 @@ function createNxJson(
|
||||
karmaProjectConfigFile ? '!{projectRoot}/karma.conf.js' : undefined,
|
||||
].filter(Boolean)
|
||||
: []),
|
||||
eslintProjectConfigFile ? '!{projectRoot}/.eslintrc.json' : undefined,
|
||||
...(eslintProjectConfigFile
|
||||
? ['!{projectRoot}/.eslintrc.json', '!{projectRoot}/eslint.config.js']
|
||||
: []),
|
||||
].filter(Boolean),
|
||||
};
|
||||
nxJson.targetDefaults = {};
|
||||
@ -115,6 +117,9 @@ function createNxJson(
|
||||
if (fileExists(join(repoRoot, '.eslintrc.json'))) {
|
||||
inputs.push('{workspaceRoot}/.eslintrc.json');
|
||||
}
|
||||
if (fileExists(join(repoRoot, 'eslint.config.js'))) {
|
||||
inputs.push('{workspaceRoot}/eslint.config.js');
|
||||
}
|
||||
nxJson.targetDefaults.lint = { inputs };
|
||||
}
|
||||
if (workspaceTargets.includes('e2e')) {
|
||||
|
||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@ -217,6 +217,12 @@ devDependencies:
|
||||
'@babel/runtime':
|
||||
specifier: ^7.22.6
|
||||
version: 7.22.6
|
||||
'@eslint/eslintrc':
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
'@eslint/js':
|
||||
specifier: ^8.46.0
|
||||
version: 8.46.0
|
||||
'@floating-ui/react':
|
||||
specifier: 0.19.2
|
||||
version: 0.19.2(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user