Skip to content

Commit 558ef00

Browse files
hanslKeen Yee Liau
authored and
Keen Yee Liau
committed
feat(@angular-devkit/architect-cli): CLI tool to use new Architect API
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.
1 parent df1b56c commit 558ef00

File tree

6 files changed

+320
-83
lines changed

6 files changed

+320
-83
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
]
6464
},
6565
"dependencies": {
66+
"@types/progress": "^2.0.3",
6667
"glob": "^7.0.3",
6768
"node-fetch": "^2.2.0",
6869
"puppeteer": "1.12.2",

packages/angular_devkit/architect_cli/BUILD

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,17 @@ ts_library(
1212
name = "architect_cli",
1313
srcs = [
1414
"bin/architect.ts",
15-
],
15+
] + glob(["src/**/*.ts"]),
1616
module_name = "@angular-devkit/architect-cli",
1717
deps = [
1818
"//packages/angular_devkit/architect",
19+
"//packages/angular_devkit/architect:node",
1920
"//packages/angular_devkit/core",
2021
"//packages/angular_devkit/core:node",
2122
"@rxjs",
2223
"@rxjs//operators",
2324
"@npm//@types/node",
2425
"@npm//@types/minimist",
26+
"@npm//@types/progress",
2527
],
2628
)

packages/angular_devkit/architect_cli/bin/architect.ts

+161-78
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,22 @@
66
* Use of this source code is governed by an MIT-style license that can be
77
* found in the LICENSE file at https://angular.io/license
88
*/
9-
10-
import 'symbol-observable';
11-
// symbol polyfill must go first
12-
// tslint:disable-next-line:ordered-imports import-groups
13-
import { Architect } from '@angular-devkit/architect';
14-
import { dirname, experimental, normalize, tags } from '@angular-devkit/core';
9+
import { index2 } from '@angular-devkit/architect';
10+
import { WorkspaceNodeModulesArchitectHost } from '@angular-devkit/architect/node';
11+
import {
12+
dirname,
13+
experimental,
14+
json,
15+
logging,
16+
normalize,
17+
schema,
18+
tags, terminal,
19+
} from '@angular-devkit/core';
1520
import { NodeJsSyncHost, createConsoleLogger } from '@angular-devkit/core/node';
1621
import { existsSync, readFileSync } from 'fs';
1722
import * as minimist from 'minimist';
1823
import * as path from 'path';
19-
import { throwError } from 'rxjs';
20-
import { concatMap } from 'rxjs/operators';
24+
import { MultiProgressBar } from '../src/progress';
2125

2226

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

66-
/** Parse the command line. */
67-
const argv = minimist(process.argv.slice(2), { boolean: ['help'] });
70+
function _targetStringFromTarget({project, target, configuration}: index2.Target) {
71+
return `${project}:${target}${configuration !== undefined ? ':' + configuration : ''}`;
72+
}
6873
69-
/** Create the DevKit Logger used through the CLI. */
70-
const logger = createConsoleLogger(argv['verbose']);
7174
72-
// Check the target.
73-
const targetStr = argv._.shift();
74-
if (!targetStr && argv.help) {
75-
// Show architect usage if there's no target.
76-
usage();
75+
interface BarInfo {
76+
status?: string;
77+
builder: index2.BuilderInfo;
78+
target?: index2.Target;
7779
}
7880
79-
// Split a target into its parts.
80-
let project: string, targetName: string, configuration: string;
81-
if (targetStr) {
82-
[project, targetName, configuration] = targetStr.split(':');
83-
}
8481
85-
// Load workspace configuration file.
86-
const currentPath = process.cwd();
87-
const configFileNames = [
88-
'angular.json',
89-
'.angular.json',
90-
'workspace.json',
91-
'.workspace.json',
92-
];
93-
94-
const configFilePath = findUp(configFileNames, currentPath);
95-
96-
if (!configFilePath) {
97-
logger.fatal(`Workspace configuration file (${configFileNames.join(', ')}) cannot be found in `
98-
+ `'${currentPath}' or in parent directories.`);
99-
process.exit(3);
100-
throw 3; // TypeScript doesn't know that process.exit() never returns.
82+
async function _executeTarget(
83+
parentLogger: logging.Logger,
84+
workspace: experimental.workspace.Workspace,
85+
root: string,
86+
argv: minimist.ParsedArgs,
87+
registry: json.schema.SchemaRegistry,
88+
) {
89+
const architectHost = new WorkspaceNodeModulesArchitectHost(workspace, root);
90+
const architect = new index2.Architect(architectHost, registry);
91+
92+
// Split a target into its parts.
93+
const targetStr = argv._.shift() || '';
94+
const [project, target, configuration] = targetStr.split(':');
95+
const targetSpec = { project, target, configuration };
96+
97+
delete argv['help'];
98+
delete argv['_'];
99+
100+
const logger = new logging.Logger('jobs');
101+
const logs: logging.LogEntry[] = [];
102+
logger.subscribe(entry => logs.push({ ...entry, message: `${entry.name}: ` + entry.message }));
103+
104+
const run = await architect.scheduleTarget(targetSpec, argv, { logger });
105+
const bars = new MultiProgressBar<number, BarInfo>(':name :bar (:current/:total) :status');
106+
107+
run.progress.subscribe(
108+
update => {
109+
const data = bars.get(update.id) || {
110+
id: update.id,
111+
builder: update.builder,
112+
target: update.target,
113+
status: update.status || '',
114+
name: ((update.target ? _targetStringFromTarget(update.target) : update.builder.name)
115+
+ ' '.repeat(80)
116+
).substr(0, 40),
117+
};
118+
119+
if (update.status !== undefined) {
120+
data.status = update.status;
121+
}
122+
123+
switch (update.state) {
124+
case index2.BuilderProgressState.Error:
125+
data.status = 'Error: ' + update.error;
126+
bars.update(update.id, data);
127+
break;
128+
129+
case index2.BuilderProgressState.Stopped:
130+
data.status = 'Done.';
131+
bars.complete(update.id);
132+
bars.update(update.id, data, update.total, update.total);
133+
break;
134+
135+
case index2.BuilderProgressState.Waiting:
136+
bars.update(update.id, data);
137+
break;
138+
139+
case index2.BuilderProgressState.Running:
140+
bars.update(update.id, data, update.current, update.total);
141+
break;
142+
}
143+
144+
bars.render();
145+
},
146+
);
147+
148+
// Wait for full completion of the builder.
149+
try {
150+
const result = await run.result;
151+
152+
if (result.success) {
153+
parentLogger.info(terminal.green('SUCCESS'));
154+
} else {
155+
parentLogger.info(terminal.yellow('FAILURE'));
156+
}
157+
158+
parentLogger.info('\nLogs:');
159+
logs.forEach(l => parentLogger.next(l));
160+
161+
await run.stop();
162+
bars.terminate();
163+
164+
return result.success ? 0 : 1;
165+
} catch (err) {
166+
parentLogger.info(terminal.red('ERROR'));
167+
parentLogger.info('\nLogs:');
168+
logs.forEach(l => parentLogger.next(l));
169+
170+
parentLogger.fatal('Exception:');
171+
parentLogger.fatal(err.stack);
172+
173+
return 2;
174+
}
101175
}
102176
103-
const root = dirname(normalize(configFilePath));
104-
const configContent = readFileSync(configFilePath, 'utf-8');
105-
const workspaceJson = JSON.parse(configContent);
106177
107-
const host = new NodeJsSyncHost();
108-
const workspace = new experimental.workspace.Workspace(root, host);
178+
async function main(args: string[]): Promise<number> {
179+
/** Parse the command line. */
180+
const argv = minimist(args, { boolean: ['help'] });
109181
110-
let lastBuildEvent = { success: true };
182+
/** Create the DevKit Logger used through the CLI. */
183+
const logger = createConsoleLogger(argv['verbose']);
111184
112-
workspace.loadWorkspaceFromJson(workspaceJson).pipe(
113-
concatMap(ws => new Architect(ws).loadArchitect()),
114-
concatMap(architect => {
185+
// Check the target.
186+
const targetStr = argv._[0] || '';
187+
if (!targetStr || argv.help) {
188+
// Show architect usage if there's no target.
189+
usage(logger);
190+
}
115191
116-
const overrides = { ...argv };
117-
delete overrides['help'];
118-
delete overrides['_'];
192+
// Load workspace configuration file.
193+
const currentPath = process.cwd();
194+
const configFileNames = [
195+
'angular.json',
196+
'.angular.json',
197+
'workspace.json',
198+
'.workspace.json',
199+
];
119200
120-
const targetSpec = {
121-
project,
122-
target: targetName,
123-
configuration,
124-
overrides,
125-
};
201+
const configFilePath = findUp(configFileNames, currentPath);
126202
127-
// TODO: better logging of what's happening.
128-
if (argv.help) {
129-
// TODO: add target help
130-
return throwError('Target help NYI.');
131-
// architect.help(targetOptions, logger);
132-
} else {
133-
const builderConfig = architect.getBuilderConfiguration(targetSpec);
203+
if (!configFilePath) {
204+
logger.fatal(`Workspace configuration file (${configFileNames.join(', ')}) cannot be found in `
205+
+ `'${currentPath}' or in parent directories.`);
134206

135-
return architect.run(builderConfig, { logger });
136-
}
137-
}),
138-
).subscribe({
139-
next: (buildEvent => lastBuildEvent = buildEvent),
140-
complete: () => process.exit(lastBuildEvent.success ? 0 : 1),
141-
error: (err: Error) => {
142-
logger.fatal(err.message);
143-
if (err.stack) {
144-
logger.fatal(err.stack);
145-
}
146-
process.exit(1);
147-
},
148-
});
207+
return 3;
208+
}
209+
210+
const root = dirname(normalize(configFilePath));
211+
const configContent = readFileSync(configFilePath, 'utf-8');
212+
const workspaceJson = JSON.parse(configContent);
213+
214+
const registry = new schema.CoreSchemaRegistry();
215+
registry.addPostTransform(schema.transforms.addUndefinedDefaults);
216+
217+
const host = new NodeJsSyncHost();
218+
const workspace = new experimental.workspace.Workspace(root, host);
219+
220+
await workspace.loadWorkspaceFromJson(workspaceJson).toPromise();
221+
222+
return await _executeTarget(logger, workspace, root, argv, registry);
223+
}
224+
225+
main(process.argv.slice(2))
226+
.then(code => {
227+
process.exit(code);
228+
}, err => {
229+
console.error('Error: ' + err.stack || err.message || err);
230+
process.exit(-1);
231+
});

packages/angular_devkit/architect_cli/package.json

+6-3
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@
1212
"tooling"
1313
],
1414
"dependencies": {
15-
"@angular-devkit/core": "0.0.0",
1615
"@angular-devkit/architect": "0.0.0",
16+
"@angular-devkit/core": "0.0.0",
17+
"@types/progress": "^2.0.3",
18+
"ascii-progress": "^1.0.5",
1719
"minimist": "1.2.0",
18-
"symbol-observable": "1.2.0",
19-
"rxjs": "6.3.3"
20+
"progress": "^2.0.3",
21+
"rxjs": "6.3.3",
22+
"symbol-observable": "1.2.0"
2023
}
2124
}

0 commit comments

Comments
 (0)