1
0
mirror of https://github.com/angular/angular-cli.git synced 2025-05-19 04:26:01 +08:00

refactor(@angular-devkit/schematics): add a BaseWorkflow which implements logic

And receives in its constructor the enginehost and registry. This simplifies the creation
of the NodeWorkflow, or later on the Google3Workflow etc, since all the duplicate logic
is now in a single base class.

This is yak shaving for internal stuff.
This commit is contained in:
Hans Larsen 2018-08-01 09:44:17 -04:00 committed by Hans
parent 84ec3022c2
commit 2ba1f16295
6 changed files with 2578 additions and 2475 deletions
package-lock.json
packages/angular_devkit/schematics

4703
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -12,7 +12,7 @@ import { Url } from 'url';
import { MergeStrategy } from '../tree/interface';
import { NullTree } from '../tree/null';
import { empty } from '../tree/static';
import { Workflow } from '../workflow';
import { Workflow } from '../workflow/interface';
import {
Collection,
CollectionDescription,

@ -9,7 +9,7 @@ import { logging } from '@angular-devkit/core';
import { Observable } from 'rxjs';
import { Url } from 'url';
import { FileEntry, MergeStrategy, Tree } from '../tree/interface';
import { Workflow } from '../workflow';
import { Workflow } from '../workflow/interface';
export interface TaskConfiguration<T = {}> {

@ -0,0 +1,178 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 { logging, schema, virtualFs } from '@angular-devkit/core';
import { Observable, Subject, concat, of, throwError } from 'rxjs';
import { concatMap, defaultIfEmpty, ignoreElements, last, map, tap } from 'rxjs/operators';
import { EngineHost, SchematicEngine } from '../engine';
import { UnsuccessfulWorkflowExecution } from '../exception/exception';
import { standardFormats } from '../formats';
import { DryRunEvent, DryRunSink } from '../sink/dryrun';
import { HostSink } from '../sink/host';
import { HostTree } from '../tree/host-tree';
import { Tree } from '../tree/interface';
import { optimize } from '../tree/static';
import {
LifeCycleEvent,
RequiredWorkflowExecutionContext,
Workflow,
WorkflowExecutionContext,
} from './interface';
export interface BaseWorkflowOptions {
host: virtualFs.Host;
engineHost: EngineHost<{}, {}>;
registry?: schema.CoreSchemaRegistry;
force?: boolean;
dryRun?: boolean;
}
/**
* Base class for workflows. Even without abstract methods, this class should not be used without
* surrounding some initialization for the registry and host. This class only adds life cycle and
* dryrun/force support. You need to provide any registry and task executors that you need to
* support.
* See {@see NodeWorkflow} implementation for how to make a specialized subclass of this.
* TODO: add default set of CoreSchemaRegistry transforms. Once the job refactor is done, use that
* as the support for tasks.
*
* @public
*/
export abstract class BaseWorkflow implements Workflow {
protected _engine: SchematicEngine<{}, {}>;
protected _engineHost: EngineHost<{}, {}>;
protected _registry: schema.CoreSchemaRegistry;
protected _host: virtualFs.Host;
protected _reporter: Subject<DryRunEvent> = new Subject();
protected _lifeCycle: Subject<LifeCycleEvent> = new Subject();
protected _context: WorkflowExecutionContext[];
protected _force: boolean;
protected _dryRun: boolean;
constructor(options: BaseWorkflowOptions) {
this._host = options.host;
this._engineHost = options.engineHost;
this._registry = options.registry || new schema.CoreSchemaRegistry(standardFormats);
this._engine = new SchematicEngine(this._engineHost, this);
this._context = [];
this._force = options.force || false;
this._dryRun = options.dryRun || false;
}
get context(): Readonly<WorkflowExecutionContext> {
const maybeContext = this._context[this._context.length - 1];
if (!maybeContext) {
throw new Error('Cannot get context when workflow is not executing...');
}
return maybeContext;
}
get registry(): schema.SchemaRegistry {
return this._registry;
}
get reporter(): Observable<DryRunEvent> {
return this._reporter.asObservable();
}
get lifeCycle(): Observable<LifeCycleEvent> {
return this._lifeCycle.asObservable();
}
execute(
options: Partial<WorkflowExecutionContext> & RequiredWorkflowExecutionContext,
): Observable<void> {
const parentContext = this._context[this._context.length - 1];
if (!parentContext) {
this._lifeCycle.next({ kind: 'start' });
}
/** Create the collection and the schematic. */
const collection = this._engine.createCollection(options.collection);
// Only allow private schematics if called from the same collection.
const allowPrivate = options.allowPrivate
|| (parentContext && parentContext.collection === options.collection);
const schematic = collection.createSchematic(options.schematic, allowPrivate);
// We need two sinks if we want to output what will happen, and actually do the work.
// Note that fsSink is technically not used if `--dry-run` is passed, but creating the Sink
// does not have any side effect.
const dryRunSink = new DryRunSink(this._host, this._force);
const fsSink = new HostSink(this._host, this._force);
let error = false;
const dryRunSubscriber = dryRunSink.reporter.subscribe(event => {
this._reporter.next(event);
error = error || (event.kind == 'error');
});
this._lifeCycle.next({ kind: 'workflow-start' });
const context = {
...options,
debug: options.debug || false,
logger: options.logger || (parentContext && parentContext.logger) || new logging.NullLogger(),
parentContext,
};
this._context.push(context);
return schematic.call(
options.options,
of(new HostTree(this._host)),
{ logger: context.logger },
).pipe(
map(tree => optimize(tree)),
concatMap((tree: Tree) => {
return concat(
dryRunSink.commit(tree).pipe(ignoreElements()),
of(tree),
);
}),
concatMap((tree: Tree) => {
dryRunSubscriber.unsubscribe();
if (error) {
return throwError(new UnsuccessfulWorkflowExecution());
}
if (this._dryRun) {
return of();
}
return fsSink.commit(tree).pipe(defaultIfEmpty(), last());
}),
concatMap(() => {
if (this._dryRun) {
return of();
}
this._lifeCycle.next({ kind: 'post-tasks-start' });
return this._engine.executePostTasks()
.pipe(
tap({ complete: () => this._lifeCycle.next({ kind: 'post-tasks-end' }) }),
defaultIfEmpty(),
last(),
);
}),
tap({ complete: () => {
this._lifeCycle.next({ kind: 'workflow-end' });
this._context.pop();
if (this._context.length == 0) {
this._lifeCycle.next({ kind: 'end' });
}
}}),
);
}
}

@ -5,4 +5,5 @@
* 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
*/
export * from './base';
export * from './interface';

@ -5,176 +5,57 @@
* 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 { Path, logging, schema, virtualFs } from '@angular-devkit/core';
import { Path, schema, virtualFs } from '@angular-devkit/core';
import {
DryRunSink,
HostSink,
HostTree,
SchematicEngine,
Tree,
UnsuccessfulWorkflowExecution,
formats,
workflow,
} from '@angular-devkit/schematics'; // tslint:disable-line:no-implicit-dependencies
import { Observable, Subject, concat, of, throwError } from 'rxjs';
import { concatMap, defaultIfEmpty, ignoreElements, last, map, tap } from 'rxjs/operators';
import { DryRunEvent } from '../../src/sink/dryrun';
import { BuiltinTaskExecutor } from '../../tasks/node';
import { NodeModulesEngineHost } from '../node-module-engine-host';
import { validateOptionsWithSchema } from '../schema-option-transform';
export class NodeWorkflow implements workflow.Workflow {
protected _engine: SchematicEngine<{}, {}>;
protected _engineHost: NodeModulesEngineHost;
protected _registry: schema.CoreSchemaRegistry;
protected _reporter: Subject<DryRunEvent> = new Subject();
protected _lifeCycle: Subject<workflow.LifeCycleEvent> = new Subject();
protected _context: workflow.WorkflowExecutionContext[];
/**
* A workflow specifically for Node tools.
*/
export class NodeWorkflow extends workflow.BaseWorkflow {
constructor(
protected _host: virtualFs.Host,
protected _options: {
host: virtualFs.Host,
options: {
force?: boolean;
dryRun?: boolean;
root?: Path,
packageManager?: string;
},
) {
/**
* Create the SchematicEngine, which is used by the Schematic library as callbacks to load a
* Collection or a Schematic.
*/
this._engineHost = new NodeModulesEngineHost();
this._engine = new SchematicEngine(this._engineHost, this);
const engineHost = new NodeModulesEngineHost();
super({
host: host,
registry: new schema.CoreSchemaRegistry(formats.standardFormats),
engineHost: engineHost,
// Add support for schemaJson.
this._registry = new schema.CoreSchemaRegistry(formats.standardFormats);
this._engineHost.registerOptionsTransform(validateOptionsWithSchema(this._registry));
force: options.force,
dryRun: options.dryRun,
});
this._engineHost.registerTaskExecutor(
engineHost.registerOptionsTransform(validateOptionsWithSchema(this._registry));
engineHost.registerTaskExecutor(
BuiltinTaskExecutor.NodePackage,
{
allowPackageManagerOverride: true,
packageManager: this._options.packageManager,
rootDirectory: this._options.root,
packageManager: options.packageManager,
rootDirectory: options.root,
},
);
this._engineHost.registerTaskExecutor(
engineHost.registerTaskExecutor(
BuiltinTaskExecutor.RepositoryInitializer,
{
rootDirectory: this._options.root,
rootDirectory: options.root,
},
);
this._engineHost.registerTaskExecutor(BuiltinTaskExecutor.RunSchematic);
this._engineHost.registerTaskExecutor(BuiltinTaskExecutor.TslintFix);
engineHost.registerTaskExecutor(BuiltinTaskExecutor.RunSchematic);
engineHost.registerTaskExecutor(BuiltinTaskExecutor.TslintFix);
this._context = [];
}
get context(): Readonly<workflow.WorkflowExecutionContext> {
const maybeContext = this._context[this._context.length - 1];
if (!maybeContext) {
throw new Error('Cannot get context when workflow is not executing...');
}
return maybeContext;
}
get registry(): schema.SchemaRegistry {
return this._registry;
}
get reporter(): Observable<DryRunEvent> {
return this._reporter.asObservable();
}
get lifeCycle(): Observable<workflow.LifeCycleEvent> {
return this._lifeCycle.asObservable();
}
execute(
options: Partial<workflow.WorkflowExecutionContext> & workflow.RequiredWorkflowExecutionContext,
): Observable<void> {
const parentContext = this._context[this._context.length - 1];
if (!parentContext) {
this._lifeCycle.next({ kind: 'start' });
}
/** Create the collection and the schematic. */
const collection = this._engine.createCollection(options.collection);
// Only allow private schematics if called from the same collection.
const allowPrivate = options.allowPrivate
|| (parentContext && parentContext.collection === options.collection);
const schematic = collection.createSchematic(options.schematic, allowPrivate);
// We need two sinks if we want to output what will happen, and actually do the work.
// Note that fsSink is technically not used if `--dry-run` is passed, but creating the Sink
// does not have any side effect.
const dryRunSink = new DryRunSink(this._host, this._options.force);
const fsSink = new HostSink(this._host, this._options.force);
let error = false;
const dryRunSubscriber = dryRunSink.reporter.subscribe(event => {
this._reporter.next(event);
error = error || (event.kind == 'error');
});
this._lifeCycle.next({ kind: 'workflow-start' });
const context = {
...options,
debug: options.debug || false,
logger: options.logger || (parentContext && parentContext.logger) || new logging.NullLogger(),
parentContext,
};
this._context.push(context);
return schematic.call(
options.options,
of(new HostTree(this._host)),
{ logger: context.logger },
).pipe(
map(tree => Tree.optimize(tree)),
concatMap((tree: Tree) => {
return concat(
dryRunSink.commit(tree).pipe(ignoreElements()),
of(tree),
);
}),
concatMap((tree: Tree) => {
dryRunSubscriber.unsubscribe();
if (error) {
return throwError(new UnsuccessfulWorkflowExecution());
}
if (this._options.dryRun) {
return of();
}
return fsSink.commit(tree).pipe(defaultIfEmpty(), last());
}),
concatMap(() => {
if (this._options.dryRun) {
return of();
}
this._lifeCycle.next({ kind: 'post-tasks-start' });
return this._engine.executePostTasks()
.pipe(
tap({ complete: () => this._lifeCycle.next({ kind: 'post-tasks-end' }) }),
defaultIfEmpty(),
last(),
);
}),
tap({ complete: () => {
this._lifeCycle.next({ kind: 'workflow-end' });
this._context.pop();
if (this._context.length == 0) {
this._lifeCycle.next({ kind: 'end' });
}
}}),
);
}
}