fix(core): include dependencies of projects which do not have a target (#6309)

This commit is contained in:
Jason Jean 2021-07-09 16:30:15 -04:00 committed by GitHub
parent d35d03e55e
commit 033579712f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 597 additions and 36 deletions

View File

@ -1,5 +1,113 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TaskGraphCreator (tasks with dependency configurations) should create a task graph (builds depend on builds of dependencies even with intermediate projects) 1`] = `
Object {
"dependencies": Object {
"app1:build": Array [
"common2:build",
],
"common2:build": Array [],
},
"roots": Array [
"common2:build",
],
"tasks": Object {
"app1:build": Object {
"id": "app1:build",
"overrides": Object {},
"projectRoot": "app1-root",
"target": Object {
"configuration": undefined,
"project": "app1",
"target": "build",
},
},
"common2:build": Object {
"id": "common2:build",
"overrides": Object {},
"projectRoot": "common2-root",
"target": Object {
"configuration": undefined,
"project": "common2",
"target": "build",
},
},
},
}
`;
exports[`TaskGraphCreator (tasks with dependency configurations) should create a task graph (builds depend on builds of dependencies with intermediate projects and circular dependencies between projects) 1`] = `
Object {
"dependencies": Object {
"app1:build": Array [
"common2:build",
],
"common2:build": Array [],
},
"roots": Array [
"common2:build",
],
"tasks": Object {
"app1:build": Object {
"id": "app1:build",
"overrides": Object {},
"projectRoot": "app1-root",
"target": Object {
"configuration": undefined,
"project": "app1",
"target": "build",
},
},
"common2:build": Object {
"id": "common2:build",
"overrides": Object {},
"projectRoot": "common2-root",
"target": Object {
"configuration": undefined,
"project": "common2",
"target": "build",
},
},
},
}
`;
exports[`TaskGraphCreator (tasks with dependency configurations) should create a task graph (builds depend on builds of dependencies with intermediate projects and circular dependencies between projects) 2 1`] = `
Object {
"dependencies": Object {
"app1:build": Array [
"common3:build",
],
"common3:build": Array [],
},
"roots": Array [
"common3:build",
],
"tasks": Object {
"app1:build": Object {
"id": "app1:build",
"overrides": Object {},
"projectRoot": "app1-root",
"target": Object {
"configuration": undefined,
"project": "app1",
"target": "build",
},
},
"common3:build": Object {
"id": "common3:build",
"overrides": Object {},
"projectRoot": "common3",
"target": Object {
"configuration": undefined,
"project": "common3",
"target": "build",
},
},
},
}
`;
exports[`TaskGraphCreator (tasks with dependency configurations) should create task graph (builds depend on build of dependencies and prebuild of self) 1`] = `
Object {
"dependencies": Object {

View File

@ -1,8 +1,8 @@
import { TasksRunner } from './tasks-runner';
import defaultTaskRunner from './default-tasks-runner';
import { createTasksForProjectToRun, getRunner } from './run-command';
import type { NxJsonConfiguration, ProjectGraph } from '@nrwl/devkit';
import { DependencyType } from '@nrwl/devkit';
import type { ProjectGraph, NxJsonConfiguration } from '@nrwl/devkit';
describe('createTasksForProjectToRun', () => {
let projectGraph: ProjectGraph;
@ -331,6 +331,279 @@ describe('createTasksForProjectToRun', () => {
]);
});
it('should create tasks for multiple sets of dependencies for multiple targets', () => {
projectGraph.nodes.app1.data.targets.build.dependsOn = [
{
target: 'prebuild',
projects: 'dependencies',
},
{
target: 'build',
projects: 'dependencies',
},
];
projectGraph.dependencies.app1.push({
type: DependencyType.static,
source: 'app1',
target: 'lib1',
});
projectGraph.nodes.lib1.data.targets.prebuild = {};
const tasks = createTasksForProjectToRun(
[projectGraph.nodes.app1],
{
target: 'build',
configuration: undefined,
overrides: {},
},
projectGraph,
projectGraph.nodes.app1.name
);
expect(tasks).toEqual([
{
id: 'lib1:prebuild',
overrides: {},
projectRoot: 'lib1-root',
target: {
configuration: undefined,
project: 'lib1',
target: 'prebuild',
},
},
{
id: 'lib1:build',
overrides: {},
projectRoot: 'lib1-root',
target: {
configuration: undefined,
project: 'lib1',
target: 'build',
},
},
{
id: 'app1:build',
overrides: {},
projectRoot: 'app1-root',
target: {
configuration: undefined,
project: 'app1',
target: 'build',
},
},
]);
});
it('should include dependencies of projects without the same target', () => {
// App 1 depends on builds of its dependencies
projectGraph.nodes.app1.data.targets.build.dependsOn = [
{
target: 'build',
projects: 'dependencies',
},
];
// App 1 depends on Lib 1
projectGraph.dependencies.app1.push({
type: DependencyType.static,
source: 'app1',
target: 'lib1',
});
// Lib 1 does not have build but depends on Lib 2
delete projectGraph.nodes.lib1.data.targets.build;
projectGraph.dependencies.lib1.push({
type: DependencyType.static,
source: 'lib1',
target: 'lib2',
});
// Lib 2 has a build
projectGraph.nodes.lib2 = {
name: 'lib2',
type: 'lib',
data: {
root: 'lib2-root',
files: [],
targets: {
build: {},
},
},
};
projectGraph.dependencies.lib2 = [];
const tasks = createTasksForProjectToRun(
[projectGraph.nodes.app1],
{
target: 'build',
configuration: undefined,
overrides: {},
},
projectGraph,
projectGraph.nodes.app1.name
);
expect(tasks).toContainEqual({
id: 'app1:build',
target: { project: 'app1', target: 'build' },
projectRoot: 'app1-root',
overrides: {},
});
expect(tasks).toContainEqual({
id: 'lib2:build',
target: { project: 'lib2', target: 'build' },
projectRoot: 'lib2-root',
overrides: {},
});
});
it('should handle circular dependencies between projects', () => {
// App 1 depends on builds of its dependencies
projectGraph.nodes.app1.data.targets.build.dependsOn = [
{
target: 'build',
projects: 'dependencies',
},
];
// App 1 depends on Lib 1
projectGraph.dependencies.app1.push({
type: DependencyType.static,
source: 'app1',
target: 'lib1',
});
// Lib 1 does not have build but depends on Lib 2
delete projectGraph.nodes.lib1.data.targets.build;
projectGraph.dependencies.lib1.push(
{
type: DependencyType.static,
source: 'lib1',
target: 'app1',
},
{
type: DependencyType.static,
source: 'lib1',
target: 'lib2',
}
);
// Lib 2 has a build
projectGraph.nodes.lib2 = {
name: 'lib2',
type: 'lib',
data: {
root: 'lib2-root',
files: [],
targets: {
build: {},
},
},
};
projectGraph.dependencies.lib2 = [
{
type: DependencyType.static,
source: 'lib2',
target: 'lib1',
},
];
const tasks = createTasksForProjectToRun(
[projectGraph.nodes.app1],
{
target: 'build',
configuration: undefined,
overrides: {},
},
projectGraph,
projectGraph.nodes.app1.name
);
expect(tasks).toContainEqual({
id: 'app1:build',
target: { project: 'app1', target: 'build' },
projectRoot: 'app1-root',
overrides: {},
});
expect(tasks).toContainEqual({
id: 'lib2:build',
target: { project: 'lib2', target: 'build' },
projectRoot: 'lib2-root',
overrides: {},
});
});
it('should handle circular dependencies between projects with no tasks', () => {
// App 1 depends on builds of its dependencies
projectGraph.nodes.app1.data.targets.build.dependsOn = [
{
target: 'build',
projects: 'dependencies',
},
];
// App 1 depends on Lib 1
projectGraph.dependencies.app1.push({
type: DependencyType.static,
source: 'app1',
target: 'lib1',
});
// Lib 1 does not have build but depends on Lib 2
delete projectGraph.nodes.lib1.data.targets.build;
projectGraph.dependencies.lib1.push(
{
type: DependencyType.static,
source: 'lib1',
target: 'app1',
},
{
type: DependencyType.static,
source: 'lib1',
target: 'lib2',
}
);
// Lib 2 has a build
projectGraph.nodes.lib2 = {
name: 'lib2',
type: 'lib',
data: {
root: 'lib2-root',
files: [],
targets: {
build: {},
},
},
};
projectGraph.dependencies.lib2 = [];
const tasks = createTasksForProjectToRun(
[projectGraph.nodes.app1],
{
target: 'build',
configuration: undefined,
overrides: {},
},
projectGraph,
projectGraph.nodes.app1.name
);
expect(tasks).toContainEqual({
id: 'app1:build',
target: { project: 'app1', target: 'build' },
projectRoot: 'app1-root',
overrides: {},
});
expect(tasks).toContainEqual({
id: 'lib2:build',
target: { project: 'lib2', target: 'build' },
projectRoot: 'lib2-root',
overrides: {},
});
});
it('should throw an error for an invalid target', () => {
jest.spyOn(process, 'exit').mockImplementation(() => {
throw new Error();

View File

@ -155,6 +155,7 @@ export function createTasksForProjectToRun(
defaultDependencyConfigs: Record<string, TargetDependencyConfig[]> = {}
) {
const tasksMap: Map<string, Task> = new Map<string, Task>();
const seenSet = new Set<string>();
for (const project of projectsToRun) {
addTasksForProjectTarget(
@ -166,7 +167,8 @@ export function createTasksForProjectToRun(
defaultDependencyConfigs,
projectGraph,
tasksMap,
[]
[],
seenSet
);
}
return Array.from(tasksMap.values());
@ -183,7 +185,8 @@ function addTasksForProjectTarget(
defaultDependencyConfigs: Record<string, TargetDependencyConfig[]> = {},
projectGraph: ProjectGraph,
tasksMap: Map<string, Task>,
path: string[]
path: string[],
seenSet: Set<string>
) {
const task = createTask({
project,
@ -211,7 +214,8 @@ function addTasksForProjectTarget(
defaultDependencyConfigs,
projectGraph,
tasksMap,
path
path,
seenSet
);
}
}
@ -267,13 +271,15 @@ function addTasksForProjectDependencyConfig(
defaultDependencyConfigs: Record<string, TargetDependencyConfig[]>,
projectGraph: ProjectGraph,
tasksMap: Map<string, Task>,
path: string[]
path: string[],
seenSet: Set<string>
) {
const targetIdentifier = getId({
project: project.name,
target,
configuration,
});
seenSet.add(project.name);
if (path.includes(targetIdentifier)) {
output.error({
@ -303,7 +309,23 @@ function addTasksForProjectDependencyConfig(
defaultDependencyConfigs,
projectGraph,
tasksMap,
[...path, targetIdentifier]
[...path, targetIdentifier],
seenSet
);
} else {
if (seenSet.has(dep.target)) {
continue;
}
addTasksForProjectDependencyConfig(
projectGraph.nodes[dep.target],
{ target, configuration },
dependencyConfig,
defaultDependencyConfigs,
projectGraph,
tasksMap,
path,
seenSet
);
}
}
@ -319,7 +341,8 @@ function addTasksForProjectDependencyConfig(
defaultDependencyConfigs,
projectGraph,
tasksMap,
[...path, targetIdentifier]
[...path, targetIdentifier],
seenSet
);
}
}

View File

@ -294,6 +294,109 @@ describe('TaskGraphCreator', () => {
expect(taskGraph).toMatchSnapshot();
});
it('should create a task graph (builds depend on builds of dependencies even with intermediate projects)', () => {
delete projectGraph.nodes.common1.data.targets.build;
projectGraph.dependencies.common1.push({
type: DependencyType.static,
source: 'common1',
target: 'common2',
});
const tasks = createTasksForProjectToRun(
[projectGraph.nodes.app1],
{
target: 'build',
configuration: undefined,
overrides: {},
},
projectGraph,
null
);
const taskGraph = new TaskGraphCreator(projectGraph, {}).createTaskGraph(
tasks
);
expect(taskGraph).toMatchSnapshot();
});
it('should create a task graph (builds depend on builds of dependencies with intermediate projects and circular dependencies between projects)', () => {
delete projectGraph.nodes.common1.data.targets.build;
projectGraph.dependencies.common1.push({
type: DependencyType.static,
source: 'common1',
target: 'common2',
});
projectGraph.dependencies.common2.push({
type: DependencyType.static,
source: 'common2',
target: 'common1',
});
const tasks = createTasksForProjectToRun(
[projectGraph.nodes.app1],
{
target: 'build',
configuration: undefined,
overrides: {},
},
projectGraph,
null
);
const taskGraph = new TaskGraphCreator(projectGraph, {}).createTaskGraph(
tasks
);
expect(taskGraph).toMatchSnapshot();
});
it('should create a task graph (builds depend on builds of dependencies with intermediate projects and circular dependencies between projects) 2', () => {
delete projectGraph.nodes.common1.data.targets.build;
projectGraph.dependencies.common1.push({
type: DependencyType.static,
source: 'common1',
target: 'common2',
});
delete projectGraph.nodes.common2.data.targets.build;
projectGraph.dependencies.common2.push({
type: DependencyType.static,
source: 'common2',
target: 'common3',
});
projectGraph.nodes.common3 = {
name: 'common3',
type: 'lib',
data: {
root: 'common3',
targets: {
build: {},
},
},
};
projectGraph.dependencies.common3 = [];
const tasks = createTasksForProjectToRun(
[projectGraph.nodes.app1],
{
target: 'build',
configuration: undefined,
overrides: {},
},
projectGraph,
null
);
const taskGraph = new TaskGraphCreator(projectGraph, {}).createTaskGraph(
tasks
);
expect(taskGraph).toMatchSnapshot();
});
it('should create task graph (builds depend on build of dependencies and prebuild of self)', () => {
projectGraph.nodes.app1.data.targets.prebuild = {};
projectGraph.nodes.app2.data.targets.prebuild = {};

View File

@ -22,8 +22,8 @@ export class TaskGraphCreator {
dependencies: {},
};
for (const task of tasks) {
graph.tasks[task.id] = task;
graph.dependencies[task.id] = [];
this.addTaskToGraph(task, graph);
const dependencyConfigs = getDependencyConfigs(
task.target,
this.defaultTargetDependencies,
@ -34,12 +34,22 @@ export class TaskGraphCreator {
continue;
}
const projectDependencies = new Set(
this.projectGraph.dependencies[task.target.project].map(
(dependency) => dependency.target
)
this.addTaskDependencies(task, dependencyConfigs, tasks, graph);
}
graph.roots = Object.keys(graph.dependencies).filter(
(k) => graph.dependencies[k].length === 0
);
return graph;
}
private addTaskDependencies(
task: Task,
dependencyConfigs: TargetDependencyConfig[],
tasks: Task[],
graph: TaskGraph
) {
for (const dependencyConfig of dependencyConfigs) {
if (dependencyConfig.projects === 'self') {
for (const t of tasks) {
@ -51,22 +61,66 @@ export class TaskGraphCreator {
}
}
} else if (dependencyConfig.projects === 'dependencies') {
for (const t of tasks) {
if (
projectDependencies.has(t.target.project) &&
t.target.target === dependencyConfig.target
) {
graph.dependencies[task.id].push(t.id);
}
}
}
}
}
const seen = new Set<string>();
graph.roots = Object.keys(graph.dependencies).filter(
(k) => graph.dependencies[k].length === 0
this.addDependencies(
task.target.project,
dependencyConfig.target,
tasks,
graph,
task.id,
seen
);
}
}
}
return graph;
private addDependencies(
project: string,
target: string,
tasks: Task[],
graph: TaskGraph,
taskId: string,
seen: Set<string>
) {
seen.add(project);
const projectDependencies = this.projectGraph.dependencies[project].map(
(dependency) => dependency.target
);
for (const projectDependency of projectDependencies) {
if (seen.has(projectDependency)) {
continue;
}
const dependency = this.findTask(
{ project: projectDependency, target },
tasks
);
if (dependency) {
graph.dependencies[taskId].push(dependency.id);
} else {
this.addDependencies(
projectDependency,
target,
tasks,
graph,
taskId,
seen
);
}
}
}
private findTask(
{ project, target }: { project: string; target: string },
tasks: Task[]
): Task {
return tasks.find(
(t) => t.target.project === project && t.target.target === target
);
}
private addTaskToGraph(task: Task, graph: TaskGraph) {
graph.tasks[task.id] = task;
graph.dependencies[task.id] = [];
}
}