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

Commit 39f093d

Browse files
committed
.
1 parent 77d3cb3 commit 39f093d

21 files changed

+304
-97
lines changed

packages/schematics/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ ts_library(
1515
"//packages/schematics/src/rules",
1616
"//packages/schematics/src/sink",
1717
"//packages/schematics/src/tree",
18+
"//packages/schematics/tooling",
1819
],
1920
tsconfig = "//:tsconfig.json",
2021
visibility = [ "//visibility:public" ],

packages/schematics/README.md

Lines changed: 97 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,94 @@ The tooling is responsible for the following tasks:
2626

2727
1. Create the Schematic Engine, and pass in a Collection and Schematic loader.
2828
1. Understand and respect the Schematics metadata and dependencies between collections. Schematics can refer to dependencies, and it's the responsibility of the tool to honor those dependencies. The reference CLI uses NPM packages for its collections.
29-
1. Create the Options object. Options can be anything, but the schematics can specify a JSON Schema that should be respected. The reference CLI, for example, parse the arguments as a JSON object and validate it with the Schema specified by the collection.
29+
1. Create the Options object. Options can be anything, but the schematics can specify a JSON Schema that should be respected. The reference CLI, for example, parses the arguments as a JSON object and validate it with the Schema specified by the collection.
3030
1. Call the schematics with the original Tree. The tree should represent the initial state of the filesystem. The reference CLI uses the current directory for this.
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

3434
The tooling API is composed of the following pieces:
3535

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
36+
## EngineHost
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, how to create a `SchematicDescription` from a `CollectionDescription` and a name, as well as how to create the `Rule` factory for that Schematics. Both of which are information necessary for the `Engine` to work properly.
38+
39+
All Description interfaces are generics that take interfaces as type parameters. Those interfaces can be used by the tooling to store additional information in the `CollectionDescription` and the `SchematicDescription`. The descriptions returned by the host are guaranteed to be the same objects when passing them as input.
40+
41+
### CollectionDescription
42+
A `CollectionDescription` is the minimum amount of information that `Engine` needs to create a collection. It is currently only a `name`, which is used to validate the collection and cache it.
43+
44+
### SchematicDescription
45+
A `SchematicDescription` is the minimum amount of information that `Engine` needs to create a schematic. It is currently a `name` (which is used to be cached), and a `CollectionDescription`. The collection description is asserted to be the same description as passed in. It is used later on to reference collections when schematics are created by name only.
46+
47+
### Source from URL
48+
It is possible for schematics to create `Source`s from a URL. These are useful when we want to load a list of template files. There are 3 default URL protocols supported by the Engine:
49+
50+
- `null:` returns a Tree that's invalid and will throw exceptions.
51+
- `empty:` returns a Tree that's empty.
52+
- `host:` returns a copy of the host passed to this schematic, from the context.
53+
54+
### RuleFactory
55+
The other method necessary to resolve a schematics is the `RuleFactory`, a function that takes an option argument and returns a `Rule`. That factory is created from both descriptions by the host and the result will be called by the Engine when necessary. Please note that the engine cache this `RuleFactory` based on both descriptions, so if a schematic is created twice the `getSchematicRuleFactory` host function will only be called once.
56+
57+
### Default MergeStrategy
58+
The `EngineHost` can have an optional `defaultMergeStrategy` to specify how the tooling wants to set the default `MergeStrategy`. This will be used if schematics don't specify a merge strategy on their own.
59+
60+
## EngineHost Implementations
61+
62+
### NodeModulesEngineHost
63+
The Schematics library provides an EngineHost that understands NPM node modules, using node modules to define collections and schematics.
64+
65+
This engine host use the following conventions:
66+
67+
1. A node package needs to define a `schematics` key in its `package.json`, which points to a JSON file that contains collection metadata. This metadata is of the follpwing type:
68+
69+
```typescript
70+
interface NodeModuleCollectionJson {
71+
name: string;
72+
version?: string;
73+
description: string;
74+
schematics: {
75+
[name: string]: {
76+
factory: string;
77+
description: string;
78+
schema?: string;
79+
}
80+
};
81+
}
82+
```
83+
84+
The name must be the same name as the NPM package.
85+
1. The `schematics` dictionary is used to resolve schematics information.
86+
- The `factory` is a string of the form `modulePath#ExportName`. It is resolved relative to the collection JSON file, and the `ExportName` will be `default` if not specified.
87+
- The `schema` is a string that points to a JSON Schema file (relative to the collection JSON file).
88+
- The `description` field is a description that can be used by the tooling to show to the user.
89+
1. The `RuleFactory` is loaded from the `factory` field above by using `require()`. The `SchematicDescription` contains the name and more information necessary for the Host to resolve more.
90+
1. This EngineHost also registers some URLs:
91+
- `file:` (or not specifying a protocol) supports loading a file system from the disk.
92+
93+
# Schematics
94+
A schematics is defined by the `RuleFactory` and its `SchematicDescription`, which contains the name and collection.
95+
96+
## Tree
97+
By definition, a schematic is a transformation between a `Tree` and another `Tree`. It receives a host `Tree`, and applies a list of actions to it, potentially returning it at the end.
98+
99+
## Action
100+
A tree is transformed by staging actions, which can write over a file, create new files, rename or delete existing files.
101+
102+
## Branching
103+
A tree can be branched, keeping its history, then adding actions on top of it. Two trees that are being merged will ignore their common history.
104+
105+
## Merging
106+
Merging two trees results in a tree containing all actions. If two actions apply on the same path, it is automatically considered a conflict and needs to be resolved.
107+
108+
### Conflicts
109+
Merge conflicts are resolved using the chosen `MergeStrategy` (with the default set by the tooling):
110+
111+
1. `MergeStrategy.Error`. Throw an exception and stops creating the schematic.
112+
1. `MergeStrategy.Overwrite`. The action from the last merge argument is preferred.
113+
1. `MergeStrategy.ContentOnly`. Creation or Renaming the same file will throw an exception, but overwriting its content will resolve as if `MergeStrategy.Overwrite` is chosen.
114+
115+
## Optimize
116+
Optimizing a tree results in the tree with a smaller staging; actions that overrules each other within the same tree are removed or simplified. The history of the tree is NOT preserved, but only the staged actions are changed.
38117

39118
# Examples
40119

@@ -52,11 +131,20 @@ export default function MySchematic(options: any) {
52131
}
53132
```
54133

55-
A few things from this example:
56-
57-
1. The function receives the list of options from the tooling.
58-
1. It returns a [`Rule`](src/engine/interface.ts#L73), which is a transformation from a `Tree` to another `Tree`.
134+
## Templated Source
135+
An example of a simple Schematics which reads a directory and apply templates to its content and path.
59136

137+
```typescript
138+
import {apply, mergeWith, template, url} from '@angular/schematics';
139+
140+
export default function(options: any) {
141+
return mergeWith([
142+
apply(url('./files'), [
143+
template({ utils: stringUtils, ...options })
144+
])
145+
]);
146+
};
147+
```
60148

61149

62150
# Future Work
@@ -66,4 +154,5 @@ Schematics is not done yet. Here's a list of things we are considering:
66154
* Smart defaults for Options. Having a JavaScript function for default values based on other default values.
67155
* Prompt for input options. This should only be prompted for the original schematics, dependencies to other schematics should not trigger another prompting.
68156
* Tasks for running tooling-specific jobs before and after a schematics has been scaffolded. Such tasks can involve initialize git, or npm install. A specific list of tasks should be provided by the tool, with unsupported tasks generating an error.
69-
157+
* Better URL support for more consistency. Right now tools define their own URLs without having consistency between two tools, which means that there is still some cohesion between the schematic and the tool.
158+
* Annotation support. Annotations are being designed right now, but they will be a type-safe way to attach metadata to a file that is updated if the file changes content. Such Annotation could tell if a file is, e.g., a test file, or binary, or the annotation could be the TypeScript AST of the file itself.

packages/schematics/src/engine/engine.ts

Lines changed: 49 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,18 @@ import {
1010
Collection,
1111
Engine,
1212
EngineHost,
13-
ProtocolHandler,
1413
Schematic,
1514
Source,
1615
TypedSchematicContext
1716
} from './interface';
1817
import {SchematicImpl} from './schematic';
1918
import {BaseException} from '../exception/exception';
20-
import {empty} from '../tree/static';
19+
import {MergeStrategy} from '../tree/interface';
20+
import {NullTree} from '../tree/null';
21+
import {branch, empty} from '../tree/static';
2122

2223
import {Url, parse, format} from 'url';
23-
import {MergeStrategy} from '../tree/interface';
24+
import 'rxjs/add/operator/map';
2425

2526

2627
export class UnknownUrlSourceProtocol extends BaseException {
@@ -38,56 +39,77 @@ export class UnknownSchematicException extends BaseException {
3839

3940

4041
export class SchematicEngine<CollectionT, SchematicT> implements Engine<CollectionT, SchematicT> {
41-
private _protocolMap = new Map<string, ProtocolHandler>();
42+
private _collectionCache = new Map<string, CollectionImpl<CollectionT, SchematicT>>();
43+
private _schematicCache
44+
= new Map<string, Map<string, SchematicImpl<CollectionT, SchematicT>>>();
4245

4346
constructor(private _host: EngineHost<CollectionT, SchematicT>) {
44-
// Default implementations.
45-
this._protocolMap.set('null', () => {
46-
return () => empty();
47-
});
48-
this._protocolMap.set('', (url: Url) => {
49-
// Make a copy, change the protocol.
50-
const fileUrl = parse(format(url));
51-
fileUrl.protocol = 'file:';
52-
return (context: TypedSchematicContext<CollectionT, SchematicT>) => {
53-
return context.engine.createSourceFromUrl(fileUrl)(context);
54-
};
55-
});
5647
}
5748

5849
get defaultMergeStrategy() { return this._host.defaultMergeStrategy || MergeStrategy.Default; }
5950

6051
createCollection(name: string): Collection<CollectionT, SchematicT> {
52+
let collection = this._collectionCache.get(name);
53+
if (collection) {
54+
return collection;
55+
}
56+
6157
const description = this._host.createCollectionDescription(name);
6258
if (!description) {
6359
throw new UnknownCollectionException(name);
6460
}
6561

66-
return new CollectionImpl<CollectionT, SchematicT>(description, this);
62+
collection = new CollectionImpl<CollectionT, SchematicT>(description, this);
63+
this._collectionCache.set(name, collection);
64+
this._schematicCache.set(name, new Map());
65+
return collection;
6766
}
6867

6968
createSchematic(
7069
name: string,
7170
collection: Collection<CollectionT, SchematicT>): Schematic<CollectionT, SchematicT> {
71+
const collectionImpl = this._collectionCache.get(collection.name);
72+
const schematicMap = this._schematicCache.get(collection.name);
73+
if (!collectionImpl || !schematicMap || collectionImpl !== collection) {
74+
// This is weird, maybe the collection was created by another engine?
75+
throw new UnknownCollectionException(collection.name);
76+
}
77+
78+
let schematic = schematicMap.get(name);
79+
if (schematic) {
80+
return schematic;
81+
}
82+
7283
const description = this._host.createSchematicDescription(name, collection.description);
7384
if (!description) {
7485
throw new UnknownSchematicException(name, collection);
7586
}
7687
const factory = this._host.getSchematicRuleFactory(description, collection.description);
88+
schematic = new SchematicImpl<CollectionT, SchematicT>(description, factory, collection, this);
7789

78-
return new SchematicImpl<CollectionT, SchematicT>(description, factory, collection, this);
79-
}
80-
81-
registerUrlProtocolHandler(protocol: string, handler: ProtocolHandler) {
82-
this._protocolMap.set(protocol, handler);
90+
schematicMap.set(name, schematic);
91+
return schematic;
8392
}
8493

8594
createSourceFromUrl(url: Url): Source {
86-
const protocol = (url.protocol || '').replace(/:$/, '');
87-
const handler = this._protocolMap.get(protocol);
88-
if (!handler) {
89-
throw new UnknownUrlSourceProtocol(url.toString());
95+
switch (url.protocol) {
96+
case 'null:': return () => new NullTree();
97+
case 'empty:': return () => empty();
98+
case 'host:': return (context: TypedSchematicContext<CollectionT, SchematicT>) => {
99+
return context.host.map(tree => branch(tree));
100+
};
101+
case '':
102+
const fileUrl = parse(format(url));
103+
fileUrl.protocol = 'file:';
104+
return (context: TypedSchematicContext<CollectionT, SchematicT>) => {
105+
return context.engine.createSourceFromUrl(fileUrl)(context);
106+
};
107+
default:
108+
const hostSource = this._host.createSourceFromUrl(url);
109+
if (!hostSource) {
110+
throw new UnknownUrlSourceProtocol(url.toString());
111+
}
112+
return hostSource;
90113
}
91-
return handler(url);
92114
}
93115
}

packages/schematics/src/engine/interface.ts

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ export type SchematicDescription<CollectionT extends {}, SchematicT extends {}>
3131
};
3232

3333

34+
/**
35+
* The Host for the Engine. Specifically, the piece of the tooling responsible for resolving
36+
* collections and schematics descriptions. The SchematicT and CollectionT type parameters contain
37+
* additional metadata that you want to store while remaining type-safe.
38+
*/
3439
export interface EngineHost<CollectionT extends {}, SchematicT extends {}> {
3540
createCollectionDescription(name: string): CollectionDescription<CollectionT> | null;
3641
createSchematicDescription(
@@ -40,6 +45,7 @@ export interface EngineHost<CollectionT extends {}, SchematicT extends {}> {
4045
getSchematicRuleFactory<OptionT>(
4146
schematic: SchematicDescription<CollectionT, SchematicT>,
4247
collection: CollectionDescription<CollectionT>): RuleFactory<OptionT>;
48+
createSourceFromUrl(url: Url): Source | null;
4349

4450
readonly defaultMergeStrategy?: MergeStrategy;
4551
}
@@ -59,27 +65,16 @@ export interface Engine<CollectionT extends {}, SchematicT extends {}> {
5965
createSchematic(
6066
name: string,
6167
collection: Collection<CollectionT, SchematicT>): Schematic<CollectionT, SchematicT>;
62-
registerUrlProtocolHandler(protocol: string, handler: ProtocolHandler): void;
6368
createSourceFromUrl(url: Url): Source;
6469

6570
readonly defaultMergeStrategy: MergeStrategy;
6671
}
6772

6873

69-
export interface Schematic<CollectionT, SchematicT> {
70-
readonly description: SchematicDescription<CollectionT, SchematicT>;
71-
readonly collection: Collection<CollectionT, SchematicT>;
72-
73-
call<T>(options: T, host: Observable<Tree>): Observable<Tree>;
74-
}
75-
76-
77-
export interface ProtocolHandler {
78-
(url: Url): Source;
79-
}
80-
81-
82-
74+
/**
75+
* A Collection as created by the Engine. This should be used by the tool to create schematics,
76+
* or by rules to create other schematics as well.
77+
*/
8378
export interface Collection<CollectionT, SchematicT> {
8479
readonly name: string;
8580
readonly description: CollectionDescription<CollectionT>;
@@ -88,21 +83,52 @@ export interface Collection<CollectionT, SchematicT> {
8883
}
8984

9085

86+
/**
87+
* A Schematic as created by the Engine. This should be used by the tool to execute the main
88+
* schematics, or by rules to execute other schematics as well.
89+
*/
90+
export interface Schematic<CollectionT, SchematicT> {
91+
readonly description: SchematicDescription<CollectionT, SchematicT>;
92+
readonly collection: Collection<CollectionT, SchematicT>;
93+
94+
call<T>(options: T, host: Observable<Tree>): Observable<Tree>;
95+
}
96+
97+
98+
/**
99+
* A SchematicContext. Contains information necessary for Schematics to execute some rules, for
100+
* example when using another schematics, as we need the engine and collection.
101+
*/
91102
export interface TypedSchematicContext<CollectionT, SchematicT> {
92103
readonly engine: Engine<CollectionT, SchematicT>;
93104
readonly schematic: Schematic<CollectionT, SchematicT>;
94105
readonly host: Observable<Tree>;
95106
readonly strategy: MergeStrategy;
96107
}
108+
109+
110+
/**
111+
* This is used by the Schematics implementations in order to avoid needing to have typing from
112+
* the tooling. Schematics are not specific to a tool.
113+
*/
97114
export type SchematicContext = TypedSchematicContext<any, any>;
98115

99116

117+
/**
118+
* A rule factory, which is normally the way schematics are implemented. Returned by the tooling
119+
* after loading a schematic description.
120+
*/
100121
export type RuleFactory<T> = (options: T) => Rule;
101122

123+
102124
/**
125+
* A source is a function that generates a Tree from a specific context. A rule transforms a tree
126+
* into another tree from a specific context. In both cases, an Observable can be returned if
127+
* the source or the rule are asynchronous. Only the last Tree generated in the observable will
128+
* be used though.
129+
*
103130
* We obfuscate the context of Source and Rule because the schematic implementation should not
104131
* know which types is the schematic or collection metadata, as they are both tooling specific.
105132
*/
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>;
133+
export type Source = (context: SchematicContext) => Tree | Observable<Tree>;
134+
export type Rule = (tree: Tree, context: SchematicContext) => Tree | Observable<Tree>;

packages/schematics/src/exception/exception.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,8 @@ export class ContentHasMutatedException extends BaseException {
2828
export class InvalidUpdateRecordException extends BaseException {
2929
constructor() { super(`Invalid record instance.`); }
3030
}
31+
export class MergeConflictException extends BaseException {
32+
constructor(path: string) {
33+
super(`A merge conflicted on path "${path}".`);
34+
}
35+
}

0 commit comments

Comments
 (0)