fix(core): wait for deps to be built before building projects
This commit is contained in:
parent
d7a769ccf5
commit
c98745a55e
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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');
|
||||||
|
|||||||
@ -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)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user