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

Commit 18b3878

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 6c93339 commit 18b3878

File tree

14 files changed

+315
-245
lines changed

14 files changed

+315
-245
lines changed

lib/packages.js

+3-3
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/README.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ The tooling is responsible for the following tasks:
3131
1. Create a Sink and commit the result of the schematics to the Sink. Many sinks are provided by the library; FileSystemSink and DryRunSink are examples.
3232
1. Output any logs propagated by the library, including debugging information.
3333

34+
The tooling API is composed of the following pieces:
35+
36+
## Engine
37+
The `SchematicEngine` is responsible for loading and constructing `Collection`s and `Schematics`'. When creating an engine, the tooling provides an `EngineHost` interface that understands how to create a `CollectionDescription` from a name, and how to create a `Schematic
38+
3439
# Examples
3540

3641
## Simple
@@ -50,7 +55,7 @@ export default function MySchematic(options: any) {
5055
A few things from this example:
5156

5257
1. The function receives the list of options from the tooling.
53-
2. It returns a [`Rule`](src/engine/interface.ts#L73), which is a transformation from a `Tree` to another `Tree`.
58+
1. It returns a [`Rule`](src/engine/interface.ts#L73), which is a transformation from a `Tree` to another `Tree`.
5459

5560

5661

packages/schematics/src/engine/collection.ts

+9-45
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

-23
This file was deleted.

packages/schematics/src/engine/engine.ts

+32-17
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 _options: 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._options.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._options.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

+67-47
Original file line numberDiff line numberDiff line change
@@ -11,78 +11,98 @@ import {Observable} from 'rxjs/Observable';
1111
import {Url} from 'url';
1212

1313

14-
export interface EngineHost {
15-
loadCollection(name: string): CollectionDescription | null;
16-
loadSchematic<T>(name: string,
17-
collection: Collection,
18-
options: T): ResolvedSchematicDescription | null;
19-
}
20-
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+
};
2122

22-
export interface Schematic {
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>;
2330
readonly name: string;
24-
readonly description: string;
25-
readonly path: string;
26-
readonly collection: Collection;
31+
};
2732

28-
call(host: Observable<Tree>, parentContext: Partial<SchematicContext>): Observable<Tree>;
29-
}
3033

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>;
3143

32-
export interface ProtocolHandler {
33-
(url: Url): Source;
44+
readonly defaultMergeStrategy?: MergeStrategy;
3445
}
3546

3647

37-
38-
export interface Engine {
39-
createCollection(name: string): Collection | null;
40-
createSchematic<T>(name: string, collection: Collection, options: T): Schematic | 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>;
4162
registerUrlProtocolHandler(protocol: string, handler: ProtocolHandler): void;
4263
createSourceFromUrl(url: Url): Source;
64+
65+
readonly defaultMergeStrategy: MergeStrategy;
4366
}
4467

4568

46-
export interface Collection {
47-
readonly engine: Engine;
48-
readonly name: string;
49-
readonly path: string;
69+
export interface Schematic<CollectionT, SchematicT> {
70+
readonly description: SchematicDescription<CollectionT, SchematicT>;
71+
readonly collection: Collection<CollectionT, SchematicT>;
5072

51-
listSchematicNames(): string[];
52-
getSchematicDescription(name: string): SchematicDescription | null;
53-
createSchematic<T>(name: string, options: T): Schematic;
73+
call<T>(options: T, host: Observable<Tree>): Observable<Tree>;
5474
}
5575

5676

57-
export interface SchematicContext {
58-
readonly engine: Engine;
59-
readonly schematic: Schematic;
60-
readonly host: Observable<Tree>;
61-
readonly strategy: MergeStrategy;
77+
export interface ProtocolHandler {
78+
(url: Url): Source;
6279
}
6380

6481

65-
export interface CollectionDescription {
66-
readonly path: string;
67-
readonly name?: string;
68-
readonly version?: string;
69-
readonly schematics: { [name: string]: SchematicDescription };
70-
}
7182

72-
export interface SchematicDescription {
73-
readonly factory: string;
74-
readonly description: string;
75-
readonly schema?: string;
83+
export interface Collection<CollectionT, SchematicT> {
84+
readonly name: string;
85+
readonly description: CollectionDescription<CollectionT>;
86+
87+
createSchematic(name: string): Schematic<CollectionT, SchematicT>;
7688
}
7789

7890

79-
export interface ResolvedSchematicDescription extends SchematicDescription {
80-
readonly name: string;
81-
readonly path: string;
82-
readonly rule: Rule;
91+
export interface TypedSchematicContext<CollectionT, SchematicT> {
92+
readonly engine: Engine<CollectionT, SchematicT>;
93+
readonly schematic: Schematic<CollectionT, SchematicT>;
94+
readonly host: Observable<Tree>;
95+
readonly strategy: MergeStrategy;
8396
}
97+
export type SchematicContext = TypedSchematicContext<any, any>;
98+
8499

85100
export type RuleFactory<T> = (options: T) => Rule;
86101

87-
export type Source = (context: SchematicContext) => Tree | Observable<Tree>;
88-
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>;

0 commit comments

Comments
 (0)