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:
parent
84ec3022c2
commit
2ba1f16295
package-lock.json
packages/angular_devkit/schematics
4703
package-lock.json
generated
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 = {}> {
|
||||
|
178
packages/angular_devkit/schematics/src/workflow/base.ts
Normal file
178
packages/angular_devkit/schematics/src/workflow/base.ts
Normal file
@ -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' });
|
||||
}
|
||||
}}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user