Skip to content

Commit

Permalink
feat(core): allow circular project dependencies to execute tasks
Browse files Browse the repository at this point in the history
If not all circular dependent projects contain the same task target, allow execution of the target.
  • Loading branch information
Cammisuli committed Oct 1, 2024
1 parent 2f09285 commit 557d966
Show file tree
Hide file tree
Showing 2 changed files with 287 additions and 8 deletions.
252 changes: 245 additions & 7 deletions packages/nx/src/tasks-runner/create-task-graph.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -1294,7 +1532,7 @@ describe('createTaskGraph', () => {
}
);
expect(taskGraph).toEqual({
roots: [],
roots: ['app1:compile', 'app3:compile'],
tasks: {
'app1:compile': {
id: 'app1:compile',
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -1386,7 +1624,7 @@ describe('createTaskGraph', () => {
}
);
expect(taskGraph).toEqual({
roots: ['app3:compile'],
roots: ['app1:compile', 'app3:compile'],
tasks: {
'app1:compile': {
id: 'app1:compile',
Expand Down Expand Up @@ -1416,7 +1654,7 @@ describe('createTaskGraph', () => {
},
},
dependencies: {
'app1:compile': ['app3:compile'],
'app1:compile': [],
'app3:compile': [],
},
});
Expand Down
43 changes: 42 additions & 1 deletion packages/nx/src/tasks-runner/create-task-graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>();
readonly tasks: { [id: string]: Task } = {};
Expand Down Expand Up @@ -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] = [
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -366,6 +406,7 @@ export function createTaskGraph(
overrides,
excludeTaskDependencies
);

return {
roots,
tasks: p.tasks,
Expand Down

0 comments on commit 557d966

Please sign in to comment.