Skip to content

Commit

Permalink
feat(@angular-devkit/architect-cli): CLI tool to use new Architect API
Browse files Browse the repository at this point in the history
Move the entire Architect CLI to use the new API, and report progress using
a progress bar for each worker currently executing. Shows log at the end
of the execution.

This is meant to be used as a debugging tool to help people move their builders
to the new API.
  • Loading branch information
hansl authored and Keen Yee Liau committed Feb 19, 2019
1 parent df1b56c commit 558ef00
Show file tree
Hide file tree
Showing 6 changed files with 320 additions and 83 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
]
},
"dependencies": {
"@types/progress": "^2.0.3",
"glob": "^7.0.3",
"node-fetch": "^2.2.0",
"puppeteer": "1.12.2",
Expand Down
4 changes: 3 additions & 1 deletion packages/angular_devkit/architect_cli/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,17 @@ ts_library(
name = "architect_cli",
srcs = [
"bin/architect.ts",
],
] + glob(["src/**/*.ts"]),
module_name = "@angular-devkit/architect-cli",
deps = [
"//packages/angular_devkit/architect",
"//packages/angular_devkit/architect:node",
"//packages/angular_devkit/core",
"//packages/angular_devkit/core:node",
"@rxjs",
"@rxjs//operators",
"@npm//@types/node",
"@npm//@types/minimist",
"@npm//@types/progress",
],
)
239 changes: 161 additions & 78 deletions packages/angular_devkit/architect_cli/bin/architect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,22 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import 'symbol-observable';
// symbol polyfill must go first
// tslint:disable-next-line:ordered-imports import-groups
import { Architect } from '@angular-devkit/architect';
import { dirname, experimental, normalize, tags } from '@angular-devkit/core';
import { index2 } from '@angular-devkit/architect';
import { WorkspaceNodeModulesArchitectHost } from '@angular-devkit/architect/node';
import {
dirname,
experimental,
json,
logging,
normalize,
schema,
tags, terminal,
} from '@angular-devkit/core';
import { NodeJsSyncHost, createConsoleLogger } from '@angular-devkit/core/node';
import { existsSync, readFileSync } from 'fs';
import * as minimist from 'minimist';
import * as path from 'path';
import { throwError } from 'rxjs';
import { concatMap } from 'rxjs/operators';
import { MultiProgressBar } from '../src/progress';


function findUp(names: string | string[], from: string) {
Expand All @@ -44,7 +48,7 @@ function findUp(names: string | string[], from: string) {
/**
* Show usage of the CLI tool, and exit the process.
*/
function usage(exitCode = 0): never {
function usage(logger: logging.Logger, exitCode = 0): never {
logger.info(tags.stripIndent`
architect [project][:target][:configuration] [options, ...]
Expand All @@ -63,86 +67,165 @@ function usage(exitCode = 0): never {
throw 0; // The node typing sometimes don't have a never type for process.exit().
}

/** Parse the command line. */
const argv = minimist(process.argv.slice(2), { boolean: ['help'] });
function _targetStringFromTarget({project, target, configuration}: index2.Target) {
return `${project}:${target}${configuration !== undefined ? ':' + configuration : ''}`;
}

/** Create the DevKit Logger used through the CLI. */
const logger = createConsoleLogger(argv['verbose']);

// Check the target.
const targetStr = argv._.shift();
if (!targetStr && argv.help) {
// Show architect usage if there's no target.
usage();
interface BarInfo {
status?: string;
builder: index2.BuilderInfo;
target?: index2.Target;
}

// Split a target into its parts.
let project: string, targetName: string, configuration: string;
if (targetStr) {
[project, targetName, configuration] = targetStr.split(':');
}

// Load workspace configuration file.
const currentPath = process.cwd();
const configFileNames = [
'angular.json',
'.angular.json',
'workspace.json',
'.workspace.json',
];

const configFilePath = findUp(configFileNames, currentPath);

if (!configFilePath) {
logger.fatal(`Workspace configuration file (${configFileNames.join(', ')}) cannot be found in `
+ `'${currentPath}' or in parent directories.`);
process.exit(3);
throw 3; // TypeScript doesn't know that process.exit() never returns.
async function _executeTarget(
parentLogger: logging.Logger,
workspace: experimental.workspace.Workspace,
root: string,
argv: minimist.ParsedArgs,
registry: json.schema.SchemaRegistry,
) {
const architectHost = new WorkspaceNodeModulesArchitectHost(workspace, root);
const architect = new index2.Architect(architectHost, registry);

// Split a target into its parts.
const targetStr = argv._.shift() || '';
const [project, target, configuration] = targetStr.split(':');
const targetSpec = { project, target, configuration };

delete argv['help'];
delete argv['_'];

const logger = new logging.Logger('jobs');
const logs: logging.LogEntry[] = [];
logger.subscribe(entry => logs.push({ ...entry, message: `${entry.name}: ` + entry.message }));

const run = await architect.scheduleTarget(targetSpec, argv, { logger });
const bars = new MultiProgressBar<number, BarInfo>(':name :bar (:current/:total) :status');

run.progress.subscribe(
update => {
const data = bars.get(update.id) || {
id: update.id,
builder: update.builder,
target: update.target,
status: update.status || '',
name: ((update.target ? _targetStringFromTarget(update.target) : update.builder.name)
+ ' '.repeat(80)
).substr(0, 40),
};

if (update.status !== undefined) {
data.status = update.status;
}

switch (update.state) {
case index2.BuilderProgressState.Error:
data.status = 'Error: ' + update.error;
bars.update(update.id, data);
break;

case index2.BuilderProgressState.Stopped:
data.status = 'Done.';
bars.complete(update.id);
bars.update(update.id, data, update.total, update.total);
break;

case index2.BuilderProgressState.Waiting:
bars.update(update.id, data);
break;

case index2.BuilderProgressState.Running:
bars.update(update.id, data, update.current, update.total);
break;
}

bars.render();
},
);

// Wait for full completion of the builder.
try {
const result = await run.result;

if (result.success) {
parentLogger.info(terminal.green('SUCCESS'));
} else {
parentLogger.info(terminal.yellow('FAILURE'));
}

parentLogger.info('\nLogs:');
logs.forEach(l => parentLogger.next(l));

await run.stop();
bars.terminate();

return result.success ? 0 : 1;
} catch (err) {
parentLogger.info(terminal.red('ERROR'));
parentLogger.info('\nLogs:');
logs.forEach(l => parentLogger.next(l));

parentLogger.fatal('Exception:');
parentLogger.fatal(err.stack);

return 2;
}
}

const root = dirname(normalize(configFilePath));
const configContent = readFileSync(configFilePath, 'utf-8');
const workspaceJson = JSON.parse(configContent);

const host = new NodeJsSyncHost();
const workspace = new experimental.workspace.Workspace(root, host);
async function main(args: string[]): Promise<number> {
/** Parse the command line. */
const argv = minimist(args, { boolean: ['help'] });

let lastBuildEvent = { success: true };
/** Create the DevKit Logger used through the CLI. */
const logger = createConsoleLogger(argv['verbose']);

workspace.loadWorkspaceFromJson(workspaceJson).pipe(
concatMap(ws => new Architect(ws).loadArchitect()),
concatMap(architect => {
// Check the target.
const targetStr = argv._[0] || '';
if (!targetStr || argv.help) {
// Show architect usage if there's no target.
usage(logger);
}

const overrides = { ...argv };
delete overrides['help'];
delete overrides['_'];
// Load workspace configuration file.
const currentPath = process.cwd();
const configFileNames = [
'angular.json',
'.angular.json',
'workspace.json',
'.workspace.json',
];

const targetSpec = {
project,
target: targetName,
configuration,
overrides,
};
const configFilePath = findUp(configFileNames, currentPath);

// TODO: better logging of what's happening.
if (argv.help) {
// TODO: add target help
return throwError('Target help NYI.');
// architect.help(targetOptions, logger);
} else {
const builderConfig = architect.getBuilderConfiguration(targetSpec);
if (!configFilePath) {
logger.fatal(`Workspace configuration file (${configFileNames.join(', ')}) cannot be found in `
+ `'${currentPath}' or in parent directories.`);

return architect.run(builderConfig, { logger });
}
}),
).subscribe({
next: (buildEvent => lastBuildEvent = buildEvent),
complete: () => process.exit(lastBuildEvent.success ? 0 : 1),
error: (err: Error) => {
logger.fatal(err.message);
if (err.stack) {
logger.fatal(err.stack);
}
process.exit(1);
},
});
return 3;
}

const root = dirname(normalize(configFilePath));
const configContent = readFileSync(configFilePath, 'utf-8');
const workspaceJson = JSON.parse(configContent);

const registry = new schema.CoreSchemaRegistry();
registry.addPostTransform(schema.transforms.addUndefinedDefaults);

const host = new NodeJsSyncHost();
const workspace = new experimental.workspace.Workspace(root, host);

await workspace.loadWorkspaceFromJson(workspaceJson).toPromise();

return await _executeTarget(logger, workspace, root, argv, registry);
}

main(process.argv.slice(2))
.then(code => {
process.exit(code);
}, err => {
console.error('Error: ' + err.stack || err.message || err);
process.exit(-1);
});
9 changes: 6 additions & 3 deletions packages/angular_devkit/architect_cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@
"tooling"
],
"dependencies": {
"@angular-devkit/core": "0.0.0",
"@angular-devkit/architect": "0.0.0",
"@angular-devkit/core": "0.0.0",
"@types/progress": "^2.0.3",
"ascii-progress": "^1.0.5",
"minimist": "1.2.0",
"symbol-observable": "1.2.0",
"rxjs": "6.3.3"
"progress": "^2.0.3",
"rxjs": "6.3.3",
"symbol-observable": "1.2.0"
}
}
Loading

0 comments on commit 558ef00

Please sign in to comment.