fix(core): wait for deps to be built before building projects

This commit is contained in:
Victor Savkin 2019-12-27 15:54:28 -05:00 committed by Victor Savkin
parent d7a769ccf5
commit c98745a55e
6 changed files with 255 additions and 76 deletions

View File

@ -240,4 +240,54 @@ forEachCli(() => {
expect(interpolatedTests).toContain(`Running target \"test\" succeeded`);
}, 1000000);
});
describe('build in the right order', () => {
let myapp, mypublishablelib;
beforeEach(() => {
ensureProject();
// create my app depending on mypublishablelib
myapp = uniq('myapp');
mypublishablelib = uniq('mypublishablelib');
runCLI(`generate @nrwl/angular:app ${myapp}`);
runCLI(`generate @nrwl/angular:lib ${mypublishablelib} --publishable`);
updateFile(
`apps/${myapp}/src/app/app.component.spec.ts`,
`
import '@proj/${mypublishablelib}';
describe('sample test', () => {
it('should test', () => {
expect(1).toEqual(1);
});
});
`
);
});
it('should wait for deps to be built before continuing', () => {
const build = runCommand(
`npm run affected:build -- --files="apps/${myapp}/src/main.ts,libs/${mypublishablelib}/src/index.ts" --parallel`
);
console.log(build);
// make sure that the package is done building before we start building the app
expect(
build.indexOf('Built Angular Package!') <
build.indexOf(`"build" "${myapp}"`)
).toBeTruthy();
});
it('should not invoke build for projects who deps fail', () => {
updateFile(
`libs/${mypublishablelib}/src/index.ts`,
`
const x: number = 'string';
`
);
const build = runCommand(
`npm run affected:build -- --files="apps/${myapp}/src/main.ts,libs/${mypublishablelib}/src/index.ts" --parallel`
);
expect(build.indexOf(`"build" "${myapp}"`)).toEqual(-1);
});
});
});

View File

@ -10,7 +10,6 @@ import {
DefaultTasksRunnerOptions
} from '@nrwl/workspace/src/tasks-runner/default-tasks-runner';
import * as fs from 'fs';
import { TasksMap } from '@nrwl/workspace/src/tasks-runner/run-command';
import { ProjectGraph } from '@nrwl/workspace/src/core/project-graph';
const axios = require('axios');
@ -20,7 +19,6 @@ interface InsightsTaskRunnerOptions extends DefaultTasksRunnerOptions {
type Context = {
projectGraph: ProjectGraph;
tasksMap: TasksMap;
target: string;
};

View File

@ -1,7 +1,11 @@
import defaultTaskRunner from './default-tasks-runner';
import { AffectedEventType, Task } from './tasks-runner';
jest.mock('npm-run-all', () => jest.fn());
import defaultTaskRunner, {
splitTasksIntoStages
} from './default-tasks-runner';
import { AffectedEventType } from './tasks-runner';
import * as runAll from 'npm-run-all';
import { DependencyType } from '@nrwl/workspace/src/core/project-graph';
jest.mock('npm-run-all', () => jest.fn());
jest.mock('../core/file-utils', () => ({
cliCommand: () => 'nx'
}));
@ -134,4 +138,77 @@ describe('defaultTasksRunner', () => {
complete: done
});
});
describe('splitTasksIntoStages', () => {
it('should return empty for an empty array', () => {
const stages = splitTasksIntoStages([], { nodes: {}, dependencies: {} });
expect(stages).toEqual([]);
});
it('should split tasks into stages based on their dependencies', () => {
const stages = splitTasksIntoStages(
[
{
target: { project: 'parent' }
},
{
target: { project: 'child1' }
},
{
target: { project: 'child2' }
},
{
target: { project: 'grandparent' }
}
] as any,
{
nodes: {},
dependencies: {
child1: [],
child2: [],
parent: [
{
source: 'parent',
target: 'child1',
type: DependencyType.static
},
{
source: 'parent',
target: 'child2',
type: DependencyType.static
}
],
grandparent: [
{
source: 'grandparent',
target: 'parent',
type: DependencyType.static
}
]
}
}
);
expect(stages).toEqual([
[
{
target: { project: 'child1' }
},
{
target: { project: 'child2' }
}
],
[
{
target: { project: 'parent' }
}
],
[
{
target: { project: 'grandparent' }
}
]
]);
});
});
});

View File

@ -11,46 +11,79 @@ import { output } from '../utils/output';
import { readJsonFile } from '../utils/fileutils';
import { getCommand } from './utils';
import { cliCommand } from '../core/file-utils';
import { ProjectGraph } from '../core/project-graph';
export interface DefaultTasksRunnerOptions {
parallel?: boolean;
maxParallel?: number;
}
function taskDependsOnDeps(
task: Task,
deps: Task[],
projectGraph: ProjectGraph
) {
function hasDep(source: string, target: string, visitedProjects: string[]) {
if (!projectGraph.dependencies[source]) {
return false;
}
if (projectGraph.dependencies[source].find(d => d.target === target)) {
return true;
}
return !!projectGraph.dependencies[source].find(r => {
if (visitedProjects.indexOf(r.target) > -1) return null;
return hasDep(r.target, target, [...visitedProjects, r.target]);
});
}
return !!deps.find(dep =>
hasDep(task.target.project, dep.target.project, [])
);
}
function topologicallySortTasks(tasks: Task[], projectGraph: ProjectGraph) {
const sortedTasks = [...tasks];
sortedTasks.sort((a, b) => {
if (taskDependsOnDeps(a, [b], projectGraph)) return 1;
if (taskDependsOnDeps(b, [a], projectGraph)) return -1;
return 0;
});
return sortedTasks;
}
export function splitTasksIntoStages(
tasks: Task[],
projectGraph: ProjectGraph
) {
if (tasks.length === 0) return [];
const res = [];
topologicallySortTasks(tasks, projectGraph).forEach(t => {
const stageWithNoDeps = res.find(
tasksInStage => !taskDependsOnDeps(t, tasksInStage, projectGraph)
);
if (stageWithNoDeps) {
stageWithNoDeps.push(t);
} else {
res.push([t]);
}
});
return res;
}
export const defaultTasksRunner: TasksRunner<DefaultTasksRunnerOptions> = (
tasks: Task[],
options: DefaultTasksRunnerOptions
options: DefaultTasksRunnerOptions,
context: { target: string; projectGraph: ProjectGraph }
): Observable<TaskCompleteEvent> => {
const cli = cliCommand();
const isYarn = basename(process.env.npm_execpath || 'npm').startsWith('yarn');
assertPackageJsonScriptExists(cli);
const commands = tasks.map(t => getCommand(cli, isYarn, t));
return new Observable(subscriber => {
runAll(commands, {
parallel: options.parallel || false,
maxParallel: options.maxParallel || 3,
continueOnError: true,
stdin: process.stdin,
stdout: process.stdout,
stderr: process.stderr
})
.then(() => {
tasks.forEach(task => {
subscriber.next({
task: task,
type: AffectedEventType.TaskComplete,
success: true
});
});
})
runTasks(tasks, options, context)
.then(data => data.forEach(d => subscriber.next(d)))
.catch(e => {
e.results.forEach((result, i) => {
subscriber.next({
task: tasks[i],
type: AffectedEventType.TaskComplete,
success: result.code === 0
});
});
console.error('Unexpected error:');
console.error(e);
process.exit(1);
})
.finally(() => {
subscriber.complete();
@ -60,6 +93,60 @@ export const defaultTasksRunner: TasksRunner<DefaultTasksRunnerOptions> = (
});
};
async function runTasks(
tasks: Task[],
options: DefaultTasksRunnerOptions,
context: { target: string; projectGraph: ProjectGraph }
): Promise<Array<{ task: Task; type: any; success: boolean }>> {
const cli = cliCommand();
assertPackageJsonScriptExists(cli);
const isYarn = basename(process.env.npm_execpath || 'npm').startsWith('yarn');
const stages =
context.target === 'build'
? splitTasksIntoStages(tasks, context.projectGraph)
: [tasks];
const res = [];
for (let i = 0; i < stages.length; ++i) {
const tasksInStage = stages[i];
try {
const commands = tasksInStage.map(t => getCommand(cli, isYarn, t));
await runAll(commands, {
parallel: options.parallel || false,
maxParallel: options.maxParallel || 3,
continueOnError: true,
stdin: process.stdin,
stdout: process.stdout,
stderr: process.stderr
});
res.push(...tasksToStatuses(tasksInStage, true));
} catch (e) {
e.results.forEach((result, i) => {
res.push({
task: tasksInStage[i],
type: AffectedEventType.TaskComplete,
success: result.code === 0
});
});
res.push(...markStagesAsNotSuccessful(stages.splice(i + 1)));
return res;
}
}
return res;
}
function markStagesAsNotSuccessful(stages: Task[][]) {
return stages.reduce((m, c) => [...m, ...tasksToStatuses(c, false)], []);
}
function tasksToStatuses(tasks: Task[], success: boolean) {
return tasks.map(task => ({
task,
type: AffectedEventType.TaskComplete,
success
}));
}
function assertPackageJsonScriptExists(cli: string) {
// Make sure the `package.json` has the `nx: "nx"` command needed by `npm-run-all`
const packageJson = readJsonFile('./package.json');

View File

@ -12,13 +12,8 @@ import { DefaultReporter, ReporterArgs } from './default-reporter';
import * as yargs from 'yargs';
import { ProjectGraph, ProjectGraphNode } from '../core/project-graph';
import { Environment, NxJson } from '../core/shared-interfaces';
import { projectHasTargetAndConfiguration } from '../utils/project-has-target-and-configuration';
import { NxArgs } from '@nrwl/workspace/src/command-line/utils';
export interface TasksMap {
[projectName: string]: { [targetName: string]: Task };
}
type RunArgs = yargs.Arguments & ReporterArgs;
export function runCommand<T extends RunArgs>(
@ -39,25 +34,6 @@ export function runCommand<T extends RunArgs>(
})
);
const tasksMap: TasksMap = {};
Object.entries(projectGraph.nodes).forEach(([projectName, project]) => {
const runnable = projectHasTargetAndConfiguration(
project,
nxArgs.target,
nxArgs.configuration
);
if (runnable) {
tasksMap[projectName] = {
[nxArgs.target]: createTask({
project: project,
target: nxArgs.target,
configuration: nxArgs.configuration,
overrides: overrides
})
};
}
});
const { tasksRunner, tasksOptions } = getRunner(
nxArgs.runner,
nxJson,
@ -65,8 +41,7 @@ export function runCommand<T extends RunArgs>(
);
tasksRunner(tasks, tasksOptions, {
target: nxArgs.target,
projectGraph,
tasksMap
projectGraph
}).subscribe({
next: (event: TaskCompleteEvent) => {
switch (event.type) {
@ -107,17 +82,14 @@ export function createTask({
configuration,
overrides
}: TaskParams): Task {
const qualifiedTarget = {
project: project.name,
target,
configuration
};
return {
id: getId({
project: project.name,
target: target,
configuration: configuration
}),
target: {
project: project.name,
target,
configuration
},
id: getId(qualifiedTarget),
target: qualifiedTarget,
overrides: interpolateOverrides(overrides, project.name, project.data)
};
}

View File

@ -25,14 +25,9 @@ export interface TaskCompleteEvent extends AffectedEvent {
export type TasksRunner<T = unknown> = (
tasks: Task[],
options?: T,
options: T,
context?: {
target?: string;
projectGraph: ProjectGraph;
tasksMap: {
[projectName: string]: {
[targetName: string]: Task;
};
};
}
) => Observable<AffectedEvent>;