diff --git a/e2e/affected.test.ts b/e2e/affected.test.ts index 49c01adc08..0b5c1d0cfa 100644 --- a/e2e/affected.test.ts +++ b/e2e/affected.test.ts @@ -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(); }); diff --git a/e2e/default-tasks-runner.test.ts b/e2e/tasks-runner-v2.test.ts similarity index 94% rename from e2e/default-tasks-runner.test.ts rename to e2e/tasks-runner-v2.test.ts index e185e1973d..d9ea47acf0 100644 --- a/e2e/default-tasks-runner.test.ts +++ b/e2e/tasks-runner-v2.test.ts @@ -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( diff --git a/packages/cli/lib/init-local.ts b/packages/cli/lib/init-local.ts index cb67c7398a..f8e8d93b9f 100644 --- a/packages/cli/lib/init-local.ts +++ b/packages/cli/lib/init-local.ts @@ -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; } } diff --git a/packages/cli/lib/parse-run-one-options.spec.ts b/packages/cli/lib/parse-run-one-options.spec.ts new file mode 100644 index 0000000000..d69c3b1909 --- /dev/null +++ b/packages/cli/lib/parse-run-one-options.spec.ts @@ -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); + }); +}); diff --git a/packages/cli/lib/parse-run-one-options.ts b/packages/cli/lib/parse-run-one-options.ts new file mode 100644 index 0000000000..0f64a423bd --- /dev/null +++ b/packages/cli/lib/parse-run-one-options.ts @@ -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; +} diff --git a/packages/cli/lib/run-cli.ts b/packages/cli/lib/run-cli.ts new file mode 100644 index 0000000000..0e16cdcda4 --- /dev/null +++ b/packages/cli/lib/run-cli.ts @@ -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('')); + } + }); +} diff --git a/packages/insights/src/insights-task-runner.ts b/packages/insights/src/insights-task-runner.ts index 16eecd1bd8..ef55d4a6d1 100644 --- a/packages/insights/src/insights-task-runner.ts +++ b/packages/insights/src/insights-task-runner.ts @@ -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); } diff --git a/packages/tao/src/commands/run.ts b/packages/tao/src/commands/run.ts index 33b8d8e934..3a94e7052e 100644 --- a/packages/tao/src/commands/run.ts +++ b/packages/tao/src/commands/run.ts @@ -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['_']; diff --git a/packages/workspace/index.ts b/packages/workspace/index.ts index 1a7f5dea23..48b0a8bdde 100644 --- a/packages/workspace/index.ts +++ b/packages/workspace/index.ts @@ -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 { diff --git a/packages/workspace/src/command-line/affected.ts b/packages/workspace/src/command-line/affected.ts index 90614b5eb4..f92dc5265e 100644 --- a/packages/workspace/src/command-line/affected.ts +++ b/packages/workspace/src/command-line/affected.ts @@ -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; } diff --git a/packages/workspace/src/command-line/nx-commands.ts b/packages/workspace/src/command-line/nx-commands.ts index a3342c9075..58a402cd5c 100644 --- a/packages/workspace/src/command-line/nx-commands.ts +++ b/packages/workspace/src/command-line/nx-commands.ts @@ -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 diff --git a/packages/workspace/src/command-line/run-many.ts b/packages/workspace/src/command-line/run-many.ts index 91f10b3bb9..a5690d4981 100644 --- a/packages/workspace/src/command-line/run-many.ts +++ b/packages/workspace/src/command-line/run-many.ts @@ -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) { diff --git a/packages/workspace/src/command-line/run-one.ts b/packages/workspace/src/command-line/run-one.ts new file mode 100644 index 0000000000..0a3b2bd777 --- /dev/null +++ b/packages/workspace/src/command-line/run-one.ts @@ -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() + ); +} diff --git a/packages/workspace/src/command-line/supported-nx-commands.ts b/packages/workspace/src/command-line/supported-nx-commands.ts new file mode 100644 index 0000000000..a93e519d64 --- /dev/null +++ b/packages/workspace/src/command-line/supported-nx-commands.ts @@ -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' +]; diff --git a/packages/workspace/src/tasks-runner/cache.ts b/packages/workspace/src/tasks-runner/cache.ts index 221fe4cf74..d41bd21de3 100644 --- a/packages/workspace/src/tasks-runner/cache.ts +++ b/packages/workspace/src/tasks-runner/cache.ts @@ -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 { - 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; + } } diff --git a/packages/workspace/src/tasks-runner/default-tasks-runner.ts b/packages/workspace/src/tasks-runner/default-tasks-runner.ts index 49fa66ffad..9ebbef4db9 100644 --- a/packages/workspace/src/tasks-runner/default-tasks-runner.ts +++ b/packages/workspace/src/tasks-runner/default-tasks-runner.ts @@ -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; diff --git a/packages/workspace/src/tasks-runner/empty-reporter.ts b/packages/workspace/src/tasks-runner/empty-reporter.ts new file mode 100644 index 0000000000..5e73c4a115 --- /dev/null +++ b/packages/workspace/src/tasks-runner/empty-reporter.ts @@ -0,0 +1,5 @@ +export class EmptyReporter { + beforeRun() {} + + printResults() {} +} diff --git a/packages/workspace/src/tasks-runner/run-command.ts b/packages/workspace/src/tasks-runner/run-command.ts index 99fc5b2fc8..ee820266b5 100644 --- a/packages/workspace/src/tasks-runner/run-command.ts +++ b/packages/workspace/src/tasks-runner/run-command.ts @@ -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( 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 }; } diff --git a/packages/workspace/src/tasks-runner/task-orchestrator.ts b/packages/workspace/src/tasks-runner/task-orchestrator.ts index d55c2cced7..0d200242ff 100644 --- a/packages/workspace/src/tasks-runner/task-orchestrator.ts +++ b/packages/workspace/src/tasks-runner/task-orchestrator.ts @@ -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 + ]; + } } diff --git a/packages/workspace/src/tasks-runner/tasks-runner-v2.ts b/packages/workspace/src/tasks-runner/tasks-runner-v2.ts index 4bf5a5c065..988bc4327e 100644 --- a/packages/workspace/src/tasks-runner/tasks-runner-v2.ts +++ b/packages/workspace/src/tasks-runner/tasks-runner-v2.ts @@ -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> { - 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; diff --git a/scripts/e2e-ci2.sh b/scripts/e2e-ci2.sh index d303bdbf77..92231e322a 100755 --- a/scripts/e2e-ci2.sh +++ b/scripts/e2e-ci2.sh @@ -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