319 lines
8.1 KiB
TypeScript
319 lines
8.1 KiB
TypeScript
import { execSync } from 'child_process';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as resolve from 'resolve';
|
|
import * as runAll from 'npm-run-all';
|
|
import * as yargs from 'yargs';
|
|
|
|
import {
|
|
getAffectedApps,
|
|
getAffectedLibs,
|
|
getAffectedProjects,
|
|
getAllAppNames,
|
|
getAllLibNames,
|
|
getProjectNames,
|
|
parseFiles,
|
|
getAllProjectsWithTarget,
|
|
getAffectedProjectsWithTarget,
|
|
readAngularJson,
|
|
printArgsWarning
|
|
} from './shared';
|
|
import { generateGraph } from './dep-graph';
|
|
import { WorkspaceResults } from './workspace-results';
|
|
|
|
export interface YargsAffectedOptions
|
|
extends yargs.Arguments,
|
|
AffectedOptions {}
|
|
|
|
export interface AffectedOptions {
|
|
target?: string;
|
|
parallel?: boolean;
|
|
maxParallel?: number;
|
|
untracked?: boolean;
|
|
uncommitted?: boolean;
|
|
all?: boolean;
|
|
base?: string;
|
|
head?: string;
|
|
exclude?: string[];
|
|
files?: string[];
|
|
onlyFailed?: boolean;
|
|
'only-failed'?: boolean;
|
|
'max-parallel'?: boolean;
|
|
verbose?: boolean;
|
|
help?: boolean;
|
|
version?: boolean;
|
|
quiet?: boolean;
|
|
}
|
|
|
|
// Commands that can do `ng [command]`
|
|
const ngCommands = ['build', 'test', 'lint', 'e2e'];
|
|
|
|
export function affected(parsedArgs: YargsAffectedOptions): void {
|
|
const target = parsedArgs.target;
|
|
const rest: string[] = [
|
|
...parsedArgs._.slice(1),
|
|
...filterNxSpecificArgs(parsedArgs)
|
|
];
|
|
|
|
const workspaceResults = new WorkspaceResults(target);
|
|
|
|
try {
|
|
switch (target) {
|
|
case 'apps':
|
|
const apps = (parsedArgs.all
|
|
? getAllAppNames()
|
|
: getAffectedApps(parseFiles(parsedArgs).files)
|
|
)
|
|
.filter(app => !parsedArgs.exclude.includes(app))
|
|
.filter(
|
|
project =>
|
|
!parsedArgs.onlyFailed || !workspaceResults.getResult(project)
|
|
);
|
|
printArgsWarning(parsedArgs);
|
|
console.log(apps.join(' '));
|
|
break;
|
|
case 'libs':
|
|
const libs = (parsedArgs.all
|
|
? getAllLibNames()
|
|
: getAffectedLibs(parseFiles(parsedArgs).files)
|
|
)
|
|
.filter(app => !parsedArgs.exclude.includes(app))
|
|
.filter(
|
|
project =>
|
|
!parsedArgs.onlyFailed || !workspaceResults.getResult(project)
|
|
);
|
|
printArgsWarning(parsedArgs);
|
|
console.log(libs.join(' '));
|
|
break;
|
|
case 'dep-graph':
|
|
const projects = parsedArgs.all
|
|
? getProjectNames()
|
|
: getAffectedProjects(parseFiles(parsedArgs).files)
|
|
.filter(app => !parsedArgs.exclude.includes(app))
|
|
.filter(
|
|
project =>
|
|
!parsedArgs.onlyFailed || !workspaceResults.getResult(project)
|
|
);
|
|
printArgsWarning(parsedArgs);
|
|
generateGraph(parsedArgs, projects);
|
|
break;
|
|
default:
|
|
const targetProjects = getProjects(
|
|
target,
|
|
parsedArgs,
|
|
workspaceResults,
|
|
parsedArgs.all
|
|
);
|
|
printArgsWarning(parsedArgs);
|
|
runCommand(
|
|
target,
|
|
targetProjects,
|
|
parsedArgs,
|
|
rest,
|
|
workspaceResults,
|
|
`Running ${target} for`,
|
|
`Running ${target} for affected projects succeeded.`,
|
|
`Running ${target} for affected projects failed.`
|
|
);
|
|
break;
|
|
}
|
|
} catch (e) {
|
|
printError(e, parsedArgs.verbose);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
function getProjects(
|
|
target: string,
|
|
parsedArgs: YargsAffectedOptions,
|
|
workspaceResults: WorkspaceResults,
|
|
all: boolean
|
|
) {
|
|
const projects = all
|
|
? getAllProjectsWithTarget(target)
|
|
: getAffectedProjectsWithTarget(target)(parseFiles(parsedArgs).files);
|
|
|
|
return projects
|
|
.filter(project => !parsedArgs.exclude.includes(project))
|
|
.filter(
|
|
project => !parsedArgs.onlyFailed || !workspaceResults.getResult(project)
|
|
);
|
|
}
|
|
|
|
function printError(e: any, verbose?: boolean) {
|
|
if (verbose && e.stack) {
|
|
console.error(e.stack);
|
|
} else {
|
|
console.error(e.message);
|
|
}
|
|
}
|
|
|
|
async function runCommand(
|
|
command: string,
|
|
projects: string[],
|
|
parsedArgs: YargsAffectedOptions,
|
|
args: string[],
|
|
workspaceResults: WorkspaceResults,
|
|
iterationMessage: string,
|
|
successMessage: string,
|
|
errorMessage: string
|
|
) {
|
|
if (projects.length <= 0) {
|
|
console.log(`No projects to run ${command}`);
|
|
return;
|
|
}
|
|
|
|
let message = `${iterationMessage} projects:\n ${projects.join(',\n ')}`;
|
|
console.log(message);
|
|
if (args.length > 0) {
|
|
console.log(`With flags: ${args.join(' ')}`);
|
|
}
|
|
const angularJson = readAngularJson();
|
|
const projectMetadata = new Map<string, any>();
|
|
projects.forEach(project => {
|
|
projectMetadata.set(project, angularJson.projects[project]);
|
|
});
|
|
|
|
// Make sure the `package.json` has the `ng: "ng"` command needed by `npm-run-all`
|
|
const packageJson = JSON.parse(
|
|
fs.readFileSync('./package.json').toString('utf-8')
|
|
);
|
|
if (!packageJson.scripts || !packageJson.scripts.ng) {
|
|
console.error(
|
|
'\nError: Your `package.json` file should contain the `ng: "ng"` command in the `scripts` section.\n'
|
|
);
|
|
return process.exit(1);
|
|
}
|
|
|
|
try {
|
|
await runAll(
|
|
projects.map(app => {
|
|
return ngCommands.includes(command)
|
|
? `ng -- ${command} --project=${app} ${transformArgs(
|
|
args,
|
|
app,
|
|
projectMetadata.get(app)
|
|
).join(' ')} `
|
|
: `ng -- run ${app}:${command} ${transformArgs(
|
|
args,
|
|
app,
|
|
projectMetadata.get(app)
|
|
).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]);
|
|
}
|
|
});
|
|
}
|
|
|
|
workspaceResults.saveResults();
|
|
workspaceResults.printResults(
|
|
parsedArgs.onlyFailed,
|
|
successMessage,
|
|
errorMessage
|
|
);
|
|
|
|
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');
|
|
}
|
|
|
|
if (group === 'name') {
|
|
return projectName;
|
|
}
|
|
return projectMetadata[group];
|
|
});
|
|
});
|
|
}
|
|
|
|
function filterNxSpecificArgs(parsedArgs: YargsAffectedOptions): string[] {
|
|
const filteredArgs = { ...parsedArgs };
|
|
// Delete Nx arguments from parsed Args
|
|
nxSpecificFlags.forEach(flag => {
|
|
delete filteredArgs[flag];
|
|
});
|
|
|
|
// These would be arguments such as app2 in --app app1 app2 which the CLI does not accept
|
|
delete filteredArgs._;
|
|
// 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[filteredArg]
|
|
.map(value => {
|
|
return `--${filteredArg}=${value}`;
|
|
})
|
|
.join(' ');
|
|
});
|
|
}
|
|
|
|
function ngPath() {
|
|
const basePath = path.dirname(
|
|
path.dirname(
|
|
path.dirname(resolve.sync('@angular/cli', { basedir: __dirname }))
|
|
)
|
|
);
|
|
return `"${path.join(basePath, 'bin', 'ng')}"`;
|
|
}
|
|
|
|
/**
|
|
* These options are only for getting an array with properties of AffectedOptions.
|
|
*
|
|
* @remark They are not defaults or useful for anything else
|
|
*/
|
|
const dummyOptions: AffectedOptions = {
|
|
target: '',
|
|
parallel: false,
|
|
maxParallel: 3,
|
|
'max-parallel': false,
|
|
onlyFailed: false,
|
|
'only-failed': false,
|
|
untracked: false,
|
|
uncommitted: false,
|
|
help: false,
|
|
version: false,
|
|
quiet: false,
|
|
all: false,
|
|
base: 'base',
|
|
head: 'head',
|
|
exclude: ['exclude'],
|
|
files: [''],
|
|
verbose: false
|
|
};
|
|
|
|
const nxSpecificFlags = Object.keys(dummyOptions);
|