feat(linter): add convert-to-inferred migration generator (#23142)

This commit is contained in:
Colum Ferry 2024-05-03 14:23:01 +01:00 committed by GitHub
parent 1d2c843c26
commit acd0993f1a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 919 additions and 8 deletions

View File

@ -7310,6 +7310,14 @@
"children": [],
"isExternal": false,
"disableCollapsible": false
},
{
"id": "convert-to-inferred",
"path": "/nx-api/eslint/generators/convert-to-inferred",
"name": "convert-to-inferred",
"children": [],
"isExternal": false,
"disableCollapsible": false
}
],
"isExternal": false,

View File

@ -766,6 +766,15 @@
"originalFilePath": "/packages/eslint/src/generators/convert-to-flat-config/schema.json",
"path": "/nx-api/eslint/generators/convert-to-flat-config",
"type": "generator"
},
"/nx-api/eslint/generators/convert-to-inferred": {
"description": "Convert existing ESLint project(s) using `@nx/eslint:lint` executor to use `@nx/eslint/plugin`.",
"file": "generated/packages/eslint/generators/convert-to-inferred.json",
"hidden": false,
"name": "convert-to-inferred",
"originalFilePath": "/packages/eslint/src/generators/convert-to-inferred/schema.json",
"path": "/nx-api/eslint/generators/convert-to-inferred",
"type": "generator"
}
},
"path": "/nx-api/eslint"

View File

@ -754,6 +754,15 @@
"originalFilePath": "/packages/eslint/src/generators/convert-to-flat-config/schema.json",
"path": "eslint/generators/convert-to-flat-config",
"type": "generator"
},
{
"description": "Convert existing ESLint project(s) using `@nx/eslint:lint` executor to use `@nx/eslint/plugin`.",
"file": "generated/packages/eslint/generators/convert-to-inferred.json",
"hidden": false,
"name": "convert-to-inferred",
"originalFilePath": "/packages/eslint/src/generators/convert-to-inferred/schema.json",
"path": "eslint/generators/convert-to-inferred",
"type": "generator"
}
],
"githubRoot": "https://github.com/nrwl/nx/blob/master",

View File

@ -0,0 +1,30 @@
{
"name": "convert-to-inferred",
"factory": "./src/generators/convert-to-inferred/convert-to-inferred",
"schema": {
"$schema": "https://json-schema.org/schema",
"$id": "NxEslintConvertToInferred",
"description": "Convert existing Eslint project(s) using `@nx/eslint:lint` executor to use `@nx/eslint/plugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.",
"title": "Convert Eslint project from executor to plugin",
"type": "object",
"properties": {
"project": {
"type": "string",
"description": "The project to convert from using the `@nx/eslint:lint` executor to use `@nx/eslint/plugin`.",
"x-priority": "important"
},
"skipFormat": {
"type": "boolean",
"description": "Whether to format files at the end of the migration.",
"default": false
}
},
"presets": []
},
"description": "Convert existing ESLint project(s) using `@nx/eslint:lint` executor to use `@nx/eslint/plugin`.",
"implementation": "/packages/eslint/src/generators/convert-to-inferred/convert-to-inferred.ts",
"aliases": [],
"hidden": false,
"path": "/packages/eslint/src/generators/convert-to-inferred/schema.json",
"type": "generator"
}

View File

@ -403,6 +403,7 @@
- [workspace-rules-project](/nx-api/eslint/generators/workspace-rules-project)
- [workspace-rule](/nx-api/eslint/generators/workspace-rule)
- [convert-to-flat-config](/nx-api/eslint/generators/convert-to-flat-config)
- [convert-to-inferred](/nx-api/eslint/generators/convert-to-inferred)
- [eslint-plugin](/nx-api/eslint-plugin)
- [documents](/nx-api/eslint-plugin/documents)
- [Overview](/nx-api/eslint-plugin/documents/overview)

View File

@ -21,7 +21,7 @@ interface Schema {
export async function convertToInferred(tree: Tree, options: Schema) {
const projectGraph = await createProjectGraphAsync();
let migratedProjects = await migrateExecutorToPlugin(
const migratedProjectsModern = await migrateExecutorToPlugin(
tree,
projectGraph,
'@nx/cypress:cypress',
@ -35,7 +35,7 @@ export async function convertToInferred(tree: Tree, options: Schema) {
options.project
);
migratedProjects += await migrateExecutorToPlugin(
const migratedProjectsLegacy = await migrateExecutorToPlugin(
tree,
projectGraph,
'@nrwl/cypress:cypress',
@ -49,6 +49,9 @@ export async function convertToInferred(tree: Tree, options: Schema) {
options.project
);
const migratedProjects =
migratedProjectsModern.size + migratedProjectsLegacy.size;
if (migratedProjects === 0) {
throw new Error('Could not find any targets to migrate.');
}

View File

@ -29,7 +29,8 @@ import type { ConfigurationResult } from 'nx/src/project-graph/utils/project-con
type PluginOptionsBuilder<T> = (targetName: string) => T;
type PostTargetTransformer = (
targetConfiguration: TargetConfiguration,
tree?: Tree
tree?: Tree,
projectDetails?: { projectName: string; root: string }
) => TargetConfiguration;
type SkipTargetFilter = (
targetConfiguration: TargetConfiguration
@ -129,7 +130,10 @@ class ExecutorToPluginMigrator<T> {
delete projectTarget.executor;
deleteMatchingProperties(projectTarget, createdTarget);
projectTarget = this.#postTargetTransformer(projectTarget, this.tree);
projectTarget = this.#postTargetTransformer(projectTarget, this.tree, {
projectName,
root: projectFromGraph.data.root,
});
if (
projectTarget.options &&
@ -308,7 +312,7 @@ export async function migrateExecutorToPlugin<T>(
createNodes: CreateNodes<T>,
specificProjectToMigrate?: string,
skipTargetFilter?: SkipTargetFilter
): Promise<number> {
): Promise<Map<string, Set<string>>> {
const migrator = new ExecutorToPluginMigrator<T>(
tree,
projectGraph,
@ -320,6 +324,5 @@ export async function migrateExecutorToPlugin<T>(
specificProjectToMigrate,
skipTargetFilter
);
const migratedProjectsAndTargets = await migrator.run();
return migratedProjectsAndTargets.size;
return await migrator.run();
}

View File

@ -23,6 +23,11 @@
"factory": "./src/generators/convert-to-flat-config/generator",
"schema": "./src/generators/convert-to-flat-config/schema.json",
"description": "Convert an Nx workspace's ESLint configs to use Flat Config."
},
"convert-to-inferred": {
"factory": "./src/generators/convert-to-inferred/convert-to-inferred",
"schema": "./src/generators/convert-to-inferred/schema.json",
"description": "Convert existing ESLint project(s) using `@nx/eslint:lint` executor to use `@nx/eslint/plugin`."
}
}
}

View File

@ -0,0 +1,671 @@
import {
getRelativeProjectJsonSchemaPath,
updateProjectConfiguration,
} from 'nx/src/generators/utils/project-configuration';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { convertToInferred } from './convert-to-inferred';
import {
addProjectConfiguration as _addProjectConfiguration,
type ExpandedPluginConfiguration,
joinPathFragments,
type ProjectConfiguration,
type ProjectGraph,
readNxJson,
readProjectConfiguration,
type Tree,
updateNxJson,
writeJson,
} from '@nx/devkit';
import { TempFs } from '@nx/devkit/internal-testing-utils';
import { join } from 'node:path';
let fs: TempFs;
let projectGraph: ProjectGraph;
jest.mock('@nx/devkit', () => ({
...jest.requireActual<any>('@nx/devkit'),
createProjectGraphAsync: jest.fn().mockImplementation(async () => {
return projectGraph;
}),
updateProjectConfiguration: jest
.fn()
.mockImplementation((tree, projectName, projectConfiguration) => {
function handleEmptyTargets(
projectName: string,
projectConfiguration: ProjectConfiguration
): void {
if (
projectConfiguration.targets &&
!Object.keys(projectConfiguration.targets).length
) {
// Re-order `targets` to appear after the `// target` comment.
delete projectConfiguration.targets;
projectConfiguration[
'// targets'
] = `to see all targets run: nx show project ${projectName} --web`;
projectConfiguration.targets = {};
} else {
delete projectConfiguration['// targets'];
}
}
const projectConfigFile = joinPathFragments(
projectConfiguration.root,
'project.json'
);
if (!tree.exists(projectConfigFile)) {
throw new Error(
`Cannot update Project ${projectName} at ${projectConfiguration.root}. It either doesn't exist yet, or may not use project.json for configuration. Use \`addProjectConfiguration()\` instead if you want to create a new project.`
);
}
handleEmptyTargets(projectName, projectConfiguration);
writeJson(tree, projectConfigFile, {
name: projectConfiguration.name ?? projectName,
$schema: getRelativeProjectJsonSchemaPath(tree, projectConfiguration),
...projectConfiguration,
root: undefined,
});
projectGraph.nodes[projectName].data = projectConfiguration;
}),
}));
function addProjectConfiguration(
tree: Tree,
name: string,
project: ProjectConfiguration
) {
_addProjectConfiguration(tree, name, project);
projectGraph.nodes[name] = {
name: name,
type: project.projectType === 'application' ? 'app' : 'lib',
data: {
projectType: project.projectType,
root: project.root,
targets: project.targets,
},
};
}
interface CreateEslintLintProjectOptions {
appName: string;
appRoot: string;
targetName: string;
legacyExecutor?: boolean;
}
const defaultCreateEslintLintProjectOptions: CreateEslintLintProjectOptions = {
appName: 'myapp',
appRoot: 'myapp',
targetName: 'lint',
legacyExecutor: false,
};
function createTestProject(
tree: Tree,
opts: Partial<CreateEslintLintProjectOptions> = defaultCreateEslintLintProjectOptions
) {
let projectOpts = { ...defaultCreateEslintLintProjectOptions, ...opts };
const project: ProjectConfiguration = {
name: projectOpts.appName,
root: projectOpts.appRoot,
projectType: 'application',
targets: {
[projectOpts.targetName]: {
executor: projectOpts.legacyExecutor
? '@nrwl/linter:eslint'
: '@nx/eslint:lint',
options: {
eslintConfig: `${projectOpts.appRoot}/.eslintrc.json`,
},
},
},
};
const eslintConfigContents = {
rules: {},
overrides: [
{
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
rules: {},
},
{
files: ['./project.json'],
parser: 'jsonc-eslint-parser',
rules: {
'@nx/nx-plugin-checks': 'error',
},
},
{
files: ['./package.json'],
parser: 'jsonc-eslint-parser',
rules: {
'@nx/dependency-checks': [
'error',
{
buildTargets: ['build-base'],
ignoredDependencies: [
'nx',
'@nx/jest',
'typescript',
'eslint',
'@angular-devkit/core',
'@typescript-eslint/eslint-plugin',
],
},
],
},
},
],
ignorePatterns: ['!**/*'],
};
const eslintConfigContentsAsString = JSON.stringify(eslintConfigContents);
tree.write(
`${projectOpts.appRoot}/.eslintrc.json`,
eslintConfigContentsAsString
);
fs.createFileSync(
`${projectOpts.appRoot}/.eslintrc.json`,
eslintConfigContentsAsString
);
tree.write(`${projectOpts.appRoot}/src/foo.ts`, `export const myValue = 2;`);
fs.createFileSync(
`${projectOpts.appRoot}/src/foo.ts`,
`export const myValue = 2;`
);
jest.doMock(
join(fs.tempDir, `${projectOpts.appRoot}/.eslintrc.json`),
() => ({
default: {
extends: '../../.eslintrc',
rules: {},
overrides: [
{
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
rules: {},
},
{
files: ['**/*.ts'],
excludedFiles: ['./src/migrations/**'],
rules: {
'no-restricted-imports': ['error', '@nx/workspace'],
},
},
{
files: [
'./package.json',
'./generators.json',
'./executors.json',
'./migrations.json',
],
parser: 'jsonc-eslint-parser',
rules: {
'@nx/nx-plugin-checks': 'error',
},
},
{
files: ['./package.json'],
parser: 'jsonc-eslint-parser',
rules: {
'@nx/dependency-checks': [
'error',
{
buildTargets: ['build-base'],
ignoredDependencies: [
'nx',
'@nx/jest',
'typescript',
'eslint',
'@angular-devkit/core',
'@typescript-eslint/eslint-plugin',
],
},
],
},
},
],
ignorePatterns: ['!**/*'],
},
}),
{
virtual: true,
}
);
addProjectConfiguration(tree, project.name, project);
fs.createFileSync(
`${projectOpts.appRoot}/project.json`,
JSON.stringify(project)
);
return project;
}
describe('Eslint - Convert Executors To Plugin', () => {
let tree: Tree;
beforeEach(() => {
fs = new TempFs('eslint');
tree = createTreeWithEmptyWorkspace();
tree.root = fs.tempDir;
projectGraph = {
nodes: {},
dependencies: {},
externalNodes: {},
};
});
afterEach(() => {
fs.reset();
});
describe('--project', () => {
it('should setup a new Eslint plugin and only migrate one specific project', async () => {
// ARRANGE
const existingProject = createTestProject(tree, {
appRoot: 'existing',
appName: 'existing',
targetName: 'lint',
});
const project = createTestProject(tree, {
targetName: 'eslint',
});
const secondProject = createTestProject(tree, {
appRoot: 'second',
appName: 'second',
targetName: 'eslint',
});
const thirdProject = createTestProject(tree, {
appRoot: 'third',
appName: 'third',
targetName: 'linter',
});
const nxJson = readNxJson(tree);
nxJson.plugins ??= [];
nxJson.plugins.push({
plugin: '@nx/eslint/plugin',
options: {
targetName: 'lint',
},
});
updateNxJson(tree, nxJson);
// ACT
await convertToInferred(tree, { project: 'myapp', skipFormat: true });
// ASSERT
// project.json modifications
const updatedProject = readProjectConfiguration(tree, project.name);
const targetKeys = Object.keys(updatedProject.targets);
['lint'].forEach((key) => expect(targetKeys).not.toContain(key));
// nx.json modifications
const nxJsonPlugins = readNxJson(tree).plugins;
const addedTestEslintPlugin = nxJsonPlugins.find((plugin) => {
if (
typeof plugin !== 'string' &&
plugin.plugin === '@nx/eslint/plugin' &&
plugin.include?.length === 1
) {
return true;
}
});
expect(addedTestEslintPlugin).toBeTruthy();
expect(
(addedTestEslintPlugin as ExpandedPluginConfiguration).include
).toEqual(['myapp/**/*']);
});
it('should add project to existing plugins includes', async () => {
// ARRANGE
const existingProject = createTestProject(tree, {
appRoot: 'existing',
appName: 'existing',
targetName: 'lint',
});
const project = createTestProject(tree, {
targetName: 'lint',
});
const secondProject = createTestProject(tree, {
appRoot: 'second',
appName: 'second',
targetName: 'lint',
});
const thirdProject = createTestProject(tree, {
appRoot: 'third',
appName: 'third',
targetName: 'lint',
});
const nxJson = readNxJson(tree);
nxJson.plugins ??= [];
nxJson.plugins.push({
plugin: '@nx/eslint/plugin',
include: ['existing/**/*'],
options: {
targetName: 'lint',
},
});
updateNxJson(tree, nxJson);
// ACT
await convertToInferred(tree, { project: 'myapp', skipFormat: true });
// ASSERT
// project.json modifications
const updatedProject = readProjectConfiguration(tree, project.name);
const targetKeys = Object.keys(updatedProject.targets);
expect(targetKeys).not.toContain('lint');
// nx.json modifications
const nxJsonPlugins = readNxJson(tree).plugins;
const addedTestEslintPlugin = nxJsonPlugins.find((plugin) => {
if (
typeof plugin !== 'string' &&
plugin.plugin === '@nx/eslint/plugin' &&
plugin.include?.length === 2
) {
return true;
}
});
expect(addedTestEslintPlugin).toBeTruthy();
expect(
(addedTestEslintPlugin as ExpandedPluginConfiguration).include
).toEqual(['existing/**/*', 'myapp/**/*']);
});
it('should remove include when all projects are included', async () => {
// ARRANGE
const existingProject = createTestProject(tree, {
appRoot: 'existing',
appName: 'existing',
targetName: 'lint',
});
const project = createTestProject(tree, {
targetName: 'lint',
});
const secondProject = createTestProject(tree, {
appRoot: 'second',
appName: 'second',
targetName: 'lint',
});
const thirdProject = createTestProject(tree, {
appRoot: 'third',
appName: 'third',
targetName: 'lint',
});
const nxJson = readNxJson(tree);
nxJson.plugins ??= [];
nxJson.plugins.push({
plugin: '@nx/eslint/plugin',
include: ['existing/**/*', 'second/**/*', 'third/**/*'],
options: {
targetName: 'lint',
},
});
updateNxJson(tree, nxJson);
// ACT
await convertToInferred(tree, { project: 'myapp', skipFormat: true });
// ASSERT
// project.json modifications
const updatedProject = readProjectConfiguration(tree, project.name);
const targetKeys = Object.keys(updatedProject.targets);
['lint'].forEach((key) => expect(targetKeys).not.toContain(key));
// nx.json modifications
const nxJsonPlugins = readNxJson(tree).plugins;
const addedTestEslintPlugin = nxJsonPlugins.find((plugin) => {
if (
typeof plugin !== 'string' &&
plugin.plugin === '@nx/eslint/plugin' &&
!plugin.include
) {
return true;
}
});
expect(addedTestEslintPlugin).toBeTruthy();
expect(
(addedTestEslintPlugin as ExpandedPluginConfiguration).include
).not.toBeDefined();
});
});
describe('--all', () => {
it('should successfully migrate a project using Eslint executors to plugin', async () => {
const project = createTestProject(tree);
// ACT
await convertToInferred(tree, { skipFormat: true });
// ASSERT
// project.json modifications
const updatedProject = readProjectConfiguration(tree, project.name);
const targetKeys = Object.keys(updatedProject.targets);
expect(targetKeys).not.toContain('lint');
// nx.json modifications
const nxJsonPlugins = readNxJson(tree).plugins;
const hasEslintPlugin = nxJsonPlugins.find((plugin) =>
typeof plugin === 'string'
? plugin === '@nx/eslint/plugin'
: plugin.plugin === '@nx/eslint/plugin'
);
expect(hasEslintPlugin).toBeTruthy();
if (typeof hasEslintPlugin !== 'string') {
[['targetName', 'lint']].forEach(([targetOptionName, targetName]) => {
expect(hasEslintPlugin.options[targetOptionName]).toEqual(targetName);
});
}
});
it('should setup Eslint plugin to match projects', async () => {
// ARRANGE
const project = createTestProject(tree, {
targetName: 'eslint',
});
// ACT
await convertToInferred(tree, { skipFormat: true });
// ASSERT
// project.json modifications
const updatedProject = readProjectConfiguration(tree, project.name);
const targetKeys = Object.keys(updatedProject.targets);
['eslint'].forEach((key) => expect(targetKeys).not.toContain(key));
// nx.json modifications
const nxJsonPlugins = readNxJson(tree).plugins;
const hasEslintPlugin = nxJsonPlugins.find((plugin) =>
typeof plugin === 'string'
? plugin === '@nx/eslint/plugin'
: plugin.plugin === '@nx/eslint/plugin'
);
expect(hasEslintPlugin).toBeTruthy();
if (typeof hasEslintPlugin !== 'string') {
[['targetName', 'eslint']].forEach(([targetOptionName, targetName]) => {
expect(hasEslintPlugin.options[targetOptionName]).toEqual(targetName);
});
}
});
it('should handle targets using legacy executor', async () => {
// ARRANGE
const project = createTestProject(tree, {
targetName: 'eslint',
legacyExecutor: true,
});
// ACT
await convertToInferred(tree, { skipFormat: true });
// ASSERT
// project.json modifications
const updatedProject = readProjectConfiguration(tree, project.name);
const targetKeys = Object.keys(updatedProject.targets);
expect(targetKeys).not.toContain('eslint');
// nx.json modifications
const nxJsonPlugins = readNxJson(tree).plugins;
const hasEslintPlugin = nxJsonPlugins.find((plugin) =>
typeof plugin === 'string'
? plugin === '@nx/eslint/plugin'
: plugin.plugin === '@nx/eslint/plugin'
);
expect(hasEslintPlugin).toBeTruthy();
if (typeof hasEslintPlugin !== 'string') {
[['targetName', 'eslint']].forEach(([targetOptionName, targetName]) => {
expect(hasEslintPlugin.options[targetOptionName]).toEqual(targetName);
});
}
});
it('should setup a new Eslint plugin to match only projects migrated', async () => {
// ARRANGE
const existingProject = createTestProject(tree, {
appRoot: 'existing',
appName: 'existing',
targetName: 'lint',
});
const project = createTestProject(tree, {
targetName: 'eslint',
});
const secondProject = createTestProject(tree, {
appRoot: 'second',
appName: 'second',
targetName: 'eslint',
});
const thirdProject = createTestProject(tree, {
appRoot: 'third',
appName: 'third',
targetName: 'linter',
});
const nxJson = readNxJson(tree);
nxJson.plugins ??= [];
nxJson.plugins.push({
plugin: '@nx/eslint/plugin',
options: {
targetName: 'lint',
},
});
updateNxJson(tree, nxJson);
// ACT
await convertToInferred(tree, { skipFormat: true });
// ASSERT
// project.json modifications
const updatedProject = readProjectConfiguration(tree, project.name);
const targetKeys = Object.keys(updatedProject.targets);
expect(targetKeys).not.toContain('eslint');
// nx.json modifications
const nxJsonPlugins = readNxJson(tree).plugins;
const addedLintEslintPlugin = nxJsonPlugins.find((plugin) => {
if (
typeof plugin !== 'string' &&
plugin.plugin === '@nx/eslint/plugin' &&
plugin.include?.length === 2
) {
return true;
}
});
expect(addedLintEslintPlugin).toBeTruthy();
expect(
(addedLintEslintPlugin as ExpandedPluginConfiguration).include
).toEqual(['myapp/**/*', 'second/**/*']);
const addedLinterEslintPlugin = nxJsonPlugins.find((plugin) => {
if (
typeof plugin !== 'string' &&
plugin.plugin === '@nx/eslint/plugin' &&
plugin.include?.length === 1
) {
return true;
}
});
expect(addedLinterEslintPlugin).toBeTruthy();
expect(
(addedLinterEslintPlugin as ExpandedPluginConfiguration).include
).toEqual(['third/**/*']);
});
it('should keep Eslint options in project.json', async () => {
// ARRANGE
const project = createTestProject(tree);
project.targets.lint.options.cacheLocation = 'cache-dir';
updateProjectConfiguration(tree, project.name, project);
// ACT
await convertToInferred(tree, { skipFormat: true });
// ASSERT
// project.json modifications
const updatedProject = readProjectConfiguration(tree, project.name);
expect(updatedProject.targets.lint).toMatchInlineSnapshot(`
{
"options": {
"cache-location": "cache-dir",
},
}
`);
// nx.json modifications
const nxJsonPlugins = readNxJson(tree).plugins;
const hasEslintPlugin = nxJsonPlugins.find((plugin) =>
typeof plugin === 'string'
? plugin === '@nx/eslint/plugin'
: plugin.plugin === '@nx/eslint/plugin'
);
expect(hasEslintPlugin).toBeTruthy();
if (typeof hasEslintPlugin !== 'string') {
[['targetName', 'lint']].forEach(([targetOptionName, targetName]) => {
expect(hasEslintPlugin.options[targetOptionName]).toEqual(targetName);
});
}
});
it('should add Eslint options found in targetDefaults for the executor to the project.json', async () => {
// ARRANGE
const nxJson = readNxJson(tree);
nxJson.targetDefaults ??= {};
nxJson.targetDefaults['@nx/eslint:lint'] = {
options: {
maxWarnings: 10,
},
};
updateNxJson(tree, nxJson);
const project = createTestProject(tree);
// ACT
await convertToInferred(tree, { skipFormat: true });
// ASSERT
// project.json modifications
const updatedProject = readProjectConfiguration(tree, project.name);
expect(updatedProject.targets.lint).toMatchInlineSnapshot(`
{
"options": {
"max-warnings": 10,
},
}
`);
// nx.json modifications
const nxJsonPlugins = readNxJson(tree).plugins;
const hasEslintPlugin = nxJsonPlugins.find((plugin) =>
typeof plugin === 'string'
? plugin === '@nx/eslint/plugin'
: plugin.plugin === '@nx/eslint/plugin'
);
expect(hasEslintPlugin).toBeTruthy();
if (typeof hasEslintPlugin !== 'string') {
[['targetName', 'lint']].forEach(([targetOptionName, targetName]) => {
expect(hasEslintPlugin.options[targetOptionName]).toEqual(targetName);
});
}
});
});
});

View File

@ -0,0 +1,137 @@
import {
createProjectGraphAsync,
formatFiles,
names,
type TargetConfiguration,
type Tree,
} from '@nx/devkit';
import { createNodes, EslintPluginOptions } from '../../plugins/plugin';
import { migrateExecutorToPlugin } from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator';
import { targetOptionsToCliMap } from './lib/target-options-map';
import { interpolate } from 'nx/src/tasks-runner/utils';
interface Schema {
project?: string;
skipFormat?: boolean;
}
export async function convertToInferred(tree: Tree, options: Schema) {
const projectGraph = await createProjectGraphAsync();
const migratedProjectsModern =
await migrateExecutorToPlugin<EslintPluginOptions>(
tree,
projectGraph,
'@nx/eslint:lint',
'@nx/eslint/plugin',
(targetName) => ({ targetName }),
postTargetTransformer,
createNodes,
options.project
);
const migratedProjectsLegacy =
await migrateExecutorToPlugin<EslintPluginOptions>(
tree,
projectGraph,
'@nrwl/linter:eslint',
'@nx/eslint/plugin',
(targetName) => ({ targetName }),
postTargetTransformer,
createNodes,
options.project
);
const migratedProjects =
migratedProjectsModern.size + migratedProjectsLegacy.size;
if (migratedProjects === 0) {
throw new Error('Could not find any targets to migrate.');
}
if (!options.skipFormat) {
await formatFiles(tree);
}
}
function postTargetTransformer(
target: TargetConfiguration,
tree: Tree,
projectDetails: { projectName: string; root: string }
): TargetConfiguration {
if (target.inputs) {
target.inputs = target.inputs.filter(
(input) =>
typeof input === 'string' &&
![
'default',
'{workspaceRoot}/.eslintrc.json',
'{workspaceRoot}/.eslintignore',
'{workspaceRoot}/eslint.config.js',
].includes(input)
);
if (target.inputs.length === 0) {
delete target.inputs;
}
}
if (target.options) {
if ('eslintConfig' in target.options) {
delete target.options.eslintConfig;
}
if ('force' in target.options) {
delete target.options.force;
}
if ('silent' in target.options) {
delete target.options.silent;
}
if ('hasTypeAwareRules' in target.options) {
delete target.options.hasTypeAwareRules;
}
if ('errorOnUnmatchedPattern' in target.options) {
if (!target.options.errorOnUnmatchedPattern) {
target.options['no-error-on-unmatched-pattern'] = true;
}
delete target.options.errorOnUnmatchedPattern;
}
if ('outputFile' in target.options) {
target.outputs ??= [];
target.outputs.push(target.options.outputFile);
}
for (const key in targetOptionsToCliMap) {
if (target.options[key]) {
target.options[targetOptionsToCliMap[key]] = target.options[key];
delete target.options[key];
}
}
if ('lintFilePatterns' in target.options) {
const normalizedLintFilePatterns = target.options.lintFilePatterns.map(
(pattern) => {
return interpolate(pattern, {
workspaceRoot: '',
projectRoot: projectDetails.root,
projectName: projectDetails.projectName,
});
}
);
target.options.args = normalizedLintFilePatterns.map((pattern) =>
pattern.startsWith(projectDetails.root)
? pattern.replace(new RegExp(`^${projectDetails.root}/`), './')
: pattern
);
delete target.options.lintFilePatterns;
}
}
return target;
}
export default convertToInferred;

View File

@ -0,0 +1,16 @@
export const targetOptionsToCliMap = {
fix: 'fix',
format: 'format',
cache: 'cache',
cacheLocation: 'cache-location',
cacheStrategy: 'cache-strategy',
noEslintrc: 'no-eslintrc',
outputFile: 'output-file',
maxWarnings: 'max-warnings',
quiet: 'quiet',
ignorePath: 'ignore-path',
rulesdir: 'rulesdir',
resolvePluginsRelativeTo: 'resolve-plugins-relative-to',
reportUnusedDisableDirectives: 'report-unused-disable-directives',
printConfig: 'print-config',
};

View File

@ -0,0 +1,19 @@
{
"$schema": "https://json-schema.org/schema",
"$id": "NxEslintConvertToInferred",
"description": "Convert existing Eslint project(s) using `@nx/eslint:lint` executor to use `@nx/eslint/plugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.",
"title": "Convert Eslint project from executor to plugin",
"type": "object",
"properties": {
"project": {
"type": "string",
"description": "The project to convert from using the `@nx/eslint:lint` executor to use `@nx/eslint/plugin`.",
"x-priority": "important"
},
"skipFormat": {
"type": "boolean",
"description": "Whether to format files at the end of the migration.",
"default": false
}
}
}

View File

@ -28,7 +28,7 @@ export async function convertToInferred(tree: Tree, options: Schema) {
options.project
);
if (migratedProjects === 0) {
if (migratedProjects.size === 0) {
throw new Error('Could not find any targets to migrate.');
}