Skip to content
This repository was archived by the owner on Apr 4, 2025. It is now read-only.

Commit 974e60a

Browse files
committed
refactor: refactor interfaces
Better separation of concern between tooling and schematic library. Engine, Collection and Schematic now take a generic type that can add tooling-specific metadata to each parts, in a type-safe manner. Context exposed to schematics is a context of <any, any>, while the tool should use TypedContext for better support internally. The Engine Host now only deals with Descriptions of collection and schematics, does not deal with actual implementation. Renamed CliEngineHost to NodeModules, since its kind of reusable if you only implement a NodeModules schematic tool. Tempted to move this into the schematics library as its highly reusable.
1 parent 2b04bd4 commit 974e60a

File tree

13 files changed

+307
-244
lines changed

13 files changed

+307
-244
lines changed

lib/packages.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ const packages =
2323
glob.sync(path.join(packageRoot, '*/package.json')),
2424
glob.sync(path.join(packageRoot, '*/*/package.json')))
2525
.filter(p => !p.match(/blueprints/))
26-
.map(pkgPath => path.relative(packageRoot, path.dirname(pkgPath)))
27-
.map(pkgName => {
28-
return { name: pkgName, root: path.join(packageRoot, pkgName) };
26+
.map(pkgPath => [pkgPath, path.relative(packageRoot, path.dirname(pkgPath))])
27+
.map(([pkgPath, pkgName]) => {
28+
return { name: pkgName, root: path.dirname(pkgPath) };
2929
})
3030
.reduce((packages, pkg) => {
3131
let pkgJson = JSON.parse(fs.readFileSync(path.join(pkg.root, 'package.json'), 'utf8'));

packages/schematics/src/engine/collection.ts

Lines changed: 9 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -6,55 +6,19 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88
import {SchematicEngine} from './engine';
9-
import {Collection, CollectionDescription, Schematic, SchematicDescription} from './interface';
10-
import {BaseException} from '../exception/exception';
9+
import {Collection, CollectionDescription, Schematic} from './interface';
1110

1211

13-
14-
export class UnknownSchematicNameException extends BaseException {
15-
constructor(collection: string, name: string) {
16-
super(`Schematic named "${name}" could not be found in collection "${collection}".`);
17-
}
18-
}
19-
export class InvalidSchematicException extends BaseException {
20-
constructor(name: string) { super(`Invalid schematic: "${name}".`); }
21-
}
22-
23-
24-
export class CollectionImpl implements Collection {
25-
private _schematics: { [name: string]: (options: any) => Schematic | null } = {};
26-
27-
constructor(private _description: CollectionDescription,
28-
private _engine: SchematicEngine) {
29-
Object.keys(this._description.schematics).forEach(name => {
30-
this._schematics[name] = (options: any) => this._engine.createSchematic(name, this, options);
31-
});
32-
}
33-
34-
get engine() { return this._engine; }
35-
get name() { return this._description.name || '<unknown>'; }
36-
get path() { return this._description.path || '<unknown>'; }
37-
38-
listSchematicNames(): string[] {
39-
return Object.keys(this._schematics);
40-
}
41-
42-
getSchematicDescription(name: string): SchematicDescription | null {
43-
if (!(name in this._description.schematics)) {
44-
return null;
45-
}
46-
return this._description.schematics[name];
12+
export class CollectionImpl<CollectionT, SchematicT>
13+
implements Collection<CollectionT, SchematicT> {
14+
constructor(private _description: CollectionDescription<CollectionT>,
15+
private _engine: SchematicEngine<CollectionT, SchematicT>) {
4716
}
4817

49-
createSchematic<T>(name: string, options: T): Schematic {
50-
if (!(name in this._schematics)) {
51-
throw new UnknownSchematicNameException(this.name, name);
52-
}
18+
get description() { return this._description; }
19+
get name() { return this.description.name || '<unknown>'; }
5320

54-
const schematic = this._schematics[name](options);
55-
if (!schematic) {
56-
throw new InvalidSchematicException(name);
57-
}
58-
return schematic;
21+
createSchematic(name: string): Schematic<CollectionT, SchematicT> {
22+
return this._engine.createSchematic(name, this);
5923
}
6024
}

packages/schematics/src/engine/context.ts

Lines changed: 0 additions & 23 deletions
This file was deleted.

packages/schematics/src/engine/engine.ts

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,31 +8,39 @@
88
import {CollectionImpl} from './collection';
99
import {
1010
Collection,
11-
Engine, EngineHost,
11+
Engine,
12+
EngineHost,
1213
ProtocolHandler,
1314
Schematic,
14-
SchematicContext,
15-
Source
15+
Source,
16+
TypedSchematicContext
1617
} from './interface';
1718
import {SchematicImpl} from './schematic';
1819
import {BaseException} from '../exception/exception';
1920
import {empty} from '../tree/static';
2021

2122
import {Url, parse, format} from 'url';
23+
import {MergeStrategy} from '../tree/interface';
2224

2325

24-
export class InvalidSourceUrlException extends BaseException {
25-
constructor(url: string) { super(`Invalid source url: "${url}".`); }
26-
}
2726
export class UnknownUrlSourceProtocol extends BaseException {
2827
constructor(url: string) { super(`Unknown Protocol on url "${url}".`); }
2928
}
3029

30+
export class UnknownCollectionException extends BaseException {
31+
constructor(name: string) { super(`Unknown collection "${name}".`); }
32+
}
33+
export class UnknownSchematicException extends BaseException {
34+
constructor(name: string, collection: Collection<any, any>) {
35+
super(`Schematic "${name}" not found in collection "${collection.name}".`);
36+
}
37+
}
38+
3139

32-
export class SchematicEngine implements Engine {
40+
export class SchematicEngine<CollectionT, SchematicT> implements Engine<CollectionT, SchematicT> {
3341
private _protocolMap = new Map<string, ProtocolHandler>();
3442

35-
constructor(private _host: EngineHost) {
43+
constructor(private _host: EngineHost<CollectionT, SchematicT>) {
3644
// Default implementations.
3745
this._protocolMap.set('null', () => {
3846
return () => empty();
@@ -41,26 +49,33 @@ export class SchematicEngine implements Engine {
4149
// Make a copy, change the protocol.
4250
const fileUrl = parse(format(url));
4351
fileUrl.protocol = 'file:';
44-
return (context: SchematicContext) => context.engine.createSourceFromUrl(fileUrl)(context);
52+
return (context: TypedSchematicContext<CollectionT, SchematicT>) => {
53+
return context.engine.createSourceFromUrl(fileUrl)(context);
54+
};
4555
});
4656
}
4757

48-
createCollection(name: string): Collection | null {
49-
const description = this._host.loadCollection(name);
58+
get defaultMergeStrategy() { return this._host.defaultMergeStrategy || MergeStrategy.Default; }
59+
60+
createCollection(name: string): Collection<CollectionT, SchematicT> {
61+
const description = this._host.createCollectionDescription(name);
5062
if (!description) {
51-
return null;
63+
throw new UnknownCollectionException(name);
5264
}
5365

54-
return new CollectionImpl(description, this);
66+
return new CollectionImpl<CollectionT, SchematicT>(description, this);
5567
}
5668

57-
createSchematic<T>(name: string, collection: Collection, options: T): Schematic | null {
58-
const description = this._host.loadSchematic<T>(name, collection, options);
69+
createSchematic(
70+
name: string,
71+
collection: Collection<CollectionT, SchematicT>): Schematic<CollectionT, SchematicT> {
72+
const description = this._host.createSchematicDescription(name, collection.description);
5973
if (!description) {
60-
return null;
74+
throw new UnknownSchematicException(name, collection);
6175
}
76+
const factory = this._host.getSchematicRuleFactory(description, collection.description);
6277

63-
return new SchematicImpl(description, collection);
78+
return new SchematicImpl<CollectionT, SchematicT>(description, factory, collection, this);
6479
}
6580

6681
registerUrlProtocolHandler(protocol: string, handler: ProtocolHandler) {

packages/schematics/src/engine/interface.ts

Lines changed: 65 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,66 @@ import {Observable} from 'rxjs/Observable';
1111
import {Url} from 'url';
1212

1313

14-
export interface EngineHost {
15-
loadCollection<T>(name: string): CollectionDescription<T> | null;
16-
loadSchematic<CT, ST>(name: string,
17-
collection: CollectionDescription<CT>): SchematicDescription<ST> | null;
14+
/**
15+
* The description (metadata) of a collection. This type contains every information the engine
16+
* needs to run. The CollectionT type parameter contains additional metadata that you want to
17+
* store while remaining type-safe.
18+
*/
19+
export type CollectionDescription<CollectionT extends {}> = CollectionT & {
20+
readonly name: string;
21+
};
22+
23+
/**
24+
* The description (metadata) of a schematic. This type contains every information the engine
25+
* needs to run. The SchematicT and CollectionT type parameters contain additional metadata
26+
* that you want to store while remaining type-safe.
27+
*/
28+
export type SchematicDescription<CollectionT extends {}, SchematicT extends {}> = SchematicT & {
29+
readonly collection: CollectionDescription<CollectionT>;
30+
readonly name: string;
31+
};
32+
33+
34+
export interface EngineHost<CollectionT extends {}, SchematicT extends {}> {
35+
createCollectionDescription(name: string): CollectionDescription<CollectionT> | null;
36+
createSchematicDescription(
37+
name: string,
38+
collection: CollectionDescription<CollectionT>):
39+
SchematicDescription<CollectionT, SchematicT> | null;
40+
getSchematicRuleFactory<OptionT>(
41+
schematic: SchematicDescription<CollectionT, SchematicT>,
42+
collection: CollectionDescription<CollectionT>): RuleFactory<OptionT>;
43+
44+
readonly defaultMergeStrategy?: MergeStrategy;
1845
}
1946

2047

21-
export interface Engine {
22-
createCollection<T>(name: string): Collection<T> | null;
23-
createSchematic<CT, ST>(name: string, collection: Collection<CT>): Schematic<CT, ST> | null;
48+
/**
49+
* The root Engine for creating and running schematics and collections. Everything related to
50+
* a schematic execution starts from this interface.
51+
*
52+
* CollectionT is, by default, a generic Collection metadata type. This is used throughout the
53+
* engine typings so that you can use a type that's merged into descriptions, while being type-safe.
54+
*
55+
* SchematicT is a type that contains additional typing for the Schematic Description.
56+
*/
57+
export interface Engine<CollectionT extends {}, SchematicT extends {}> {
58+
createCollection(name: string): Collection<CollectionT, SchematicT>;
59+
createSchematic(
60+
name: string,
61+
collection: Collection<CollectionT, SchematicT>): Schematic<CollectionT, SchematicT>;
2462
registerUrlProtocolHandler(protocol: string, handler: ProtocolHandler): void;
2563
createSourceFromUrl(url: Url): Source;
64+
65+
readonly defaultMergeStrategy: MergeStrategy;
2666
}
2767

2868

29-
export interface Schematic<CT, ST> {
30-
readonly description: SchematicDescription<ST>;
31-
readonly collection: Collection<CT>;
69+
export interface Schematic<CollectionT, SchematicT> {
70+
readonly description: SchematicDescription<CollectionT, SchematicT>;
71+
readonly collection: Collection<CollectionT, SchematicT>;
3272

33-
call(host: Observable<Tree>, parentContext: SchematicContext | null): Observable<Tree>;
73+
call<T>(options: T, host: Observable<Tree>): Observable<Tree>;
3474
}
3575

3676

@@ -40,34 +80,29 @@ export interface ProtocolHandler {
4080

4181

4282

43-
export interface Collection<T> {
83+
export interface Collection<CollectionT, SchematicT> {
4484
readonly name: string;
45-
readonly description: CollectionDescription<T>;
85+
readonly description: CollectionDescription<CollectionT>;
4686

47-
listSchematicNames(): string[];
48-
getSchematicDescription<T>(name: string): SchematicDescription<T> | null;
49-
createSchematic(name: string): Schematic;
87+
createSchematic(name: string): Schematic<CollectionT, SchematicT>;
5088
}
5189

5290

53-
export interface SchematicContext {
54-
readonly engine: Engine;
55-
readonly schematic: Schematic;
91+
export interface TypedSchematicContext<CollectionT, SchematicT> {
92+
readonly engine: Engine<CollectionT, SchematicT>;
93+
readonly schematic: Schematic<CollectionT, SchematicT>;
5694
readonly host: Observable<Tree>;
5795
readonly strategy: MergeStrategy;
5896
}
59-
60-
61-
export type CollectionDescription<T> = T & {
62-
readonly name: string;
63-
};
64-
export type SchematicDescription<T, OT> = T & {
65-
readonly name: string;
66-
readonly factory: RuleFactory<OT>;
67-
};
97+
export type SchematicContext = TypedSchematicContext<any, any>;
6898

6999

70100
export type RuleFactory<T> = (options: T) => Rule;
71101

72-
export type Source = (context: SchematicContext) => Tree | Observable<Tree>;
73-
export type Rule = (tree: Tree, context: SchematicContext) => Tree | Observable<Tree>;
102+
/**
103+
* We obfuscate the context of Source and Rule because the schematic implementation should not
104+
* know which types is the schematic or collection metadata, as they are both tooling specific.
105+
*/
106+
export type Source = (context: TypedSchematicContext<any, any>) => Tree | Observable<Tree>;
107+
export type Rule = (tree: Tree,
108+
context: TypedSchematicContext<any, any>) => Tree | Observable<Tree>;

packages/schematics/src/engine/schematic.ts

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,15 @@
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-
import {SchematicContextImpl} from './context';
9-
import {Collection, ResolvedSchematicDescription, Schematic, SchematicContext} from './interface';
10-
import {MergeStrategy, Tree} from '../tree/interface';
8+
import {
9+
Collection,
10+
Engine,
11+
RuleFactory,
12+
Schematic,
13+
SchematicDescription,
14+
TypedSchematicContext
15+
} from './interface';
16+
import {Tree} from '../tree/interface';
1117
import {BaseException} from '../exception/exception';
1218

1319
import {Observable} from 'rxjs/Observable';
@@ -17,30 +23,35 @@ import 'rxjs/add/operator/concatMap';
1723

1824

1925
export class InvalidSchematicsNameException extends BaseException {
20-
constructor(path: string, name: string) {
21-
super(`Schematics at path "${path}" has invalid name "${name}".`);
26+
constructor(name: string) {
27+
super(`Schematics has invalid name: "${name}".`);
2228
}
2329
}
2430

2531

26-
export class SchematicImpl implements Schematic {
27-
constructor(private _descriptor: ResolvedSchematicDescription,
28-
private _collection: Collection) {
29-
if (!_descriptor.name.match(/^[-_.a-zA-Z0-9]+$/)) {
30-
throw new InvalidSchematicsNameException(_descriptor.path, _descriptor.name);
32+
export class SchematicImpl<CollectionT, SchematicT> implements Schematic<CollectionT, SchematicT> {
33+
constructor(private _description: SchematicDescription<CollectionT, SchematicT>,
34+
private _factory: RuleFactory<any>,
35+
private _collection: Collection<CollectionT, SchematicT>,
36+
private _engine: Engine<CollectionT, SchematicT>) {
37+
if (!_description.name.match(/^[-_.a-zA-Z0-9]+$/)) {
38+
throw new InvalidSchematicsNameException(_description.name);
3139
}
3240
}
3341

34-
get name() { return this._descriptor.name; }
35-
get description() { return this._descriptor.description; }
36-
get path() { return this._descriptor.path; }
42+
get description() { return this._description; }
3743
get collection() { return this._collection; }
3844

39-
call(host: Observable<Tree>, parentContext: Partial<SchematicContext>): Observable<Tree> {
40-
const context = new SchematicContextImpl(this, host,
41-
parentContext.strategy || MergeStrategy.Default);
45+
call<OptionT>(options: OptionT, host: Observable<Tree>): Observable<Tree> {
46+
let context: TypedSchematicContext<CollectionT, SchematicT> = {
47+
engine: this._engine,
48+
schematic: this,
49+
host,
50+
strategy: this._engine.defaultMergeStrategy
51+
};
52+
4253
return host.concatMap(tree => {
43-
const result = this._descriptor.rule(tree, context);
54+
const result = this._factory(options)(tree, context);
4455
if (result instanceof Observable) {
4556
return result;
4657
} else {

0 commit comments

Comments
 (0)