feat(core): allow circular project dependencies to execute tasks (#28227)
<!-- Please make sure you have read the submission guidelines before posting an PR --> <!-- https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr --> <!-- Please make sure that your commit message follows our format --> <!-- Example: `fix(nx): must begin with lowercase` --> <!-- If this is a particularly complex change or feature addition, you can request a dedicated Nx release for this pull request branch. Mention someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they will confirm if the PR warrants its own release for testing purposes, and generate it for you if appropriate. --> ## Current Behavior <!-- This is the behavior we have today --> If there are project dependencies and not all projects contain the same task target, a circular dependency error is shown. ## Expected Behavior <!-- This is the behavior we should expect with the changes in this PR --> If not all circular dependent projects contain the same task target, allow execution of the target. ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes #
This commit is contained in:
parent
ec801b4c16
commit
3c791c7133
@ -1235,7 +1235,482 @@ 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: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: {
|
||||
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' }],
|
||||
},
|
||||
};
|
||||
|
||||
const taskGraph = createTaskGraph(
|
||||
projectGraph,
|
||||
{
|
||||
build: [{ target: 'build', dependencies: true }],
|
||||
},
|
||||
['lib1'],
|
||||
['build'],
|
||||
'development',
|
||||
{
|
||||
__overrides_unparsed__: [],
|
||||
}
|
||||
);
|
||||
expect(taskGraph).toEqual({
|
||||
roots: ['lib4: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,
|
||||
}),
|
||||
'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': ['lib4:build'],
|
||||
'lib4:build': [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle cycles between projects where all projects do not contain the same task target (lib1:build -> lib2:build -> lib3 -> 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: {},
|
||||
},
|
||||
},
|
||||
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,
|
||||
}),
|
||||
'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': ['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 +1769,7 @@ describe('createTaskGraph', () => {
|
||||
}
|
||||
);
|
||||
expect(taskGraph).toEqual({
|
||||
roots: [],
|
||||
roots: ['app1:compile', 'app3:compile'],
|
||||
tasks: {
|
||||
'app1:compile': {
|
||||
id: 'app1:compile',
|
||||
@ -1324,13 +1799,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 +1861,7 @@ describe('createTaskGraph', () => {
|
||||
}
|
||||
);
|
||||
expect(taskGraph).toEqual({
|
||||
roots: ['app3:compile'],
|
||||
roots: ['app1:compile', 'app3:compile'],
|
||||
tasks: {
|
||||
'app1:compile': {
|
||||
id: 'app1:compile',
|
||||
@ -1416,7 +1891,7 @@ describe('createTaskGraph', () => {
|
||||
},
|
||||
},
|
||||
dependencies: {
|
||||
'app1:compile': ['app3:compile'],
|
||||
'app1:compile': [],
|
||||
'app3:compile': [],
|
||||
},
|
||||
});
|
||||
|
||||
@ -9,6 +9,8 @@ import { TargetDefaults, TargetDependencies } from '../config/nx-json';
|
||||
import { TargetDependencyConfig } from '../devkit-exports';
|
||||
import { output } from '../utils/output';
|
||||
|
||||
const DUMMY_TASK_TARGET = '__nx_dummy_task__';
|
||||
|
||||
export class ProcessTasks {
|
||||
private readonly seen = new Set<string>();
|
||||
readonly tasks: { [id: string]: Task } = {};
|
||||
@ -81,6 +83,8 @@ export class ProcessTasks {
|
||||
}
|
||||
}
|
||||
|
||||
this.filterDummyTasks();
|
||||
|
||||
for (const projectName of Object.keys(this.dependencies)) {
|
||||
if (this.dependencies[projectName].length > 1) {
|
||||
this.dependencies[projectName] = [
|
||||
@ -228,6 +232,14 @@ export class ProcessTasks {
|
||||
taskOverrides: Object | { __overrides_unparsed__: any[] },
|
||||
overrides: Object
|
||||
) {
|
||||
if (
|
||||
!this.projectGraph.dependencies.hasOwnProperty(
|
||||
projectUsedToDeriveDependencies
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const dep of this.projectGraph.dependencies[
|
||||
projectUsedToDeriveDependencies
|
||||
]) {
|
||||
@ -272,11 +284,26 @@ export class ProcessTasks {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.processTask(task, depProject.name, configuration, overrides);
|
||||
const dummyId = this.getId(
|
||||
depProject.name,
|
||||
DUMMY_TASK_TARGET,
|
||||
undefined
|
||||
);
|
||||
this.dependencies[task.id].push(dummyId);
|
||||
this.dependencies[dummyId] = [];
|
||||
const noopTask = this.createDummyTask(dummyId, task);
|
||||
this.processTask(noopTask, depProject.name, configuration, overrides);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private createDummyTask(id: string, task: Task): Task {
|
||||
return {
|
||||
...task,
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
||||
createTask(
|
||||
id: string,
|
||||
project: ProjectGraphProjectNode,
|
||||
@ -347,6 +374,31 @@ export class ProcessTasks {
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
private filterDummyTasks() {
|
||||
for (const [key, deps] of Object.entries(this.dependencies)) {
|
||||
const normalizedDeps = [];
|
||||
for (const dep of deps) {
|
||||
if (dep.endsWith(DUMMY_TASK_TARGET)) {
|
||||
normalizedDeps.push(
|
||||
...this.dependencies[dep].filter(
|
||||
(d) => !d.endsWith(DUMMY_TASK_TARGET)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
normalizedDeps.push(dep);
|
||||
}
|
||||
}
|
||||
|
||||
this.dependencies[key] = normalizedDeps;
|
||||
}
|
||||
|
||||
for (const key of Object.keys(this.dependencies)) {
|
||||
if (key.endsWith(DUMMY_TASK_TARGET)) {
|
||||
delete this.dependencies[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createTaskGraph(
|
||||
@ -366,6 +418,7 @@ export function createTaskGraph(
|
||||
overrides,
|
||||
excludeTaskDependencies
|
||||
);
|
||||
|
||||
return {
|
||||
roots,
|
||||
tasks: p.tasks,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user