Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
ProjectGraph,
readJson,
Tree,
updateNxJson,
} from '@nx/devkit';

import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
Expand Down Expand Up @@ -88,4 +89,115 @@ describe('@nx/eslint:lint-project (convert to monorepo style)', () => {
},
});
});

// Regression: the root lint target was just written to the tree earlier in
// the same generator run. The project graph is stale and would not see it,
// so relying on the graph alone deferred the split to a second invocation.
it('should split the root eslint config when the root lint target is only on the tree', async () => {
projectGraph = { nodes: {}, dependencies: {} };

addProjectConfiguration(tree, 'nestedpkg', {
root: 'nestedpkg',
projectType: 'library',
targets: {},
});

await lintProjectGenerator(tree, {
...defaultOptions,
linter: 'eslint',
project: 'nestedpkg',
setParserOptionsProject: false,
});

expect(tree.exists('.eslintrc.base.json')).toBe(true);
});

// Regression for #23147: root lint target is only inferred by
// `@nx/eslint/plugin`, so it is not on the tree. The graph surfaces it and
// migration should still fire.
it('should split the root eslint config for plugin-inferred root lint targets', async () => {
tree = createTreeWithEmptyWorkspace();
updateNxJson(tree, { plugins: ['@nx/eslint/plugin'] });
tree.write('.eslintrc.cjs', 'module.exports = {};');
addProjectConfiguration(tree, 'rootpkg', {
root: '.',
projectType: 'library',
targets: {},
});
addProjectConfiguration(tree, 'nestedpkg', {
root: 'nestedpkg',
projectType: 'library',
targets: {},
});
projectGraph = {
nodes: {
rootpkg: {
type: 'lib',
name: 'rootpkg',
data: {
root: '.',
targets: {
lint: {
executor: 'nx:run-commands',
options: { command: 'eslint .' },
},
},
},
},
},
dependencies: {},
};

await lintProjectGenerator(tree, {
...defaultOptions,
linter: 'eslint',
project: 'nestedpkg',
setParserOptionsProject: false,
});

expect(tree.exists('.eslintrc.base.json')).toBe(true);
});

it('should not split the root eslint config when no root lint target exists', async () => {
tree = createTreeWithEmptyWorkspace();
tree.write('.eslintrc.cjs', 'module.exports = {};');
addProjectConfiguration(tree, 'rootpkg', {
root: '.',
projectType: 'library',
targets: {},
});
addProjectConfiguration(tree, 'nestedpkg', {
root: 'nestedpkg',
projectType: 'library',
targets: {},
});
projectGraph = { nodes: {}, dependencies: {} };

await lintProjectGenerator(tree, {
...defaultOptions,
linter: 'eslint',
project: 'nestedpkg',
setParserOptionsProject: false,
});

expect(tree.exists('.eslintrc.base.json')).toBe(false);
});

it('should not split the root eslint config when the base config already exists', async () => {
tree.write('.eslintrc.base.json', '{}');
addProjectConfiguration(tree, 'nestedpkg', {
root: 'nestedpkg',
projectType: 'library',
targets: {},
});

await lintProjectGenerator(tree, {
...defaultOptions,
linter: 'eslint',
project: 'nestedpkg',
setParserOptionsProject: false,
});

expect(readJson(tree, '.eslintrc.base.json')).toEqual({});
});
});
54 changes: 39 additions & 15 deletions packages/eslint/src/generators/lint-project/lint-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
NxJsonConfiguration,
offsetFromRoot,
ProjectConfiguration,
ProjectGraph,
readJson,
readNxJson,
readProjectConfiguration,
Expand Down Expand Up @@ -141,13 +140,11 @@ export async function lintProjectGeneratorInternal(
// companion e2e app so we should check if migration to
// monorepo style is needed
if (!options.rootProject) {
const projects = {} as any;
getProjects(tree).forEach((v, k) => (projects[k] = v));
const graph = await createProjectGraphAsync();
if (isMigrationToMonorepoNeeded(tree, graph)) {
const projectsFromTree = getProjects(tree);
if (await isMigrationToMonorepoNeeded(tree, projectsFromTree)) {
// we only migrate project configurations that have been created
const filteredProjects = [];
Object.entries(projects).forEach(([name, project]) => {
const filteredProjects: ProjectConfiguration[] = [];
projectsFromTree.forEach((project, name) => {
if (name !== options.project) {
filteredProjects.push(project);
}
Expand Down Expand Up @@ -372,9 +369,17 @@ function isBuildableLibraryProject(

/**
* Detect based on the state of lint target configuration of the root project
* if we should migrate eslint configs to monorepo style
* if we should migrate eslint configs to monorepo style.
*
* Checks the tree first so explicit root lint targets written earlier in the
* same generator run are visible; the project graph is only consulted to
* surface targets inferred by `@nx/eslint/plugin`, which don't appear on the
* tree.
*/
function isMigrationToMonorepoNeeded(tree: Tree, graph: ProjectGraph): boolean {
async function isMigrationToMonorepoNeeded(
tree: Tree,
projectsFromTree: Map<string, ProjectConfiguration>
): Promise<boolean> {
// the base config is already created, migration has been done
if (
[baseEsLintConfigFile, ...BASE_ESLINT_CONFIG_FILENAMES].some((f) =>
Expand All @@ -384,15 +389,35 @@ function isMigrationToMonorepoNeeded(tree: Tree, graph: ProjectGraph): boolean {
return false;
}

const nodes = Object.values(graph.nodes);
for (const project of projectsFromTree.values()) {
if (project.root === '.') {
if (rootHasEslintLintTarget(project.targets)) {
return true;
}
break;
}
}

// get root project
const rootProject = nodes.find((p) => p.data.root === '.');
if (!rootProject || !rootProject.data.targets) {
if (!hasEslintPlugin(tree)) {
return false;
}

for (const targetConfig of Object.values(rootProject.data.targets ?? {})) {
const graph = await createProjectGraphAsync();
for (const node of Object.values(graph.nodes)) {
if (node.data.root === '.') {
return rootHasEslintLintTarget(node.data.targets);
}
}
return false;
}

function rootHasEslintLintTarget(
targets: ProjectConfiguration['targets'] | undefined
): boolean {
if (!targets) {
return false;
}
for (const targetConfig of Object.values(targets)) {
if (
['@nx/eslint:lint', '@nx/linter:eslint'].includes(
targetConfig.executor
Expand All @@ -404,6 +429,5 @@ function isMigrationToMonorepoNeeded(tree: Tree, graph: ProjectGraph): boolean {
return true;
}
}

return false;
}
Loading