From 557d9664ac473dfb8eea92c917874405f5c9a83d Mon Sep 17 00:00:00 2001 From: Jonathan Cammisuli Date: Tue, 1 Oct 2024 14:51:45 -0400 Subject: [PATCH] feat(core): allow circular project dependencies to execute tasks If not all circular dependent projects contain the same task target, allow execution of the target. --- .../tasks-runner/create-task-graph.spec.ts | 252 +++++++++++++++++- .../nx/src/tasks-runner/create-task-graph.ts | 43 ++- 2 files changed, 287 insertions(+), 8 deletions(-) diff --git a/packages/nx/src/tasks-runner/create-task-graph.spec.ts b/packages/nx/src/tasks-runner/create-task-graph.spec.ts index 6b26c3a93d9645..1f482dc6bf7302 100644 --- a/packages/nx/src/tasks-runner/create-task-graph.spec.ts +++ b/packages/nx/src/tasks-runner/create-task-graph.spec.ts @@ -1235,7 +1235,245 @@ describe('createTaskGraph', () => { }); }); - it('should handle cycles between projects (app1:build <-> app2 <-> app3:build)', () => { + it('should handle cycles between projects where all projects contain the same task target (lib1:build -> lib2:build -> lib3:build -> lib4:build -> lib1:build)', () => { + projectGraph = { + nodes: { + lib1: { + name: 'lib1', + type: 'lib', + data: { + root: 'lib1-root', + targets: { + build: { + executor: 'nx:run-commands', + }, + }, + }, + }, + lib2: { + name: 'lib2', + type: 'lib', + data: { + root: 'lib2-root', + targets: { + build: { + executor: 'nx:run-commands', + }, + }, + }, + }, + lib3: { + name: 'lib3', + type: 'lib', + data: { + root: 'lib3-root', + targets: { + build: { + executor: 'nx:run-commands', + }, + }, + }, + }, + lib4: { + name: 'lib4', + type: 'lib', + data: { + root: 'lib4-root', + targets: { + build: { + executor: 'nx:run-commands', + }, + }, + }, + }, + }, + dependencies: { + lib1: [{ source: 'lib1', target: 'lib2', type: 'static' }], + lib2: [{ source: 'lib2', target: 'lib3', type: 'static' }], + lib3: [{ source: 'lib3', target: 'lib4', type: 'static' }], + lib4: [{ source: 'lib4', target: 'lib1', type: 'static' }], + }, + }; + + const taskGraph = createTaskGraph( + projectGraph, + { + build: [{ target: 'build', dependencies: true }], + }, + ['lib1'], + ['build'], + 'development', + { + __overrides_unparsed__: [], + } + ); + expect(taskGraph).toEqual({ + roots: [], + tasks: { + 'lib1:build': expect.objectContaining({ + id: 'lib1:build', + target: { + project: 'lib1', + target: 'build', + }, + outputs: expect.arrayContaining([expect.any(String)]), + overrides: { + __overrides_unparsed__: [], + }, + projectRoot: 'lib1-root', + parallelism: true, + }), + 'lib2:build': expect.objectContaining({ + id: 'lib2:build', + target: { + project: 'lib2', + target: 'build', + }, + outputs: expect.arrayContaining([expect.any(String)]), + overrides: { + __overrides_unparsed__: [], + }, + projectRoot: 'lib2-root', + parallelism: true, + }), + 'lib3:build': expect.objectContaining({ + id: 'lib3:build', + target: { + project: 'lib3', + target: 'build', + }, + outputs: expect.arrayContaining([expect.any(String)]), + overrides: { + __overrides_unparsed__: [], + }, + projectRoot: 'lib3-root', + parallelism: true, + }), + 'lib4:build': expect.objectContaining({ + id: 'lib4:build', + target: { + project: 'lib4', + target: 'build', + }, + outputs: expect.arrayContaining([expect.any(String)]), + overrides: { + __overrides_unparsed__: [], + }, + projectRoot: 'lib4-root', + parallelism: true, + }), + }, + dependencies: { + 'lib1:build': ['lib2:build'], + 'lib2:build': ['lib3:build'], + 'lib3:build': ['lib4:build'], + 'lib4:build': ['lib1:build'], + }, + }); + }); + + it('should handle cycles between projects where all projects do not contain the same task target (lib1:build -> lib2:build -> lib3 -> lib4 -> lib1:build)', () => { + projectGraph = { + nodes: { + lib1: { + name: 'lib1', + type: 'lib', + data: { + root: 'lib1-root', + targets: { + build: { + executor: 'nx:run-commands', + }, + }, + }, + }, + lib2: { + name: 'lib2', + type: 'lib', + data: { + root: 'lib2-root', + targets: { + build: { + executor: 'nx:run-commands', + }, + }, + }, + }, + lib3: { + name: 'lib3', + type: 'lib', + data: { + root: 'lib3-root', + targets: {}, + }, + }, + lib4: { + name: 'lib4', + type: 'lib', + data: { + root: 'lib4-root', + targets: {}, + }, + }, + }, + dependencies: { + lib1: [{ source: 'lib1', target: 'lib2', type: 'static' }], + lib2: [{ source: 'lib2', target: 'lib3', type: 'static' }], + lib3: [{ source: 'lib3', target: 'lib4', type: 'static' }], + lib4: [{ source: 'lib4', target: 'lib1', type: 'static' }], + }, + }; + + const taskGraph = createTaskGraph( + projectGraph, + { + build: [{ target: 'build', dependencies: true }], + }, + ['lib1'], + ['build'], + 'development', + { + __overrides_unparsed__: [], + } + ); + expect(taskGraph).toEqual({ + roots: ['lib2:build'], + tasks: { + 'lib1:build': expect.objectContaining({ + id: 'lib1:build', + target: { + project: 'lib1', + target: 'build', + }, + outputs: expect.arrayContaining([expect.any(String)]), + overrides: { + __overrides_unparsed__: [], + }, + projectRoot: 'lib1-root', + parallelism: true, + }), + 'lib2:build': expect.objectContaining({ + id: 'lib2:build', + target: { + project: 'lib2', + target: 'build', + }, + outputs: expect.arrayContaining([expect.any(String)]), + overrides: { + __overrides_unparsed__: [], + }, + projectRoot: 'lib2-root', + parallelism: true, + }), + }, + dependencies: { + 'lib1:build': ['lib2:build'], + 'lib2:build': [], + }, + }); + }); + + it('should handle cycles between projects where all projects do not contain the same task target (app1:build <-> app2 <-> app3:build)', () => { projectGraph = { nodes: { app1: { @@ -1294,7 +1532,7 @@ describe('createTaskGraph', () => { } ); expect(taskGraph).toEqual({ - roots: [], + roots: ['app1:compile', 'app3:compile'], tasks: { 'app1:compile': { id: 'app1:compile', @@ -1324,13 +1562,13 @@ describe('createTaskGraph', () => { }, }, dependencies: { - 'app1:compile': ['app3:compile'], - 'app3:compile': ['app1:compile'], + 'app1:compile': [], + 'app3:compile': [], }, }); }); - it('should handle cycles between projects that do not create cycles between tasks (app1:build -> app2 <-> app3:build)``', () => { + it('should handle cycles between projects that do not create cycles between tasks and not contain the same task target (app1:build -> app2 <-> app3:build)``', () => { projectGraph = { nodes: { app1: { @@ -1386,7 +1624,7 @@ describe('createTaskGraph', () => { } ); expect(taskGraph).toEqual({ - roots: ['app3:compile'], + roots: ['app1:compile', 'app3:compile'], tasks: { 'app1:compile': { id: 'app1:compile', @@ -1416,7 +1654,7 @@ describe('createTaskGraph', () => { }, }, dependencies: { - 'app1:compile': ['app3:compile'], + 'app1:compile': [], 'app3:compile': [], }, }); diff --git a/packages/nx/src/tasks-runner/create-task-graph.ts b/packages/nx/src/tasks-runner/create-task-graph.ts index 909fc411beba16..85d84ccb227d18 100644 --- a/packages/nx/src/tasks-runner/create-task-graph.ts +++ b/packages/nx/src/tasks-runner/create-task-graph.ts @@ -9,6 +9,8 @@ import { TargetDefaults, TargetDependencies } from '../config/nx-json'; import { TargetDependencyConfig } from '../devkit-exports'; import { output } from '../utils/output'; +const NOOP_TASK_TARGET = '__nx__noop'; + export class ProcessTasks { private readonly seen = new Set(); readonly tasks: { [id: string]: Task } = {}; @@ -81,6 +83,8 @@ export class ProcessTasks { } } + this.filterNoopTasks(); + for (const projectName of Object.keys(this.dependencies)) { if (this.dependencies[projectName].length > 1) { this.dependencies[projectName] = [ @@ -272,11 +276,22 @@ export class ProcessTasks { ); } } else { - this.processTask(task, depProject.name, configuration, overrides); + const noopId = this.getId(depProject.name, NOOP_TASK_TARGET, undefined); + this.dependencies[task.id].push(noopId); + this.dependencies[noopId] = []; + const noopTask = this.createNoopTask(noopId, task); + this.processTask(noopTask, depProject.name, configuration, overrides); } } } + private createNoopTask(id: string, task: Task): Task { + return { + ...task, + id, + }; + } + createTask( id: string, project: ProjectGraphProjectNode, @@ -347,6 +362,31 @@ export class ProcessTasks { } return id; } + + private filterNoopTasks() { + for (const [key, deps] of Object.entries(this.dependencies)) { + const normalizedDeps = []; + for (const dep of deps) { + if (dep.endsWith(NOOP_TASK_TARGET)) { + normalizedDeps.push( + ...this.dependencies[dep].filter( + (d) => !d.endsWith(NOOP_TASK_TARGET) + ) + ); + } else { + normalizedDeps.push(dep); + } + } + + this.dependencies[key] = normalizedDeps; + } + + for (const key of Object.keys(this.dependencies)) { + if (key.endsWith(NOOP_TASK_TARGET)) { + delete this.dependencies[key]; + } + } + } } export function createTaskGraph( @@ -366,6 +406,7 @@ export function createTaskGraph( overrides, excludeTaskDependencies ); + return { roots, tasks: p.tasks,