feat(linter): add convert-to-inferred migration generator (#23142)
This commit is contained in:
parent
1d2c843c26
commit
acd0993f1a
@ -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,
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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"
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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.');
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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`."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
@ -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',
|
||||
};
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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.');
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user