Skip to content

Commit 90e0a65

Browse files
committed
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.
1 parent 63cf153 commit 90e0a65

File tree

6 files changed

+206
-146
lines changed

6 files changed

+206
-146
lines changed

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/angular_devkit/schematics/src/engine/engine.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { Url } from 'url';
1212
import { MergeStrategy } from '../tree/interface';
1313
import { NullTree } from '../tree/null';
1414
import { empty } from '../tree/static';
15-
import { Workflow } from '../workflow';
15+
import { Workflow } from '../workflow/interface';
1616
import {
1717
Collection,
1818
CollectionDescription,

packages/angular_devkit/schematics/src/engine/interface.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { logging } from '@angular-devkit/core';
99
import { Observable } from 'rxjs';
1010
import { Url } from 'url';
1111
import { FileEntry, MergeStrategy, Tree } from '../tree/interface';
12-
import { Workflow } from '../workflow';
12+
import { Workflow } from '../workflow/interface';
1313

1414

1515
export interface TaskConfiguration<T = {}> {
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import { logging, schema, virtualFs } from '@angular-devkit/core';
9+
import { Observable, Subject, concat, of, throwError } from 'rxjs';
10+
import { concatMap, defaultIfEmpty, ignoreElements, last, map, tap } from 'rxjs/operators';
11+
import { EngineHost, SchematicEngine } from '../engine';
12+
import { UnsuccessfulWorkflowExecution } from '../exception/exception';
13+
import { standardFormats } from '../formats';
14+
import { DryRunEvent, DryRunSink } from '../sink/dryrun';
15+
import { HostSink } from '../sink/host';
16+
import { HostTree } from '../tree/host-tree';
17+
import { Tree } from '../tree/interface';
18+
import { optimize } from '../tree/static';
19+
import {
20+
LifeCycleEvent,
21+
RequiredWorkflowExecutionContext,
22+
Workflow,
23+
WorkflowExecutionContext,
24+
} from './interface';
25+
26+
27+
export interface BaseWorkflowOptions {
28+
host: virtualFs.Host;
29+
engineHost: EngineHost<{}, {}>;
30+
registry?: schema.CoreSchemaRegistry;
31+
32+
force?: boolean;
33+
dryRun?: boolean;
34+
}
35+
36+
/**
37+
* Base class for workflows. Even without abstract methods, this class should not be used without
38+
* surrounding some initialization for the registry and host. This class only adds life cycle and
39+
* dryrun/force support. You need to provide any registry and task executors that you need to
40+
* support.
41+
* See {@see NodeWorkflow} implementation for how to make a specialized subclass of this.
42+
* TODO: add default set of CoreSchemaRegistry transforms. Once the job refactor is done, use that
43+
* as the support for tasks.
44+
*
45+
* @public
46+
*/
47+
export abstract class BaseWorkflow implements Workflow {
48+
protected _engine: SchematicEngine<{}, {}>;
49+
protected _engineHost: EngineHost<{}, {}>;
50+
protected _registry: schema.CoreSchemaRegistry;
51+
52+
protected _host: virtualFs.Host;
53+
54+
protected _reporter: Subject<DryRunEvent> = new Subject();
55+
protected _lifeCycle: Subject<LifeCycleEvent> = new Subject();
56+
57+
protected _context: WorkflowExecutionContext[];
58+
59+
protected _force: boolean;
60+
protected _dryRun: boolean;
61+
62+
constructor(options: BaseWorkflowOptions) {
63+
this._host = options.host;
64+
this._engineHost = options.engineHost;
65+
this._registry = options.registry || new schema.CoreSchemaRegistry(standardFormats);
66+
this._engine = new SchematicEngine(this._engineHost, this);
67+
68+
this._context = [];
69+
70+
this._force = options.force || false;
71+
this._dryRun = options.dryRun || false;
72+
}
73+
74+
get context(): Readonly<WorkflowExecutionContext> {
75+
const maybeContext = this._context[this._context.length - 1];
76+
if (!maybeContext) {
77+
throw new Error('Cannot get context when workflow is not executing...');
78+
}
79+
80+
return maybeContext;
81+
}
82+
get registry(): schema.SchemaRegistry {
83+
return this._registry;
84+
}
85+
get reporter(): Observable<DryRunEvent> {
86+
return this._reporter.asObservable();
87+
}
88+
get lifeCycle(): Observable<LifeCycleEvent> {
89+
return this._lifeCycle.asObservable();
90+
}
91+
92+
execute(
93+
options: Partial<WorkflowExecutionContext> & RequiredWorkflowExecutionContext,
94+
): Observable<void> {
95+
const parentContext = this._context[this._context.length - 1];
96+
97+
if (!parentContext) {
98+
this._lifeCycle.next({ kind: 'start' });
99+
}
100+
101+
/** Create the collection and the schematic. */
102+
const collection = this._engine.createCollection(options.collection);
103+
// Only allow private schematics if called from the same collection.
104+
const allowPrivate = options.allowPrivate
105+
|| (parentContext && parentContext.collection === options.collection);
106+
const schematic = collection.createSchematic(options.schematic, allowPrivate);
107+
108+
// We need two sinks if we want to output what will happen, and actually do the work.
109+
// Note that fsSink is technically not used if `--dry-run` is passed, but creating the Sink
110+
// does not have any side effect.
111+
const dryRunSink = new DryRunSink(this._host, this._force);
112+
const fsSink = new HostSink(this._host, this._force);
113+
114+
let error = false;
115+
const dryRunSubscriber = dryRunSink.reporter.subscribe(event => {
116+
this._reporter.next(event);
117+
error = error || (event.kind == 'error');
118+
});
119+
120+
this._lifeCycle.next({ kind: 'workflow-start' });
121+
122+
const context = {
123+
...options,
124+
debug: options.debug || false,
125+
logger: options.logger || (parentContext && parentContext.logger) || new logging.NullLogger(),
126+
parentContext,
127+
};
128+
this._context.push(context);
129+
130+
return schematic.call(
131+
options.options,
132+
of(new HostTree(this._host)),
133+
{ logger: context.logger },
134+
).pipe(
135+
map(tree => optimize(tree)),
136+
concatMap((tree: Tree) => {
137+
return concat(
138+
dryRunSink.commit(tree).pipe(ignoreElements()),
139+
of(tree),
140+
);
141+
}),
142+
concatMap((tree: Tree) => {
143+
dryRunSubscriber.unsubscribe();
144+
if (error) {
145+
return throwError(new UnsuccessfulWorkflowExecution());
146+
}
147+
148+
if (this._dryRun) {
149+
return of();
150+
}
151+
152+
return fsSink.commit(tree).pipe(defaultIfEmpty(), last());
153+
}),
154+
concatMap(() => {
155+
if (this._dryRun) {
156+
return of();
157+
}
158+
159+
this._lifeCycle.next({ kind: 'post-tasks-start' });
160+
161+
return this._engine.executePostTasks()
162+
.pipe(
163+
tap({ complete: () => this._lifeCycle.next({ kind: 'post-tasks-end' }) }),
164+
defaultIfEmpty(),
165+
last(),
166+
);
167+
}),
168+
tap({ complete: () => {
169+
this._lifeCycle.next({ kind: 'workflow-end' });
170+
this._context.pop();
171+
172+
if (this._context.length == 0) {
173+
this._lifeCycle.next({ kind: 'end' });
174+
}
175+
}}),
176+
);
177+
}
178+
}

packages/angular_devkit/schematics/src/workflow/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.io/license
77
*/
8+
export * from './base';
89
export * from './interface';

0 commit comments

Comments
 (0)