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`); expect(interpolatedTests).toContain(`Running target \"test\" succeeded`);
}, 1000000); }, 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 DefaultTasksRunnerOptions
} from '@nrwl/workspace/src/tasks-runner/default-tasks-runner'; } from '@nrwl/workspace/src/tasks-runner/default-tasks-runner';
import * as fs from 'fs'; import * as fs from 'fs';
import { TasksMap } from '@nrwl/workspace/src/tasks-runner/run-command';
import { ProjectGraph } from '@nrwl/workspace/src/core/project-graph'; import { ProjectGraph } from '@nrwl/workspace/src/core/project-graph';
const axios = require('axios'); const axios = require('axios');
@ -20,7 +19,6 @@ interface InsightsTaskRunnerOptions extends DefaultTasksRunnerOptions {
type Context = { type Context = {
projectGraph: ProjectGraph; projectGraph: ProjectGraph;
tasksMap: TasksMap;
target: string; target: string;
}; };

View File

@ -1,7 +1,11 @@
import defaultTaskRunner from './default-tasks-runner'; import defaultTaskRunner, {
import { AffectedEventType, Task } from './tasks-runner'; splitTasksIntoStages
jest.mock('npm-run-all', () => jest.fn()); } from './default-tasks-runner';
import { AffectedEventType } from './tasks-runner';
import * as runAll from 'npm-run-all'; 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', () => ({ jest.mock('../core/file-utils', () => ({
cliCommand: () => 'nx' cliCommand: () => 'nx'
})); }));
@ -134,4 +138,77 @@ describe('defaultTasksRunner', () => {
complete: done 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 { readJsonFile } from '../utils/fileutils';
import { getCommand } from './utils'; import { getCommand } from './utils';
import { cliCommand } from '../core/file-utils'; import { cliCommand } from '../core/file-utils';
import { ProjectGraph } from '../core/project-graph';
export interface DefaultTasksRunnerOptions { export interface DefaultTasksRunnerOptions {
parallel?: boolean; parallel?: boolean;
maxParallel?: number; 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> = ( export const defaultTasksRunner: TasksRunner<DefaultTasksRunnerOptions> = (
tasks: Task[], tasks: Task[],
options: DefaultTasksRunnerOptions options: DefaultTasksRunnerOptions,
context: { target: string; projectGraph: ProjectGraph }
): Observable<TaskCompleteEvent> => { ): 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 => { return new Observable(subscriber => {
runAll(commands, { runTasks(tasks, options, context)
parallel: options.parallel || false, .then(data => data.forEach(d => subscriber.next(d)))
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
});
});
})
.catch(e => { .catch(e => {
e.results.forEach((result, i) => { console.error('Unexpected error:');
subscriber.next({ console.error(e);
task: tasks[i], process.exit(1);
type: AffectedEventType.TaskComplete,
success: result.code === 0
});
});
}) })
.finally(() => { .finally(() => {
subscriber.complete(); 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) { function assertPackageJsonScriptExists(cli: string) {
// Make sure the `package.json` has the `nx: "nx"` command needed by `npm-run-all` // Make sure the `package.json` has the `nx: "nx"` command needed by `npm-run-all`
const packageJson = readJsonFile('./package.json'); const packageJson = readJsonFile('./package.json');

View File

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

View File

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