feat(core): running one task uses the same tasks runner as run-many

This commit is contained in:
Victor Savkin 2020-01-08 10:24:00 -05:00 committed by Victor Savkin
parent 66634804ad
commit c3fdf2e702
21 changed files with 459 additions and 144 deletions

View File

@ -268,11 +268,11 @@ forEachCli(() => {
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}"`)
build.indexOf(`Generating ES5 bundles for differential loading.`)
).toBeTruthy();
});

View File

@ -36,7 +36,7 @@ forEachCli(() => {
});
describe('Cache', () => {
it('should not use cache when it is not enabled', async () => {
it('should cache command execution', async () => {
ensureProject();
const myapp1 = uniq('myapp1');
@ -128,6 +128,12 @@ forEachCli(() => {
'read the output from cache'
);
// build individual project with caching
const individualBuildWithCache = runCommand(
`npm run nx -- build ${myapp1}`
);
expect(individualBuildWithCache).toContain('Cached Output');
// run lint with caching
// --------------------------------------------
const outputWithNoLintCached = runCommand(

View File

@ -1,5 +1,7 @@
import * as path from 'path';
import * as fs from 'fs';
import { Workspace } from './workspace';
import { parseRunOneOptions } from './parse-run-one-options';
/**
* Nx is being run inside a workspace.
@ -7,25 +9,35 @@ import { Workspace } from './workspace';
* @param workspace Relevant local workspace properties
*/
export function initLocal(workspace: Workspace) {
// required to make sure nrwl/workspace import works
if (workspace.type === 'nx') {
require(path.join(
workspace.dir,
'node_modules',
'@nrwl',
'tao',
'src',
'compat',
'compat.js'
));
}
const supportedNxCommands = require('@nrwl/workspace/src/command-line/supported-nx-commands')
.supportedNxCommands;
const runOpts = runOneOptions(workspace);
// The commandsObject is a Yargs object declared in `nx-commands.ts`,
// It is exposed and bootstrapped here to provide CLI features.
const w = require('@nrwl/workspace');
if (w.supportedNxCommands.includes(process.argv[2])) {
w.commandsObject.argv;
} else if (workspace.type === 'nx') {
if (supportedNxCommands.includes(process.argv[2])) {
// required to make sure nrwl/workspace import works
if (workspace.type === 'nx') {
require(path.join(
workspace.dir,
'node_modules',
'@nrwl',
'tao',
'src',
'compat',
'compat.js'
));
}
require('@nrwl/workspace/src/command-line/nx-commands').commandsObject.argv;
} else {
if (runOpts === false || process.env.NX_SKIP_TASKS_RUNNER) {
loadCli(workspace);
} else {
require('@nrwl/workspace/src/command-line/run-one').runOne(runOpts);
}
}
}
function loadCli(workspace: Workspace) {
if (workspace.type === 'nx') {
require(path.join(
workspace.dir,
'node_modules',
@ -34,9 +46,6 @@ export function initLocal(workspace: Workspace) {
'index.js'
));
} else if (workspace.type === 'angular') {
w.output.note({
title: `Nx didn't recognize the command, forwarding on to the Angular CLI.`
});
require(path.join(
workspace.dir,
'node_modules',
@ -45,5 +54,37 @@ export function initLocal(workspace: Workspace) {
'lib',
'init.js'
));
} else {
console.error(`Cannot recognize the workspace type.`);
process.exit(1);
}
}
function runOneOptions(
workspace: Workspace
): false | { project; target; configuration; overrides } {
try {
const nxJson = JSON.parse(
fs.readFileSync(path.join(workspace.dir, 'nx.json')).toString()
);
const workspaceConfigJson = JSON.parse(
fs
.readFileSync(
path.join(
workspace.dir,
workspace.type === 'nx' ? 'workspace.json' : 'angular.json'
)
)
.toString()
);
return parseRunOneOptions(
nxJson,
workspaceConfigJson,
process.argv.slice(2)
);
} catch (e) {
return false;
}
}

View File

@ -0,0 +1,77 @@
import { parseRunOneOptions } from './parse-run-one-options';
describe('parseRunOneOptions', () => {
const nxJson = { tasksRunnerOptions: { default: { runner: 'somerunner' } } };
const workspaceJson = { projects: { myproj: { architect: { build: {} } } } };
const args = ['build', 'myproj', '--configuration=production', '--flag=true'];
it('should work', () => {
expect(parseRunOneOptions(nxJson, workspaceJson, args)).toEqual({
project: 'myproj',
target: 'build',
configuration: 'production',
overrides: { flag: 'true' }
});
});
it('should work with run syntax', () => {
expect(
parseRunOneOptions(nxJson, workspaceJson, [
'run',
'myproj:build:production',
'--flag=true'
])
).toEqual({
project: 'myproj',
target: 'build',
configuration: 'production',
overrides: { flag: 'true' }
});
});
it('should use defaultProjectName when no provided', () => {
expect(
parseRunOneOptions(
nxJson,
{ ...workspaceJson, cli: { defaultProjectName: 'myproj' } },
['build', '--flag=true']
)
).toEqual({
project: 'myproj',
target: 'build',
overrides: { flag: 'true' }
});
});
it('should return false when no runner is set', () => {
expect(parseRunOneOptions({}, workspaceJson, args)).toBe(false);
expect(
parseRunOneOptions({ tasksRunnerOptions: {} }, workspaceJson, args)
).toBe(false);
expect(
parseRunOneOptions(
{ tasksRunnerOptions: { default: {} } },
workspaceJson,
args
)
).toBe(false);
});
it('should return false when the task is not recognized', () => {
expect(parseRunOneOptions(nxJson, {}, args)).toBe(false);
expect(parseRunOneOptions(nxJson, { projects: {} }, args)).toBe(false);
expect(
parseRunOneOptions(nxJson, { projects: { architect: {} } }, args)
).toBe(false);
});
it('should return false when cannot find the right project', () => {
expect(
parseRunOneOptions(nxJson, workspaceJson, ['build', 'wrongproj'])
).toBe(false);
});
it('should return false when no project specified', () => {
expect(parseRunOneOptions(nxJson, workspaceJson, ['build'])).toBe(false);
});
});

View File

@ -0,0 +1,76 @@
import yargsParser = require('yargs-parser');
export function parseRunOneOptions(
nxJson: any,
workspaceConfigJson: any,
args: string[]
): false | { project; target; configuration; overrides } {
// custom runner is not set, no tasks runner
if (
!nxJson.tasksRunnerOptions ||
!nxJson.tasksRunnerOptions.default ||
!nxJson.tasksRunnerOptions.default.runner
) {
return false;
}
// the list of all possible tasks doesn't include the given name, no tasks runner
let allPossibleTasks = ['run'];
Object.values(workspaceConfigJson.projects || {}).forEach((p: any) => {
allPossibleTasks.push(...Object.keys(p.architect || {}));
});
if (allPossibleTasks.indexOf(args[0]) === -1) {
return false;
}
let defaultProjectName = null;
try {
defaultProjectName = workspaceConfigJson.cli.defaultProjectName;
} catch (e) {}
const overrides = yargsParser(args, {
boolean: ['prod'],
string: ['configuration', 'project']
});
let project;
let target;
let configuration;
if (overrides._[0] === 'run') {
[project, target, configuration] = overrides._[1].split(':');
} else {
target = overrides._[0];
project = overrides._[1];
}
if (!project && defaultProjectName) {
project = defaultProjectName;
}
if (overrides.configuration) {
configuration = overrides.configuration;
}
if (overrides.prod) {
configuration = 'production';
}
if (overrides.project) {
project = overrides.project;
}
// we need both to be able to run a target, no tasks runner
if (!project || !target) {
return false;
}
// we need both to be able to run a target, no tasks runner
if (!workspaceConfigJson.projects[project]) return false;
const res = { project, target, configuration, overrides };
delete overrides['_'];
delete overrides['configuration'];
delete overrides['prod'];
delete overrides['project'];
return res;
}

View File

@ -0,0 +1,70 @@
import * as path from 'path';
import * as fs from 'fs';
import { findWorkspaceRoot } from './find-workspace-root';
const workspace = findWorkspaceRoot(process.cwd());
setUpOutputWatching();
requireCli();
function requireCli() {
if (workspace.type === 'nx') {
require(path.join(
workspace.dir,
'node_modules',
'@nrwl',
'tao',
'index.js'
));
} else {
require(path.join(
workspace.dir,
'node_modules',
'@angular',
'cli',
'lib',
'init.js'
));
}
}
/**
* We need to collect all stdout and stderr and store it, so the caching mechanism
* could store it.
*
* Writing stdout and stderr into different stream is too risky when using TTY.
*
* So we are simply monkey-patching the Javascript object. In this case the actual output will always be correct.
* And the cached output should be correct unless the CLI bypasses process.stdout or console.log and uses some
* C-binary to write to stdout.
*/
function setUpOutputWatching() {
const stdoutWrite = process.stdout._write;
const stderrWrite = process.stderr._write;
let out = [];
process.stdout._write = (
chunk: any,
encoding: string,
callback: Function
) => {
out.push(chunk.toString());
stdoutWrite.apply(process.stdout, [chunk, encoding, callback]);
};
process.stderr._write = (
chunk: any,
encoding: string,
callback: Function
) => {
out.push(chunk.toString());
stderrWrite.apply(process.stderr, [chunk, encoding, callback]);
};
process.on('exit', code => {
if (code === 0) {
fs.writeFileSync(process.env.NX_TERMINAL_OUTPUT_PATH, out.join(''));
}
});
}

View File

@ -51,7 +51,7 @@ class InsightsRemoteCache implements RemoteCache {
if (e.response && e.response.status === 404) {
// cache miss. print nothing
} else if (e.code === 'ECONNREFUSED') {
console.error(`Error: Cannot cannot to remote cache.`);
console.error(`Error: Cannot connect to remote cache.`);
} else {
console.error(e.message);
}
@ -79,7 +79,7 @@ class InsightsRemoteCache implements RemoteCache {
return true;
} catch (e) {
if (e.code === 'ECONNREFUSED') {
console.error(`Error: Cannot cannot to remote cache.`);
console.error(`Error: Cannot connect to remote cache.`);
} else {
console.error(e.message);
}

View File

@ -57,9 +57,6 @@ function parseRunOpts(
);
project = defaultProjectName;
}
if (!project || !target) {
throwInvalidInvocation();
}
if (runOptions.configuration) {
configuration = runOptions.configuration;
}
@ -69,6 +66,9 @@ function parseRunOpts(
if (runOptions.project) {
project = runOptions.project;
}
if (!project || !target) {
throwInvalidInvocation();
}
const res = { project, target, configuration, help, runOptions };
delete runOptions['help'];
delete runOptions['_'];

View File

@ -20,10 +20,8 @@ export {
resolveUserExistingPrettierConfig
} from './src/utils/common';
export { output } from './src/utils/output';
export {
commandsObject,
supportedNxCommands
} from './src/command-line/nx-commands';
export { commandsObject } from './src/command-line/nx-commands';
export { supportedNxCommands } from './src/command-line/supported-nx-commands';
export { readWorkspaceJson, readNxJson } from './src/core/file-utils';
export { NxJson } from './src/core/shared-interfaces';
export {

View File

@ -15,6 +15,7 @@ import {
import { calculateFileChanges, readEnvironment } from '../core/file-utils';
import { printAffected } from './print-affected';
import { projectHasTargetAndConfiguration } from '../utils/project-has-target-and-configuration';
import { DefaultReporter } from '../tasks-runner/default-reporter';
export function affected(command: string, parsedArgs: yargs.Arguments): void {
const { nxArgs, overrides } = splitArgsIntoNxArgsAndOverrides(parsedArgs);
@ -109,7 +110,8 @@ export function affected(command: string, parsedArgs: yargs.Arguments): void {
projectGraph,
env,
nxArgs,
overrides
overrides,
new DefaultReporter()
);
break;
}

View File

@ -14,31 +14,6 @@ import { runMany } from './run-many';
const noop = (yargs: yargs.Argv): yargs.Argv => yargs;
export const supportedNxCommands = [
'affected',
'affected:apps',
'affected:libs',
'affected:build',
'affected:test',
'affected:e2e',
'affected:dep-graph',
'affected:lint',
'print-affected',
'dep-graph',
'format',
'format:check',
'format:write',
'workspace-schematic',
'workspace-lint',
'migrate',
'report',
'run-many',
'list',
'help',
'--help',
'--version'
];
/**
* Exposing the Yargs commands object so the documentation generator can
* parse it. The CLI will consume it and call the `.argv` to bootstrapped

View File

@ -10,13 +10,21 @@ import {
} from '../core/project-graph';
import { readEnvironment } from '../core/file-utils';
import { projectHasTargetAndConfiguration } from '../utils/project-has-target-and-configuration';
import { DefaultReporter } from '../tasks-runner/default-reporter';
export function runMany(parsedArgs: yargs.Arguments): void {
const { nxArgs, overrides } = splitArgsIntoNxArgsAndOverrides(parsedArgs);
const env = readEnvironment(nxArgs.target);
const projectGraph = createProjectGraph();
const projects = projectsToRun(nxArgs, projectGraph);
runCommand(projects, projectGraph, env, nxArgs, overrides);
runCommand(
projects,
projectGraph,
env,
nxArgs,
overrides,
new DefaultReporter()
);
}
function projectsToRun(nxArgs: NxArgs, projectGraph: ProjectGraph) {

View File

@ -0,0 +1,23 @@
import { runCommand } from '../tasks-runner/run-command';
import { createProjectGraph } from '../core/project-graph';
import { readEnvironment } from '../core/file-utils';
import { EmptyReporter } from '../tasks-runner/empty-reporter';
export function runOne(opts: {
project: string;
target: string;
configuration: string;
overrides: any;
}): void {
const env = readEnvironment(opts.target);
const projectGraph = createProjectGraph();
const projects = [projectGraph.nodes[opts.project]];
runCommand(
projects,
projectGraph,
env,
opts,
opts.overrides,
new EmptyReporter()
);
}

View File

@ -0,0 +1,24 @@
export const supportedNxCommands = [
'affected',
'affected:apps',
'affected:libs',
'affected:build',
'affected:test',
'affected:e2e',
'affected:dep-graph',
'affected:lint',
'print-affected',
'dep-graph',
'format',
'format:check',
'format:write',
'workspace-schematic',
'workspace-lint',
'migrate',
'report',
'run-many',
'list',
'help',
'--help',
'--version'
];

View File

@ -2,13 +2,7 @@ import { appRootPath } from '../utils/app-root';
import { ProjectGraph } from '../core/project-graph';
import { NxJson } from '../core/shared-interfaces';
import { Task } from './tasks-runner';
import {
existsSync,
mkdirSync,
readFileSync,
rmdirSync,
writeFileSync
} from 'fs';
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
import { join } from 'path';
import { Hasher } from './hasher';
import * as fsExtra from 'fs-extra';
@ -17,10 +11,28 @@ import { DefaultTasksRunnerOptions } from './tasks-runner-v2';
export type CachedResult = { terminalOutput: string; outputsPath: string };
export type TaskWithCachedResult = { task: Task; cachedResult: CachedResult };
class CacheConfig {
constructor(private readonly options: DefaultTasksRunnerOptions) {}
isCacheableTask(task: Task) {
return (
this.options.cacheableOperations &&
this.options.cacheableOperations.indexOf(task.target.target) > -1 &&
!this.longRunningTask(task)
);
}
private longRunningTask(task: Task) {
return task.overrides['watch'] !== undefined;
}
}
export class Cache {
root = appRootPath;
cachePath = this.createCacheDir();
terminalOutputsDir = this.createTerminalOutputsDir();
hasher = new Hasher(this.projectGraph, this.nxJson);
cacheConfig = new CacheConfig(this.options);
constructor(
private readonly projectGraph: ProjectGraph,
@ -29,7 +41,7 @@ export class Cache {
) {}
async get(task: Task): Promise<CachedResult> {
if (!this.cacheable(task)) return null;
if (!this.cacheConfig.isCacheableTask(task)) return null;
const res = await this.getFromLocalDir(task);
@ -47,8 +59,9 @@ export class Cache {
}
}
async put(task: Task, terminalOutput: string, folders: string[]) {
if (!this.cacheable(task)) return;
async put(task: Task, terminalOutputPath: string, folders: string[]) {
if (!this.cacheConfig.isCacheableTask(task)) return;
const terminalOutput = readFileSync(terminalOutputPath).toString();
const hash = await this.hasher.hash(task);
const td = join(this.cachePath, hash);
const tdCommit = join(this.cachePath, `${hash}.commit`);
@ -101,6 +114,10 @@ export class Cache {
});
}
async temporaryOutputPath(task: Task) {
return join(this.terminalOutputsDir, await this.hasher.hash(task));
}
private async getFromLocalDir(task: Task) {
const hash = await this.hasher.hash(task);
const tdCommit = join(this.cachePath, `${hash}.commit`);
@ -116,13 +133,6 @@ export class Cache {
}
}
private cacheable(task: Task) {
return (
this.options.cacheableOperations &&
this.options.cacheableOperations.indexOf(task.target.target) > -1
);
}
private createCacheDir() {
let dir;
if (this.options.cacheDirectory) {
@ -139,4 +149,10 @@ export class Cache {
}
return dir;
}
private createTerminalOutputsDir() {
const path = join(this.cachePath, 'terminalOutputs');
mkdirSync(path, { recursive: true });
return path;
}
}

View File

@ -12,7 +12,7 @@ import { readJsonFile } from '../utils/fileutils';
import { getCommand, getCommandAsString } from './utils';
import { cliCommand } from '../core/file-utils';
import { ProjectGraph } from '../core/project-graph';
import { NxJson } from '@nrwl/workspace/src/core/shared-interfaces';
import { NxJson } from '../core/shared-interfaces';
export interface DefaultTasksRunnerOptions {
parallel?: boolean;

View File

@ -0,0 +1,5 @@
export class EmptyReporter {
beforeRun() {}
printResults() {}
}

View File

@ -1,13 +1,12 @@
import { AffectedEventType, Task, TasksRunner } from './tasks-runner';
import { defaultTasksRunner } from './default-tasks-runner';
import { isRelativePath } from '../utils/fileutils';
import { join } from 'path';
import { appRootPath } from '../utils/app-root';
import { DefaultReporter, ReporterArgs } from './default-reporter';
import { ReporterArgs } from './default-reporter';
import * as yargs from 'yargs';
import { ProjectGraph, ProjectGraphNode } from '../core/project-graph';
import { Environment, NxJson } from '../core/shared-interfaces';
import { NxArgs } from '@nrwl/workspace/src/command-line/utils';
import { isRelativePath } from '../utils/fileutils';
type RunArgs = yargs.Arguments & ReporterArgs;
@ -16,9 +15,9 @@ export function runCommand<T extends RunArgs>(
projectGraph: ProjectGraph,
{ nxJson, workspace }: Environment,
nxArgs: NxArgs,
overrides: any
overrides: any,
reporter: any
) {
const reporter = new DefaultReporter();
reporter.beforeRun(projectsToRun.map(p => p.name), nxArgs, overrides);
const tasks: Task[] = projectsToRun.map(project =>
createTask({
@ -122,15 +121,17 @@ export function getRunner(
tasksOptions: unknown;
} {
if (!nxJson.tasksRunnerOptions) {
const t = require('./default-tasks-runner');
return {
tasksRunner: defaultTasksRunner,
tasksRunner: t.defaultTasksRunner,
tasksOptions: overrides
};
}
if (!runner && !nxJson.tasksRunnerOptions.default) {
const t = require('./default-tasks-runner');
return {
tasksRunner: defaultTasksRunner,
tasksRunner: t.defaultTasksRunner,
tasksOptions: overrides
};
}

View File

@ -4,15 +4,16 @@ import { NxJson } from '../core/shared-interfaces';
import { ProjectGraph } from '../core/project-graph';
import { AffectedEventType, Task } from './tasks-runner';
import { getCommand, getOutputs } from './utils';
import { basename } from 'path';
import { spawn } from 'child_process';
import { fork, spawn } from 'child_process';
import { DefaultTasksRunnerOptions } from './tasks-runner-v2';
import { output } from '../utils/output';
import * as path from 'path';
import { appRootPath } from '../utils/app-root';
export class TaskOrchestrator {
workspaceRoot = appRootPath;
cache = new Cache(this.projectGraph, this.nxJson, this.options);
cli = cliCommand();
isYarn = basename(process.env.npm_execpath || 'npm').startsWith('yarn');
constructor(
private readonly nxJson: NxJson,
@ -40,7 +41,7 @@ export class TaskOrchestrator {
if (left.length > 0) {
const task = left.pop();
return that
.spawnProcess(task)
.forkProcess(task)
.then(code => {
res.push({
task,
@ -103,39 +104,56 @@ export class TaskOrchestrator {
}, []);
}
private spawnProcess(task: Task) {
private forkProcess(task: Task) {
const taskOutputs = getOutputs(this.projectGraph.nodes, task);
return new Promise(res => {
const command = this.isYarn ? 'yarn' : 'npm';
const commandArgs = this.isYarn
? getCommand(this.cli, this.isYarn, task)
: ['run', ...getCommand(this.cli, this.isYarn, task)];
const p = spawn(command, commandArgs, {
stdio: [process.stdin, 'pipe', 'pipe'],
env: { ...process.env, FORCE_COLOR: 'true' }
});
let out = [];
p.stdout.on('data', data => {
out.push(data);
process.stdout.write(data);
});
p.stderr.on('data', data => {
out.push(data);
process.stderr.write(data);
});
p.on('close', code => {
if (code === 0) {
this.cache.put(task, out.join(''), taskOutputs).then(() => {
res(code);
return this.cache.temporaryOutputPath(task).then(outputPath => {
return new Promise((res, rej) => {
try {
const p = fork(this.getCommand(), this.getCommandArgs(task), {
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
env: { ...process.env, NX_TERMINAL_OUTPUT_PATH: outputPath }
});
} else {
res(code);
p.on('close', code => {
if (code === 0) {
this.cache.put(task, outputPath, taskOutputs).then(() => {
res(code);
});
} else {
res(code);
}
});
} catch (e) {
console.error(e);
rej(e);
}
});
});
}
private getCommand() {
return path.join(
this.workspaceRoot,
'node_modules',
'@nrwl',
'cli',
'lib',
'run-cli.js'
);
}
private getCommandArgs(task: Task) {
const args = Object.entries(task.overrides || {}).map(
([prop, value]) => `--${prop}=${value}`
);
const config = task.target.configuration
? `:${task.target.configuration}`
: '';
return [
'run',
`${task.target.project}:${task.target.target}${config}`,
...args
];
}
}

View File

@ -5,9 +5,6 @@ import {
TaskCompleteEvent,
TasksRunner
} from './tasks-runner';
import { output } from '../utils/output';
import { readJsonFile } from '../utils/fileutils';
import { cliCommand } from '../core/file-utils';
import { ProjectGraph } from '../core/project-graph';
import { NxJson } from '../core/shared-interfaces';
import { TaskOrderer } from './task-orderer';
@ -52,7 +49,6 @@ async function runAllTasks(
options: DefaultTasksRunnerOptions,
context: { target: string; projectGraph: ProjectGraph; nxJson: NxJson }
): Promise<Array<{ task: Task; type: any; success: boolean }>> {
assertPackageJsonScriptExists();
const stages = new TaskOrderer(
context.target,
context.projectGraph
@ -91,25 +87,4 @@ function tasksToStatuses(tasks: Task[], success: boolean) {
}));
}
function assertPackageJsonScriptExists() {
const cli = cliCommand();
// Make sure the `package.json` has the `nx: "nx"`
const packageJson = readJsonFile('./package.json');
if (!packageJson.scripts || !packageJson.scripts[cli]) {
output.error({
title: `The "scripts" section of your 'package.json' must contain "${cli}": "${cli}"`,
bodyLines: [
output.colors.gray('...'),
' "scripts": {',
output.colors.gray(' ...'),
` "${cli}": "${cli}"`,
output.colors.gray(' ...'),
' }',
output.colors.gray('...')
]
});
return process.exit(1);
}
}
export default tasksRunnerV2;

View File

@ -19,4 +19,4 @@ jest --maxWorkers=1 ./build/e2e/run-many.test.js &&
jest --maxWorkers=1 ./build/e2e/storybook.test.js &&
jest --maxWorkers=1 ./build/e2e/upgrade-module.test.js &&
jest --maxWorkers=1 ./build/e2e/web.test.js &&
jest --maxWorkers=1 ./build/e2e/default-tasks-runner.test.js
jest --maxWorkers=1 ./build/e2e/tasks-runner-v2.test.js