feat(nx): add generic task execution

This commit is contained in:
Jason Jean 2019-09-13 17:04:00 -04:00 committed by Victor Savkin
parent 34a887a93f
commit ed546337f1
43 changed files with 1177 additions and 223 deletions

View File

@ -40,6 +40,10 @@ All projects
Base of the current branch (usually master)
### configuration
This is the configuration to use when performing tasks on projects
### exclude
Default: ``
@ -68,6 +72,10 @@ Isolate projects which previously failed
Produces a plain output for affected:apps and affected:libs
### runner
This is the name of the tasks runner configured in nx.json
### uncommitted
Uncommitted changes

View File

@ -58,6 +58,10 @@ All projects
Base of the current branch (usually master)
### configuration
This is the configuration to use when performing tasks on projects
### exclude
Default: ``
@ -98,6 +102,10 @@ Parallelize the command
Produces a plain output for affected:apps and affected:libs
### runner
This is the name of the tasks runner configured in nx.json
### uncommitted
Uncommitted changes

View File

@ -46,6 +46,10 @@ All projects
Base of the current branch (usually master)
### configuration
This is the configuration to use when performing tasks on projects
### exclude
Default: ``
@ -78,6 +82,10 @@ Isolate projects which previously failed
Produces a plain output for affected:apps and affected:libs
### runner
This is the name of the tasks runner configured in nx.json
### uncommitted
Uncommitted changes

View File

@ -58,6 +58,10 @@ All projects
Base of the current branch (usually master)
### configuration
This is the configuration to use when performing tasks on projects
### exclude
Default: ``
@ -98,6 +102,10 @@ Parallelize the command
Produces a plain output for affected:apps and affected:libs
### runner
This is the name of the tasks runner configured in nx.json
### uncommitted
Uncommitted changes

View File

@ -40,6 +40,10 @@ All projects
Base of the current branch (usually master)
### configuration
This is the configuration to use when performing tasks on projects
### exclude
Default: ``
@ -68,6 +72,10 @@ Isolate projects which previously failed
Produces a plain output for affected:apps and affected:libs
### runner
This is the name of the tasks runner configured in nx.json
### uncommitted
Uncommitted changes

View File

@ -58,6 +58,10 @@ All projects
Base of the current branch (usually master)
### configuration
This is the configuration to use when performing tasks on projects
### exclude
Default: ``
@ -98,6 +102,10 @@ Parallelize the command
Produces a plain output for affected:apps and affected:libs
### runner
This is the name of the tasks runner configured in nx.json
### uncommitted
Uncommitted changes

View File

@ -58,6 +58,10 @@ All projects
Base of the current branch (usually master)
### configuration
This is the configuration to use when performing tasks on projects
### exclude
Default: ``
@ -98,6 +102,10 @@ Parallelize the command
Produces a plain output for affected:apps and affected:libs
### runner
This is the name of the tasks runner configured in nx.json
### uncommitted
Uncommitted changes

View File

@ -64,6 +64,10 @@ All projects
Base of the current branch (usually master)
### configuration
This is the configuration to use when performing tasks on projects
### exclude
Default: ``
@ -104,6 +108,10 @@ Parallelize the command
Produces a plain output for affected:apps and affected:libs
### runner
This is the name of the tasks runner configured in nx.json
### target
Task to run for affected projects

View File

@ -22,6 +22,10 @@ All projects
Base of the current branch (usually master)
### configuration
This is the configuration to use when performing tasks on projects
### exclude
Default: ``
@ -50,6 +54,10 @@ Isolate projects which previously failed
Produces a plain output for affected:apps and affected:libs
### runner
This is the name of the tasks runner configured in nx.json
### uncommitted
Uncommitted changes

View File

@ -22,6 +22,10 @@ All projects
Base of the current branch (usually master)
### configuration
This is the configuration to use when performing tasks on projects
### exclude
Default: ``
@ -50,6 +54,10 @@ Isolate projects which previously failed
Produces a plain output for affected:apps and affected:libs
### runner
This is the name of the tasks runner configured in nx.json
### uncommitted
Uncommitted changes

View File

@ -40,6 +40,10 @@ All projects
Base of the current branch (usually master)
### configuration
This is the configuration to use when performing tasks on projects
### exclude
Default: ``
@ -68,6 +72,10 @@ Isolate projects which previously failed
Produces a plain output for affected:apps and affected:libs
### runner
This is the name of the tasks runner configured in nx.json
### uncommitted
Uncommitted changes

View File

@ -58,6 +58,10 @@ All projects
Base of the current branch (usually master)
### configuration
This is the configuration to use when performing tasks on projects
### exclude
Default: ``
@ -98,6 +102,10 @@ Parallelize the command
Produces a plain output for affected:apps and affected:libs
### runner
This is the name of the tasks runner configured in nx.json
### uncommitted
Uncommitted changes

View File

@ -46,6 +46,10 @@ All projects
Base of the current branch (usually master)
### configuration
This is the configuration to use when performing tasks on projects
### exclude
Default: ``
@ -78,6 +82,10 @@ Isolate projects which previously failed
Produces a plain output for affected:apps and affected:libs
### runner
This is the name of the tasks runner configured in nx.json
### uncommitted
Uncommitted changes

View File

@ -58,6 +58,10 @@ All projects
Base of the current branch (usually master)
### configuration
This is the configuration to use when performing tasks on projects
### exclude
Default: ``
@ -98,6 +102,10 @@ Parallelize the command
Produces a plain output for affected:apps and affected:libs
### runner
This is the name of the tasks runner configured in nx.json
### uncommitted
Uncommitted changes

View File

@ -40,6 +40,10 @@ All projects
Base of the current branch (usually master)
### configuration
This is the configuration to use when performing tasks on projects
### exclude
Default: ``
@ -68,6 +72,10 @@ Isolate projects which previously failed
Produces a plain output for affected:apps and affected:libs
### runner
This is the name of the tasks runner configured in nx.json
### uncommitted
Uncommitted changes

View File

@ -58,6 +58,10 @@ All projects
Base of the current branch (usually master)
### configuration
This is the configuration to use when performing tasks on projects
### exclude
Default: ``
@ -98,6 +102,10 @@ Parallelize the command
Produces a plain output for affected:apps and affected:libs
### runner
This is the name of the tasks runner configured in nx.json
### uncommitted
Uncommitted changes

View File

@ -58,6 +58,10 @@ All projects
Base of the current branch (usually master)
### configuration
This is the configuration to use when performing tasks on projects
### exclude
Default: ``
@ -98,6 +102,10 @@ Parallelize the command
Produces a plain output for affected:apps and affected:libs
### runner
This is the name of the tasks runner configured in nx.json
### uncommitted
Uncommitted changes

View File

@ -64,6 +64,10 @@ All projects
Base of the current branch (usually master)
### configuration
This is the configuration to use when performing tasks on projects
### exclude
Default: ``
@ -104,6 +108,10 @@ Parallelize the command
Produces a plain output for affected:apps and affected:libs
### runner
This is the name of the tasks runner configured in nx.json
### target
Task to run for affected projects

View File

@ -22,6 +22,10 @@ All projects
Base of the current branch (usually master)
### configuration
This is the configuration to use when performing tasks on projects
### exclude
Default: ``
@ -50,6 +54,10 @@ Isolate projects which previously failed
Produces a plain output for affected:apps and affected:libs
### runner
This is the name of the tasks runner configured in nx.json
### uncommitted
Uncommitted changes

View File

@ -22,6 +22,10 @@ All projects
Base of the current branch (usually master)
### configuration
This is the configuration to use when performing tasks on projects
### exclude
Default: ``
@ -50,6 +54,10 @@ Isolate projects which previously failed
Produces a plain output for affected:apps and affected:libs
### runner
This is the name of the tasks runner configured in nx.json
### uncommitted
Uncommitted changes

View File

@ -40,6 +40,10 @@ All projects
Base of the current branch (usually master)
### configuration
This is the configuration to use when performing tasks on projects
### exclude
Default: ``
@ -68,6 +72,10 @@ Isolate projects which previously failed
Produces a plain output for affected:apps and affected:libs
### runner
This is the name of the tasks runner configured in nx.json
### uncommitted
Uncommitted changes

View File

@ -58,6 +58,10 @@ All projects
Base of the current branch (usually master)
### configuration
This is the configuration to use when performing tasks on projects
### exclude
Default: ``
@ -98,6 +102,10 @@ Parallelize the command
Produces a plain output for affected:apps and affected:libs
### runner
This is the name of the tasks runner configured in nx.json
### uncommitted
Uncommitted changes

View File

@ -46,6 +46,10 @@ All projects
Base of the current branch (usually master)
### configuration
This is the configuration to use when performing tasks on projects
### exclude
Default: ``
@ -78,6 +82,10 @@ Isolate projects which previously failed
Produces a plain output for affected:apps and affected:libs
### runner
This is the name of the tasks runner configured in nx.json
### uncommitted
Uncommitted changes

View File

@ -58,6 +58,10 @@ All projects
Base of the current branch (usually master)
### configuration
This is the configuration to use when performing tasks on projects
### exclude
Default: ``
@ -98,6 +102,10 @@ Parallelize the command
Produces a plain output for affected:apps and affected:libs
### runner
This is the name of the tasks runner configured in nx.json
### uncommitted
Uncommitted changes

View File

@ -40,6 +40,10 @@ All projects
Base of the current branch (usually master)
### configuration
This is the configuration to use when performing tasks on projects
### exclude
Default: ``
@ -68,6 +72,10 @@ Isolate projects which previously failed
Produces a plain output for affected:apps and affected:libs
### runner
This is the name of the tasks runner configured in nx.json
### uncommitted
Uncommitted changes

View File

@ -58,6 +58,10 @@ All projects
Base of the current branch (usually master)
### configuration
This is the configuration to use when performing tasks on projects
### exclude
Default: ``
@ -98,6 +102,10 @@ Parallelize the command
Produces a plain output for affected:apps and affected:libs
### runner
This is the name of the tasks runner configured in nx.json
### uncommitted
Uncommitted changes

View File

@ -58,6 +58,10 @@ All projects
Base of the current branch (usually master)
### configuration
This is the configuration to use when performing tasks on projects
### exclude
Default: ``
@ -98,6 +102,10 @@ Parallelize the command
Produces a plain output for affected:apps and affected:libs
### runner
This is the name of the tasks runner configured in nx.json
### uncommitted
Uncommitted changes

View File

@ -64,6 +64,10 @@ All projects
Base of the current branch (usually master)
### configuration
This is the configuration to use when performing tasks on projects
### exclude
Default: ``
@ -104,6 +108,10 @@ Parallelize the command
Produces a plain output for affected:apps and affected:libs
### runner
This is the name of the tasks runner configured in nx.json
### target
Task to run for affected projects

View File

@ -22,6 +22,10 @@ All projects
Base of the current branch (usually master)
### configuration
This is the configuration to use when performing tasks on projects
### exclude
Default: ``
@ -50,6 +54,10 @@ Isolate projects which previously failed
Produces a plain output for affected:apps and affected:libs
### runner
This is the name of the tasks runner configured in nx.json
### uncommitted
Uncommitted changes

View File

@ -22,6 +22,10 @@ All projects
Base of the current branch (usually master)
### configuration
This is the configuration to use when performing tasks on projects
### exclude
Default: ``
@ -50,6 +54,10 @@ Isolate projects which previously failed
Produces a plain output for affected:apps and affected:libs
### runner
This is the name of the tasks runner configured in nx.json
### uncommitted
Uncommitted changes

View File

@ -143,13 +143,14 @@ forEachCli(() => {
// affected:build should pass non-nx flags to the CLI
const buildWithFlags = runCommand(
`npm run affected:build -- --files="libs/${mylib}/src/index.ts" --stats-json`
`npm run affected:build -- --files="libs/${mylib}/src/index.ts" -- --stats-json`
);
expect(buildWithFlags).toContain(`Running target build for projects:`);
expect(buildWithFlags).toContain(`- ${myapp}`);
expect(buildWithFlags).toContain(`- ${mypublishablelib}`);
expect(buildWithFlags).toContain('With flags: --stats-json=true');
expect(buildWithFlags).toContain('With flags:');
expect(buildWithFlags).toContain('--stats-json=true');
if (supportUi()) {
const e2e = runCommand(
@ -221,7 +222,8 @@ forEachCli(() => {
const lintWithJsonFormating = runCommand(
`npm run affected:lint -- --files="libs/${mylib}/src/index.ts" -- --format json`
);
expect(lintWithJsonFormating).toContain('With flags: --format json');
expect(lintWithJsonFormating).toContain('With flags:');
expect(lintWithJsonFormating).toContain('--format=json');
const unitTestsExcluded = runCommand(
`npm run affected:test -- --files="libs/${mylib}/src/index.ts" --exclude=${myapp},${mypublishablelib}`

View File

@ -3,11 +3,11 @@ import {
getAffectedApps,
getAffectedLibs,
getAffectedProjects,
getAffectedProjectsWithTarget,
getAffectedProjectsWithTargetAndConfiguration,
getAllApps,
getAllLibs,
getAllProjects,
getAllProjectsWithTarget
getAllProjectsWithTargetAndConfiguration
} from './affected-apps';
import { DependencyType } from './deps-calculator';
@ -23,7 +23,11 @@ describe('affected-apps', () => {
architect: {
lint: {},
test: {},
build: {}
build: {
configurations: {
production: {}
}
}
}
},
app2: {
@ -196,11 +200,46 @@ describe('affected-apps', () => {
});
});
describe('getAffectedProjectsWithTarget', () => {
describe('getAffectedProjectsWithTargetAndConfiguration', () => {
it('should get none if no projects are affected', () => {
expect(getAffectedProjectsWithTarget(affectedMetadata, 'test')).toEqual(
[]
);
expect(
getAffectedProjectsWithTargetAndConfiguration(affectedMetadata, 'test')
).toEqual([]);
});
it('should find affected projects that can be built', () => {
projectStates.lib1 = {
affected: true,
touched: true
};
projectStates.app1.affected = true;
projectStates['app1-e2e'].affected = true;
projectStates.app2.affected = true;
projectStates['customName-e2e'].affected = true;
expect(
getAffectedProjectsWithTargetAndConfiguration(affectedMetadata, 'build')
).toEqual([
affectedMetadata.dependencyGraph.projects.app1,
affectedMetadata.dependencyGraph.projects.app2
]);
});
it('should find affected projects that can be built for production', () => {
projectStates.lib1 = {
affected: true,
touched: true
};
projectStates.app1.affected = true;
projectStates['app1-e2e'].affected = true;
projectStates.app2.affected = true;
projectStates['customName-e2e'].affected = true;
expect(
getAffectedProjectsWithTargetAndConfiguration(
affectedMetadata,
'build',
'production'
)
).toEqual([affectedMetadata.dependencyGraph.projects.app1]);
});
it('should find affected projects that can be linted', () => {
@ -212,12 +251,14 @@ describe('affected-apps', () => {
projectStates['app1-e2e'].affected = true;
projectStates.app2.affected = true;
projectStates['customName-e2e'].affected = true;
expect(getAffectedProjectsWithTarget(affectedMetadata, 'lint')).toEqual([
'lib1',
'app1',
'app1-e2e',
'app2',
'customName-e2e'
expect(
getAffectedProjectsWithTargetAndConfiguration(affectedMetadata, 'lint')
).toEqual([
affectedMetadata.dependencyGraph.projects.lib1,
affectedMetadata.dependencyGraph.projects.app1,
affectedMetadata.dependencyGraph.projects['app1-e2e'],
affectedMetadata.dependencyGraph.projects.app2,
affectedMetadata.dependencyGraph.projects['customName-e2e']
]);
});
@ -230,10 +271,12 @@ describe('affected-apps', () => {
projectStates['app1-e2e'].affected = true;
projectStates.app2.affected = true;
projectStates['customName-e2e'].affected = true;
expect(getAffectedProjectsWithTarget(affectedMetadata, 'test')).toEqual([
'lib1',
'app1',
'app2'
expect(
getAffectedProjectsWithTargetAndConfiguration(affectedMetadata, 'test')
).toEqual([
affectedMetadata.dependencyGraph.projects.lib1,
affectedMetadata.dependencyGraph.projects.app1,
affectedMetadata.dependencyGraph.projects.app2
]);
});
@ -246,9 +289,11 @@ describe('affected-apps', () => {
projectStates['app1-e2e'].affected = true;
projectStates.app2.affected = true;
projectStates['customName-e2e'].affected = true;
expect(getAffectedProjectsWithTarget(affectedMetadata, 'e2e')).toEqual([
'app1-e2e',
'customName-e2e'
expect(
getAffectedProjectsWithTargetAndConfiguration(affectedMetadata, 'e2e')
).toEqual([
affectedMetadata.dependencyGraph.projects['app1-e2e'],
affectedMetadata.dependencyGraph.projects['customName-e2e']
]);
});
});
@ -280,36 +325,54 @@ describe('affected-apps', () => {
describe('getAllProjectsWithTarget', () => {
it('should get all projects that can be linted', () => {
expect(getAllProjectsWithTarget(affectedMetadata, 'lint')).toEqual([
'lib1',
'app1',
'app1-e2e',
'lib2',
'app2',
'customName-e2e'
expect(
getAllProjectsWithTargetAndConfiguration(affectedMetadata, 'lint')
).toEqual([
affectedMetadata.dependencyGraph.projects.lib1,
affectedMetadata.dependencyGraph.projects.app1,
affectedMetadata.dependencyGraph.projects['app1-e2e'],
affectedMetadata.dependencyGraph.projects.lib2,
affectedMetadata.dependencyGraph.projects.app2,
affectedMetadata.dependencyGraph.projects['customName-e2e']
]);
});
it('should get all projects that can be tested', () => {
expect(getAllProjectsWithTarget(affectedMetadata, 'test')).toEqual([
'lib1',
'app1',
'lib2',
'app2'
expect(
getAllProjectsWithTargetAndConfiguration(affectedMetadata, 'test')
).toEqual([
affectedMetadata.dependencyGraph.projects.lib1,
affectedMetadata.dependencyGraph.projects.app1,
affectedMetadata.dependencyGraph.projects.lib2,
affectedMetadata.dependencyGraph.projects.app2
]);
});
it('should get all projects that can be built', () => {
expect(getAllProjectsWithTarget(affectedMetadata, 'build')).toEqual([
'app1',
'app2'
expect(
getAllProjectsWithTargetAndConfiguration(affectedMetadata, 'build')
).toEqual([
affectedMetadata.dependencyGraph.projects.app1,
affectedMetadata.dependencyGraph.projects.app2
]);
});
it('should get all projects that can be built for production', () => {
expect(
getAllProjectsWithTargetAndConfiguration(
affectedMetadata,
'build',
'production'
)
).toEqual([affectedMetadata.dependencyGraph.projects.app1]);
});
it('should get all projects that can be e2e-tested', () => {
expect(getAllProjectsWithTarget(affectedMetadata, 'e2e')).toEqual([
'app1-e2e',
'customName-e2e'
expect(
getAllProjectsWithTargetAndConfiguration(affectedMetadata, 'e2e')
).toEqual([
affectedMetadata.dependencyGraph.projects['app1-e2e'],
affectedMetadata.dependencyGraph.projects['customName-e2e']
]);
});
});

View File

@ -26,16 +26,17 @@ export function getAffectedProjects(
).map(project => project.name);
}
export function getAffectedProjectsWithTarget(
export function getAffectedProjectsWithTargetAndConfiguration(
affectedMetadata: AffectedMetadata,
target: string
): string[] {
target: string,
configuration?: string
): ProjectNode[] {
return filterAffectedMetadata(
affectedMetadata,
project =>
affectedMetadata.projectStates[project.name].affected &&
project.architect[target]
).map(project => project.name);
projectHasTargetAndConfiguration(project, target, configuration)
);
}
export function getAllApps(affectedMetadata: AffectedMetadata): string[] {
@ -58,14 +59,33 @@ export function getAllProjects(affectedMetadata: AffectedMetadata): string[] {
);
}
export function getAllProjectsWithTarget(
export function getAllProjectsWithTargetAndConfiguration(
affectedMetadata: AffectedMetadata,
target: string
): string[] {
return filterAffectedMetadata(
affectedMetadata,
project => project.architect[target]
).map(project => project.name);
target: string,
configuration?: string
): ProjectNode[] {
return filterAffectedMetadata(affectedMetadata, project =>
projectHasTargetAndConfiguration(project, target, configuration)
);
}
export function projectHasTargetAndConfiguration(
project: ProjectNode,
target: string,
configuration?: string
) {
if (!project.architect[target]) {
return false;
}
if (!configuration) {
return !!project.architect[target];
} else {
return (
project.architect[target].configurations &&
project.architect[target].configurations[configuration]
);
}
}
function filterAffectedMetadata(

View File

@ -0,0 +1,153 @@
import { processArgs } from './affected';
import { NxJson } from './shared';
import defaultTasksRunner from '../tasks-runner/default-tasks-runner';
import { TasksRunner } from '../tasks-runner/tasks-runner';
describe('processArgs', () => {
let nxJson: NxJson;
let mockRunner: TasksRunner;
beforeEach(() => {
nxJson = {
npmScope: 'proj',
projects: {}
};
mockRunner = jest.fn();
});
it('should process nx specific arguments as affected args', () => {
expect(
processArgs(
{
files: [''],
notNxArg: true,
_: ['--override'],
$0: ''
},
nxJson
).affectedArgs
).toEqual({
files: ['']
});
});
it('should process non nx specific arguments as tasks runner args', () => {
expect(
processArgs(
{
files: [''],
notNxArg: true,
_: ['--override'],
$0: ''
},
nxJson
).tasksRunnerOptions
).toEqual({
notNxArg: true
});
});
it('should process delimited args as task overrides', () => {
expect(
processArgs(
{
files: [''],
notNxArg: true,
_: ['', '--override'],
$0: ''
},
nxJson
).taskOverrides
).toEqual({
override: true
});
});
it('should get a default tasks runner', () => {
expect(
processArgs(
{
files: [''],
notNxArg: true,
_: ['', '--override'],
$0: ''
},
nxJson
).tasksRunner
).toEqual(defaultTasksRunner);
});
it('should get a custom tasks runner', () => {
jest.mock('custom-runner', () => mockRunner, {
virtual: true
});
nxJson.tasksRunnerOptions = {
custom: {
runner: 'custom-runner'
}
};
expect(
processArgs(
{
files: [''],
notNxArg: true,
runner: 'custom',
_: ['', '--override'],
$0: ''
},
nxJson
).tasksRunner
).toEqual(mockRunner);
});
it('should get a custom tasks runner with options', () => {
jest.mock('custom-runner', () => mockRunner, {
virtual: true
});
nxJson.tasksRunnerOptions = {
custom: {
runner: 'custom-runner',
options: {
runnerOption: 'runner-option'
}
}
};
expect(
processArgs(
{
files: [''],
notNxArg: true,
runner: 'custom',
_: ['', '--override'],
$0: ''
},
nxJson
).tasksRunnerOptions
).toEqual({
runnerOption: 'runner-option',
notNxArg: true
});
});
it('should get a custom defined default tasks runner', () => {
jest.mock('custom-default-runner', () => mockRunner, {
virtual: true
});
nxJson.tasksRunnerOptions = {
default: {
runner: 'custom-default-runner'
}
};
expect(
processArgs(
{
files: [''],
notNxArg: true,
_: ['', '--override'],
$0: ''
},
nxJson
).tasksRunner
).toEqual(mockRunner);
});
});

View File

@ -1,28 +1,39 @@
import * as fs from 'fs';
import * as path from 'path';
import * as runAll from 'npm-run-all';
import * as yargs from 'yargs';
import * as yargsParser from 'yargs-parser';
import { join } from 'path';
import {
parseFiles,
readWorkspaceJson,
printArgsWarning,
cliCommand,
getAffectedMetadata
getAffectedMetadata,
AffectedMetadata,
readNxJson,
ProjectNode,
NxJson
} from './shared';
import {
getAffectedApps,
getAffectedLibs,
getAffectedProjects,
getAffectedProjectsWithTarget,
getAffectedProjectsWithTargetAndConfiguration,
getAllApps,
getAllLibs,
getAllProjects,
getAllProjectsWithTarget
getAllProjectsWithTargetAndConfiguration,
projectHasTargetAndConfiguration
} from './affected-apps';
import { generateGraph } from './dep-graph';
import { WorkspaceResults } from './workspace-results';
import { output } from './output';
import {
AffectedEventType,
Task,
TaskCompleteEvent,
TasksRunner
} from '../tasks-runner/tasks-runner';
import { appRootPath } from '../utils/app-root';
import { defaultTasksRunner } from '../tasks-runner/default-tasks-runner';
import { isRelativePath } from '../utils/fileutils';
export interface YargsAffectedOptions
extends yargs.Arguments,
@ -30,6 +41,8 @@ export interface YargsAffectedOptions
export interface AffectedOptions {
target?: string;
configuration?: string;
runner?: string;
parallel?: boolean;
maxParallel?: number;
untracked?: boolean;
@ -49,14 +62,15 @@ export interface AffectedOptions {
plain?: boolean;
}
const commonCommands = ['build', 'test', 'lint', 'e2e'];
interface ProcessedArgs {
affectedArgs: YargsAffectedOptions;
taskOverrides: any;
tasksRunnerOptions: any;
tasksRunner: TasksRunner;
}
export function affected(parsedArgs: YargsAffectedOptions): void {
const target = parsedArgs.target;
const rest: string[] = [
...parsedArgs._.slice(1),
...filterNxSpecificArgs(parsedArgs)
];
const workspaceResults = new WorkspaceResults(target);
@ -70,10 +84,10 @@ export function affected(parsedArgs: YargsAffectedOptions): void {
? getAllApps(affectedMetadata)
: getAffectedApps(affectedMetadata)
)
.filter(app => !parsedArgs.exclude.includes(app))
.filter(affectedApp => !parsedArgs.exclude.includes(affectedApp))
.filter(
project =>
!parsedArgs.onlyFailed || !workspaceResults.getResult(project)
affectedApp =>
!parsedArgs.onlyFailed || !workspaceResults.getResult(affectedApp)
);
if (parsedArgs.plain) {
console.log(apps.join(' '));
@ -92,10 +106,10 @@ export function affected(parsedArgs: YargsAffectedOptions): void {
? getAllLibs(affectedMetadata)
: getAffectedLibs(affectedMetadata)
)
.filter(app => !parsedArgs.exclude.includes(app))
.filter(affectedLib => !parsedArgs.exclude.includes(affectedLib))
.filter(
project =>
!parsedArgs.onlyFailed || !workspaceResults.getResult(project)
affectedLib =>
!parsedArgs.onlyFailed || !workspaceResults.getResult(affectedLib)
);
if (parsedArgs.plain) {
@ -125,17 +139,28 @@ export function affected(parsedArgs: YargsAffectedOptions): void {
break;
}
default: {
const nxJson = readNxJson();
const processedArgs = processArgs(parsedArgs, nxJson);
const projects = (parsedArgs.all
? getAllProjectsWithTarget(affectedMetadata, target)
: getAffectedProjectsWithTarget(affectedMetadata, target)
? getAllProjectsWithTargetAndConfiguration(
affectedMetadata,
target,
processedArgs.affectedArgs.configuration
)
: getAffectedProjectsWithTargetAndConfiguration(
affectedMetadata,
target,
processedArgs.affectedArgs.configuration
)
)
.filter(project => !parsedArgs.exclude.includes(project))
.filter(project => !parsedArgs.exclude.includes(project.name))
.filter(
project =>
!parsedArgs.onlyFailed || !workspaceResults.getResult(project)
!parsedArgs.onlyFailed ||
!workspaceResults.getResult(project.name)
);
printArgsWarning(parsedArgs);
runCommand(target, projects, parsedArgs, rest, workspaceResults);
runCommand(projects, affectedMetadata, processedArgs, workspaceResults);
break;
}
}
@ -158,145 +183,202 @@ function printError(e: any, verbose?: boolean) {
}
async function runCommand(
targetName: string,
projects: string[],
parsedArgs: YargsAffectedOptions,
args: string[],
affectedProjects: ProjectNode[],
affectedMetadata: AffectedMetadata,
processedArgs: ProcessedArgs,
workspaceResults: WorkspaceResults
) {
if (projects.length <= 0) {
output.logSingleLine(
`No affected projects to run target "${targetName}" on`
);
const {
affectedArgs,
taskOverrides,
tasksRunnerOptions,
tasksRunner
} = processedArgs;
if (affectedProjects.length <= 0) {
let description = `with "${affectedArgs.target}"`;
if (affectedArgs.configuration) {
description += ` that are configured for "${affectedArgs.configuration}"`;
}
output.logSingleLine(`No projects ${description} were affected`);
return;
}
const cli = cliCommand();
const bodyLines = projects.map(
project => `${output.colors.gray('-')} ${project}`
const bodyLines = affectedProjects.map(
affectedProject => `${output.colors.gray('-')} ${affectedProject.name}`
);
if (args.length > 0) {
if (Object.keys(taskOverrides).length > 0) {
bodyLines.push('');
bodyLines.push(
`${output.colors.gray('With flags:')} ${output.bold(args.join(' '))}`
);
bodyLines.push(`${output.colors.gray('With flags:')}`);
Object.entries(taskOverrides)
.map(([flag, value]) => ` --${flag}=${value}`)
.forEach(arg => bodyLines.push(arg));
}
output.log({
title: `${output.colors.gray(
'Running target'
)} ${targetName} ${output.colors.gray('for projects:')}`,
title: `${output.colors.gray('Running target')} ${
affectedArgs.target
} ${output.colors.gray('for projects:')}`,
bodyLines
});
output.addVerticalSeparator();
const workspaceJson = readWorkspaceJson();
const projectMetadata = new Map<string, any>();
projects.forEach(project => {
projectMetadata.set(project, workspaceJson.projects[project]);
});
// Make sure the `package.json` has the `nx: "nx"` command needed by `npm-run-all`
const packageJson = JSON.parse(
fs.readFileSync('./package.json').toString('utf-8')
);
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);
}
try {
const isYarn = path
.basename(process.env.npm_execpath || 'npm')
.startsWith('yarn');
await runAll(
projects.map(proj => {
return commonCommands.includes(targetName)
? `${cli} ${isYarn ? '' : '--'} ${targetName} ${proj} ${transformArgs(
args,
proj,
projectMetadata.get(proj)
).join(' ')} `
: `${cli} ${
isYarn ? '' : '--'
} run ${proj}:${targetName} ${transformArgs(
args,
proj,
projectMetadata.get(proj)
).join(' ')} `;
}),
{
parallel: parsedArgs.parallel || false,
maxParallel: parsedArgs.maxParallel || 1,
continueOnError: true,
stdin: process.stdin,
stdout: process.stdout,
stderr: process.stderr
}
);
projects.forEach(project => {
workspaceResults.success(project);
});
} catch (e) {
e.results.forEach((result, i) => {
if (result.code === 0) {
workspaceResults.success(projects[i]);
} else {
workspaceResults.fail(projects[i]);
}
});
} finally {
// fix for https://github.com/nrwl/nx/issues/1666
if (process.stdin['unref']) (process.stdin as any).unref();
}
workspaceResults.saveResults();
workspaceResults.printResults(
parsedArgs.onlyFailed,
`Running target "${targetName}" for affected projects succeeded`,
`Running target "${targetName}" for affected projects failed`
const tasks: Task[] = affectedProjects.map(affectedProject =>
createTask({
project: affectedProject,
target: processedArgs.affectedArgs.target,
configuration: processedArgs.affectedArgs.configuration,
overrides: processedArgs.taskOverrides
})
);
if (workspaceResults.hasFailure) {
process.exit(1);
}
}
function transformArgs(
args: string[],
projectName: string,
projectMetadata: any
) {
return args.map(arg => {
const regex = /{project\.([^}]+)}/g;
return arg.replace(regex, (_, group: string) => {
if (group.includes('.')) {
throw new Error('Only top-level properties can be interpolated');
const tasksMap: {
[projectName: string]: { [targetName: string]: Task };
} = {};
Object.entries(affectedMetadata.dependencyGraph.projects).forEach(
([projectName, project]) => {
if (
projectHasTargetAndConfiguration(
project,
processedArgs.affectedArgs.target,
processedArgs.affectedArgs.configuration
)
) {
tasksMap[projectName] = {
[processedArgs.affectedArgs.target]: createTask({
project: project,
target: processedArgs.affectedArgs.target,
configuration: processedArgs.affectedArgs.configuration,
overrides: processedArgs.taskOverrides
})
};
}
}
);
if (group === 'name') {
return projectName;
tasksRunner(tasks, tasksRunnerOptions, {
dependencyGraph: affectedMetadata.dependencyGraph,
tasksMap
}).subscribe({
next: (event: TaskCompleteEvent) => {
switch (event.type) {
case AffectedEventType.TaskComplete: {
workspaceResults.setResult(event.task.target.project, event.success);
}
}
return projectMetadata[group];
});
},
error: console.error,
complete: () => {
// fix for https://github.com/nrwl/nx/issues/1666
if (process.stdin['unref']) (process.stdin as any).unref();
workspaceResults.saveResults();
workspaceResults.printResults(
affectedArgs.onlyFailed,
`Running target "${affectedArgs.target}" for affected projects succeeded`,
`Running target "${affectedArgs.target}" for affected projects failed`
);
if (workspaceResults.hasFailure) {
process.exit(1);
}
}
});
}
function filterNxSpecificArgs(parsedArgs: YargsAffectedOptions): string[] {
function createTask({
project,
target,
configuration,
overrides
}: {
project: ProjectNode;
target: string;
configuration: string;
overrides: Object;
}): Task {
return {
id: getTaskId({
project: project.name,
target: target,
configuration: configuration
}),
target: {
project: project.name,
target,
configuration
},
overrides: interpolateOverrides(overrides, project.name, project)
};
}
function getTaskId({
project,
target,
configuration
}: {
project: string;
target: string;
configuration?: string;
}): string {
let id = project + ':' + target;
if (configuration) {
id += ':' + configuration;
}
return id;
}
function getTasksRunner(
runner: string | undefined,
nxJson: NxJson
): {
tasksRunner: TasksRunner;
options: unknown;
} {
if (!nxJson.tasksRunnerOptions) {
return {
tasksRunner: defaultTasksRunner,
options: {}
};
}
if (!runner && !nxJson.tasksRunnerOptions.default) {
return {
tasksRunner: defaultTasksRunner,
options: {}
};
}
runner = runner || 'default';
if (nxJson.tasksRunnerOptions[runner]) {
let modulePath: string = nxJson.tasksRunnerOptions[runner].runner;
if (isRelativePath(modulePath)) {
modulePath = join(appRootPath, modulePath);
}
return {
tasksRunner: require(modulePath),
options: nxJson.tasksRunnerOptions[runner].options
};
} else {
throw new Error(`Could not find runner configuration for ${runner}`);
}
}
function getNxSpecificOptions(
parsedArgs: YargsAffectedOptions
): YargsAffectedOptions {
const filteredArgs = {};
nxSpecificFlags.forEach(flag => {
filteredArgs[flag] = parsedArgs[flag];
});
return filteredArgs as YargsAffectedOptions;
}
function getNonNxSpecificOptions(
parsedArgs: YargsAffectedOptions
): YargsAffectedOptions {
const filteredArgs = { ...parsedArgs };
// Delete Nx arguments from parsed Args
nxSpecificFlags.forEach(flag => {
@ -308,18 +390,54 @@ function filterNxSpecificArgs(parsedArgs: YargsAffectedOptions): string[] {
// Also remove the node path
delete filteredArgs.$0;
// Re-serialize into a list of args
return Object.keys(filteredArgs).map(filteredArg => {
if (!Array.isArray(filteredArgs[filteredArg])) {
filteredArgs[filteredArg] = [filteredArgs[filteredArg]];
}
return filteredArgs;
}
return filteredArgs[filteredArg]
.map(value => {
return `--${filteredArg}=${value}`;
})
.join(' ');
export function processArgs(
parsedArgs: YargsAffectedOptions,
nxJson: NxJson
): ProcessedArgs {
const affectedArgs = getNxSpecificOptions(parsedArgs);
const { tasksRunner, options: configOptions } = getTasksRunner(
affectedArgs.runner,
nxJson
);
const tasksRunnerOptions = {
...configOptions,
...getNonNxSpecificOptions(parsedArgs)
};
const taskOverrides = yargsParser(parsedArgs._.slice(1));
delete taskOverrides._;
return {
affectedArgs,
taskOverrides,
tasksRunner,
tasksRunnerOptions: tasksRunnerOptions
};
}
function interpolateOverrides<T = any>(
args: T,
projectName: string,
projectMetadata: any
): T {
const interpolatedArgs: T = { ...args };
Object.entries(interpolatedArgs).forEach(([name, value]) => {
if (typeof value === 'string') {
const regex = /{project\.([^}]+)}/g;
interpolatedArgs[name] = value.replace(regex, (_, group: string) => {
if (group.includes('.')) {
throw new Error('Only top-level properties can be interpolated');
}
if (group === 'name') {
return projectName;
}
return projectMetadata[group];
});
}
});
return interpolatedArgs;
}
/**
@ -329,13 +447,12 @@ function filterNxSpecificArgs(parsedArgs: YargsAffectedOptions): string[] {
*/
const dummyOptions: AffectedOptions = {
target: '',
parallel: false,
maxParallel: 3,
'max-parallel': false,
configuration: '',
onlyFailed: false,
'only-failed': false,
untracked: false,
uncommitted: false,
runner: '',
help: false,
version: false,
quiet: false,

View File

@ -254,6 +254,15 @@ function withAffectedOptions(yargs: yargs.Argv): yargs.Argv {
coerce: parseCSV,
default: []
})
.options('runner', {
describe: 'This is the name of the tasks runner configured in nx.json',
type: 'string'
})
.options('configuration', {
describe:
'This is the configuration to use when performing tasks on projects',
type: 'string'
})
.options('only-failed', {
describe: 'Isolate projects which previously failed',
type: 'boolean',

View File

@ -24,6 +24,12 @@ export interface NxJson {
projects: {
[projectName: string]: NxJsonProjectConfig;
};
tasksRunnerOptions?: {
[tasksRunnerName: string]: {
runner: string;
options?: unknown;
};
};
}
export interface NxJsonProjectConfig {
@ -443,6 +449,7 @@ export function createAffectedMetadata(
): AffectedMetadata {
const projectStates: ProjectStates = {};
const projects: ProjectMap = {};
projectNodes.forEach(project => {
projectStates[project.name] = {
touched: false,

View File

@ -22,7 +22,7 @@ describe('WorkspacesResults', () => {
describe('success', () => {
it('should return true when getting results', () => {
results.success('proj');
results.setResult('proj', true);
expect(results.getResult('proj')).toBe(true);
});
@ -32,7 +32,7 @@ describe('WorkspacesResults', () => {
spyOn(fs, 'unlinkSync');
spyOn(fs, 'existsSync').and.returnValue(true);
results.success('proj');
results.setResult('proj', true);
results.saveResults();
expect(fs.writeSync).not.toHaveBeenCalled();
@ -41,7 +41,7 @@ describe('WorkspacesResults', () => {
it('should print results', () => {
const projectName = 'proj';
results.success(projectName);
results.setResult(projectName, true);
spyOn(output, 'success');
const successTitle = 'Success';
@ -60,7 +60,7 @@ describe('WorkspacesResults', () => {
spyOn(output, 'success');
spyOn(output, 'warn');
results.success(projectName);
results.setResult(projectName, true);
const successTitle = 'Success';
@ -85,7 +85,7 @@ describe('WorkspacesResults', () => {
describe('fail', () => {
it('should return false when getting results', () => {
results.fail('proj');
results.setResult('proj', false);
expect(results.getResult('proj')).toBe(false);
});
@ -93,7 +93,7 @@ describe('WorkspacesResults', () => {
it('should save results to file system', () => {
spyOn(fs, 'writeFileSync');
results.fail('proj');
results.setResult('proj', false);
results.saveResults();
expect(fs.writeFileSync).toHaveBeenCalledWith(
@ -109,7 +109,7 @@ describe('WorkspacesResults', () => {
it('should print results', () => {
const projectName = 'proj';
results.fail(projectName);
results.setResult(projectName, false);
spyOn(output, 'error');
const errorTitle = 'Fail';
@ -128,7 +128,7 @@ describe('WorkspacesResults', () => {
it('should tell the user that they can isolate only the failed tests', () => {
const projectName = 'proj';
results.fail(projectName);
results.setResult(projectName, false);
spyOn(output, 'error');
const errorTitle = 'Fail';

View File

@ -51,14 +51,6 @@ export class WorkspaceResults {
return this.commandResults.results[projectName];
}
fail(projectName: string) {
this.setResult(projectName, false);
}
success(projectName: string) {
this.setResult(projectName, true);
}
saveResults() {
if (Object.values<boolean>(this.commandResults.results).includes(false)) {
writeJsonFile(RESULTS_FILE, this.commandResults);
@ -120,7 +112,7 @@ export class WorkspaceResults {
});
}
private setResult(projectName: string, result: boolean) {
setResult(projectName: string, result: boolean) {
this.commandResults.results[projectName] = result;
}
}

View File

@ -0,0 +1,189 @@
import defaultTasksRunner from './default-tasks-runner';
import { AffectedEventType, Task } from './tasks-runner';
jest.mock('npm-run-all', () => jest.fn());
import * as runAll from 'npm-run-all';
jest.mock('../command-line/shared', () => ({
cliCommand: () => 'nx'
}));
jest.mock('../utils/fileutils', () => ({
readJsonFile: () => ({
scripts: {
nx: 'nx'
}
})
}));
describe('defaultTasksRunner', () => {
let tasks: Task[];
beforeEach(() => {
tasks = [
{
id: 'task-1',
target: {
project: 'app-1',
target: 'target'
},
overrides: {}
},
{
id: 'task-2',
target: {
project: 'app-2',
target: 'target'
},
overrides: {}
}
];
});
it('should run the correct commands through "npm-run-all"', done => {
runAll.mockImplementation(() => Promise.resolve());
defaultTasksRunner(tasks, {}).subscribe({
complete: () => {
expect(runAll).toHaveBeenCalledWith(
['nx run app-1:target', 'nx run app-2:target'],
jasmine.anything()
);
done();
}
});
});
it('should run the correct commands through "npm-run-all" when tasks have a configuration', done => {
runAll.mockImplementation(() => Promise.resolve());
tasks = tasks.map(task => {
task.target.configuration = 'production';
return task;
});
defaultTasksRunner(tasks, {}).subscribe({
complete: () => {
expect(runAll).toHaveBeenCalledWith(
['nx run app-1:target:production', 'nx run app-2:target:production'],
jasmine.anything()
);
done();
}
});
});
it('should run the correct commands through "npm-run-all" when tasks have overrides', done => {
runAll.mockImplementation(() => Promise.resolve());
tasks = tasks.map(task => {
task.overrides = {
override: 'override-value'
};
return task;
});
defaultTasksRunner(tasks, {}).subscribe({
complete: () => {
expect(runAll).toHaveBeenCalledWith(
[
'nx run app-1:target --override=override-value',
'nx run app-2:target --override=override-value'
],
jasmine.anything()
);
done();
}
});
});
it('should run the correct commands through "npm-run-all" when tasks have configurations and overrides', done => {
runAll.mockImplementation(() => Promise.resolve());
tasks = tasks.map(task => {
task.target.configuration = 'production';
task.overrides = {
override: 'override-value'
};
return task;
});
defaultTasksRunner(tasks, {}).subscribe({
complete: () => {
expect(runAll).toHaveBeenCalledWith(
[
'nx run app-1:target:production --override=override-value',
'nx run app-2:target:production --override=override-value'
],
jasmine.anything()
);
done();
}
});
});
it('should pass the right options when options are passed', done => {
runAll.mockImplementation(() => Promise.resolve());
defaultTasksRunner(tasks, {
parallel: true,
maxParallel: 5
}).subscribe({
complete: () => {
expect(runAll).toHaveBeenCalledWith(
jasmine.any(Array),
jasmine.objectContaining({
parallel: true,
maxParallel: 5
})
);
done();
}
});
});
it('should run emit task complete events when "run-all-prerender" resolves', done => {
runAll.mockImplementation(() => Promise.resolve());
let i = 0;
const expected = [
{
task: tasks[0],
type: AffectedEventType.TaskComplete,
success: true
},
{
task: tasks[1],
type: AffectedEventType.TaskComplete,
success: true
}
];
defaultTasksRunner(tasks, {}).subscribe({
next: event => {
expect(event).toEqual(expected[i++]);
},
complete: done
});
});
it('should run emit task complete events when "run-all-prerender" rejects', done => {
runAll.mockImplementation(() =>
Promise.reject({
results: [
{
code: 0
},
{
code: 1
}
]
})
);
let i = 0;
const expected = [
{
task: tasks[0],
type: AffectedEventType.TaskComplete,
success: true
},
{
task: tasks[1],
type: AffectedEventType.TaskComplete,
success: false
}
];
defaultTasksRunner(tasks, {}).subscribe({
next: event => {
expect(event).toEqual(expected[i++]);
},
complete: done
});
});
});

View File

@ -0,0 +1,121 @@
import * as runAll from 'npm-run-all';
import { Observable } from 'rxjs';
import { basename } from 'path';
import {
AffectedEventType,
Task,
TaskCompleteEvent,
TasksRunner
} from './tasks-runner';
import { cliCommand } from '../command-line/shared';
import { output } from '../command-line/output';
import { readJsonFile } from '../utils/fileutils';
const commonCommands = ['build', 'test', 'lint', 'e2e', 'deploy'];
export interface DefaultTasksRunnerOptions {
parallel?: boolean;
maxParallel?: number;
}
export const defaultTasksRunner: TasksRunner<DefaultTasksRunnerOptions> = (
tasks: Task[],
options: DefaultTasksRunnerOptions
): Observable<TaskCompleteEvent> => {
const additionalTaskOverrides = getLegacyTaskOverrides(options);
tasks.forEach(task => {
task.overrides = {
...task.overrides,
...additionalTaskOverrides
};
});
const commands = getCommands(tasks);
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
});
});
})
.catch(e => {
e.results.forEach((result, i) => {
subscriber.next({
task: tasks[i],
type: AffectedEventType.TaskComplete,
success: result.code === 0
});
});
})
.finally(() => {
subscriber.complete();
// fix for https://github.com/nrwl/nx/issues/1666
if (process.stdin['unref']) (process.stdin as any).unref();
});
});
};
export default defaultTasksRunner;
function getLegacyTaskOverrides(options: any) {
const legacyTaskOverrides = { ...options };
delete legacyTaskOverrides.maxParallel;
delete legacyTaskOverrides['max-parallel'];
delete legacyTaskOverrides.parallel;
delete legacyTaskOverrides.verbose;
return legacyTaskOverrides;
}
function getCommands(tasks: Task[]) {
const cli = cliCommand();
assertPackageJsonScriptExists(cli);
const isYarn = basename(process.env.npm_execpath || 'npm').startsWith('yarn');
return tasks.map(task => {
const args = Object.entries(task.overrides)
.map(([prop, value]) => `--${prop}=${value}`)
.join(' ');
return commonCommands.includes(task.target.target)
? `${cli}${isYarn ? '' : ' --'} ${task.target.target} ${
task.target.project
} ${
task.target.configuration
? `--configuration ${task.target.configuration} `
: ''
}${args}`
: `${cli}${isYarn ? '' : ' --'} run ${task.target.project}:${
task.target.target
}${task.target.configuration ? `:${task.target.configuration}` : ''}${
args ? ' ' + args : ''
}`;
});
}
function assertPackageJsonScriptExists(cli: string) {
// Make sure the `package.json` has the `nx: "nx"` command needed by `npm-run-all`
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);
}
}

View File

@ -0,0 +1,37 @@
import { Observable } from 'rxjs';
import { Target } from '@angular-devkit/architect';
import { DependencyGraph } from '../command-line/shared';
export interface Task {
id: string;
target: Target;
overrides: Object;
}
export enum AffectedEventType {
TaskComplete = '[Task] Complete'
}
export interface AffectedEvent {
task: Task;
type: AffectedEventType;
}
export interface TaskCompleteEvent extends AffectedEvent {
type: AffectedEventType.TaskComplete;
success: boolean;
}
export type TasksRunner<T = unknown> = (
tasks: Task[],
options?: T,
context?: {
dependencyGraph: DependencyGraph;
tasksMap: {
[projectName: string]: {
[targetName: string]: Task;
};
};
}
) => Observable<AffectedEvent>;

View File

@ -123,3 +123,7 @@ export function renameSync(
cb(e);
}
}
export function isRelativePath(path: string): boolean {
return path.startsWith('./') || path.startsWith('../');
}