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

Commit 9db8bf7

Browse files
committed
feat(@angular/schematics): implement the schematics library
This is not including a README, which will come later.
1 parent 539028c commit 9db8bf7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+3494
-0
lines changed

packages/schematics/package.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "@angular/schematics",
3+
"version": "0.0.0",
4+
"description": "CLI tool for Angular",
5+
"main": "src/index.js",
6+
"typings": "src/index.d.ts",
7+
"keywords": [
8+
"angular",
9+
"sdk",
10+
"blueprints",
11+
"code generation",
12+
"schematics",
13+
"Angular SDK"
14+
],
15+
"repository": {
16+
"type": "git",
17+
"url": "https://github.com/angular/sdk.git"
18+
},
19+
"engines": {
20+
"node": ">= 6.9.0",
21+
"npm": ">= 3.0.0"
22+
},
23+
"author": "Angular Authors",
24+
"license": "MIT",
25+
"bugs": {
26+
"url": "https://github.com/angular/sdk/issues"
27+
},
28+
"homepage": "https://github.com/angular/sdk",
29+
"dependencies": {
30+
}
31+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import {SchematicEngine} from './engine';
2+
import {Collection, CollectionDescription, Schematic, SchematicDescription} from './interface';
3+
import {BaseException} from '../exception/exception';
4+
5+
6+
7+
export class UnknownSchematicNameException extends BaseException {
8+
constructor(collection: string, name: string) {
9+
super(`Schematic named "${name}" could not be found in collection "${collection}".`);
10+
}
11+
}
12+
export class InvalidSchematicException extends BaseException {
13+
constructor(name: string) { super(`Invalid schematic: "${name}".`); }
14+
}
15+
16+
17+
export class CollectionImpl implements Collection {
18+
private _schematics: { [name: string]: (options: any) => Schematic | null } = {};
19+
20+
constructor(private _description: CollectionDescription,
21+
private _engine: SchematicEngine) {
22+
Object.keys(this._description.schematics).forEach(name => {
23+
this._schematics[name] = (options: any) => this._engine.createSchematic(name, this, options);
24+
});
25+
}
26+
27+
get engine() { return this._engine; }
28+
get name() { return this._description.name || '<unknown>'; }
29+
get path() { return this._description.path || '<unknown>'; }
30+
31+
listSchematicNames(): string[] {
32+
return Object.keys(this._schematics);
33+
}
34+
35+
getSchematicDescription(name: string): SchematicDescription | null {
36+
if (!(name in this._description.schematics)) {
37+
return null;
38+
}
39+
return this._description.schematics[name];
40+
}
41+
42+
createSchematic<T>(name: string, options: T): Schematic {
43+
if (!(name in this._schematics)) {
44+
throw new UnknownSchematicNameException(this.name, name);
45+
}
46+
47+
const schematic = this._schematics[name](options);
48+
if (!schematic) {
49+
throw new InvalidSchematicException(name);
50+
}
51+
return schematic;
52+
}
53+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import {Engine, Schematic, SchematicContext} from './interface';
2+
import {MergeStrategy, Tree} from '../tree/interface';
3+
4+
import {Observable} from 'rxjs/Observable';
5+
6+
7+
export class SchematicContextImpl implements SchematicContext {
8+
constructor(private _schematic: Schematic,
9+
private _host: Observable<Tree>,
10+
private _strategy = MergeStrategy.Default) {}
11+
12+
get host() { return this._host; }
13+
get engine(): Engine { return this._schematic.collection.engine; }
14+
get schematic() { return this._schematic; }
15+
get strategy() { return this._strategy; }
16+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import {CollectionImpl} from './collection';
2+
import {
3+
Collection,
4+
CollectionDescription,
5+
Engine,
6+
ProtocolHandler,
7+
ResolvedSchematicDescription,
8+
Schematic,
9+
SchematicContext,
10+
Source
11+
} from './interface';
12+
import {SchematicImpl} from './schematic';
13+
import {BaseException} from '../exception/exception';
14+
import {empty} from '../tree/static';
15+
16+
import {Url, parse, format} from 'url';
17+
18+
19+
export class InvalidSourceUrlException extends BaseException {
20+
constructor(url: string) { super(`Invalid source url: "${url}".`); }
21+
}
22+
export class UnknownUrlSourceProtocol extends BaseException {
23+
constructor(url: string) { super(`Unknown Protocol on url "${url}".`); }
24+
}
25+
26+
27+
export interface SchematicEngineOptions {
28+
loadCollection(name: string): CollectionDescription | null;
29+
loadSchematic<T>(name: string,
30+
collection: Collection,
31+
options: T): ResolvedSchematicDescription | null;
32+
}
33+
34+
35+
export class SchematicEngine implements Engine {
36+
private _protocolMap = new Map<string, ProtocolHandler>();
37+
38+
constructor(private _options: SchematicEngineOptions) {
39+
// Default implementations.
40+
this._protocolMap.set('null', () => {
41+
return () => empty();
42+
});
43+
this._protocolMap.set('', (url: Url) => {
44+
// Make a copy, change the protocol.
45+
const fileUrl = parse(format(url));
46+
fileUrl.protocol = 'file:';
47+
return (context: SchematicContext) => context.engine.createSourceFromUrl(fileUrl)(context);
48+
});
49+
}
50+
51+
createCollection(name: string): Collection | null {
52+
const description = this._options.loadCollection(name);
53+
if (!description) {
54+
return null;
55+
}
56+
57+
return new CollectionImpl(description, this);
58+
}
59+
60+
createSchematic<T>(name: string, collection: Collection, options: T): Schematic | null {
61+
const description = this._options.loadSchematic<T>(name, collection, options);
62+
if (!description) {
63+
return null;
64+
}
65+
66+
return new SchematicImpl(description, collection);
67+
}
68+
69+
registerUrlProtocolHandler(protocol: string, handler: ProtocolHandler) {
70+
this._protocolMap.set(protocol, handler);
71+
}
72+
73+
createSourceFromUrl(url: Url): Source {
74+
const protocol = (url.protocol || '').replace(/:$/, '');
75+
const handler = this._protocolMap.get(protocol);
76+
if (!handler) {
77+
throw new UnknownUrlSourceProtocol(url.toString());
78+
}
79+
return handler(url);
80+
}
81+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import {MergeStrategy, Tree} from '../tree/interface';
2+
3+
import {Observable} from 'rxjs/Observable';
4+
import {Url} from 'url';
5+
6+
7+
export interface Schematic {
8+
readonly name: string;
9+
readonly description: string;
10+
readonly path: string;
11+
readonly collection: Collection;
12+
13+
call(host: Observable<Tree>, parentContext: Partial<SchematicContext>): Observable<Tree>;
14+
}
15+
16+
17+
export interface ProtocolHandler {
18+
(url: Url): Source;
19+
}
20+
21+
22+
23+
export interface Engine {
24+
createCollection(name: string): Collection | null;
25+
createSchematic<T>(name: string, collection: Collection, options: T): Schematic | null;
26+
registerUrlProtocolHandler(protocol: string, handler: ProtocolHandler): void;
27+
createSourceFromUrl(url: Url): Source;
28+
}
29+
30+
31+
export interface Collection {
32+
readonly engine: Engine;
33+
readonly name: string;
34+
readonly path: string;
35+
36+
listSchematicNames(): string[];
37+
getSchematicDescription(name: string): SchematicDescription | null;
38+
createSchematic<T>(name: string, options: T): Schematic;
39+
}
40+
41+
42+
export interface SchematicContext {
43+
readonly engine: Engine;
44+
readonly schematic: Schematic;
45+
readonly host: Observable<Tree>;
46+
readonly strategy: MergeStrategy;
47+
}
48+
49+
50+
export interface CollectionDescription {
51+
readonly path: string;
52+
readonly name?: string;
53+
readonly version?: string;
54+
readonly schematics: { [name: string]: SchematicDescription };
55+
}
56+
57+
export interface SchematicDescription {
58+
readonly factory: string;
59+
readonly description: string;
60+
readonly schema?: string;
61+
}
62+
63+
64+
export interface ResolvedSchematicDescription extends SchematicDescription {
65+
readonly name: string;
66+
readonly path: string;
67+
readonly rule: Rule;
68+
}
69+
70+
export type RuleFactory<T> = (options: T) => Rule;
71+
72+
export type Source = (context: SchematicContext) => Tree | Observable<Tree>;
73+
export type Rule = (tree: Tree, context: SchematicContext) => Tree | Observable<Tree>;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {SchematicContextImpl} from './context';
2+
import {Collection, ResolvedSchematicDescription, Schematic, SchematicContext} from './interface';
3+
import {MergeStrategy, Tree} from '../tree/interface';
4+
import {BaseException} from '../exception/exception';
5+
6+
import {Observable} from 'rxjs/Observable';
7+
import 'rxjs/add/observable/fromPromise';
8+
import 'rxjs/add/observable/of';
9+
import 'rxjs/add/operator/concatMap';
10+
11+
12+
export class InvalidSchematicsNameException extends BaseException {
13+
constructor(path: string, name: string) {
14+
super(`Schematics at path "${path}" has invalid name "${name}".`);
15+
}
16+
}
17+
18+
19+
export class SchematicImpl implements Schematic {
20+
constructor(private _descriptor: ResolvedSchematicDescription,
21+
private _collection: Collection) {
22+
if (!_descriptor.name.match(/^[-_.a-zA-Z0-9]+$/)) {
23+
throw new InvalidSchematicsNameException(_descriptor.path, _descriptor.name);
24+
}
25+
}
26+
27+
get name() { return this._descriptor.name; }
28+
get description() { return this._descriptor.description; }
29+
get path() { return this._descriptor.path; }
30+
get collection() { return this._collection; }
31+
32+
call(host: Observable<Tree>, parentContext: Partial<SchematicContext>): Observable<Tree> {
33+
const context = new SchematicContextImpl(this, host,
34+
parentContext.strategy || MergeStrategy.Default);
35+
return host.concatMap(tree => {
36+
const result = this._descriptor.rule(tree, context);
37+
if (result instanceof Observable) {
38+
return result;
39+
} else {
40+
return Observable.of(result);
41+
}
42+
});
43+
}
44+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import {ResolvedSchematicDescription} from './interface';
2+
import {SchematicImpl} from './schematic';
3+
import {Tree} from '../tree/interface';
4+
import {branch, empty} from '../tree/static';
5+
6+
import 'rxjs/add/operator/toArray';
7+
import 'rxjs/add/operator/toPromise';
8+
import {Observable} from 'rxjs/Observable';
9+
10+
11+
12+
describe('Schematic', () => {
13+
it('works with a rule', done => {
14+
let inner: any = null;
15+
const desc: ResolvedSchematicDescription = {
16+
name: 'test',
17+
description: '',
18+
factory: '',
19+
path: 'a/b/c',
20+
rule: (tree: Tree) => {
21+
inner = branch(tree);
22+
tree.create('a/b/c', 'some content');
23+
return tree;
24+
}
25+
};
26+
27+
const schematic = new SchematicImpl(desc, null !);
28+
29+
schematic.call(Observable.of(empty()), {})
30+
.toPromise()
31+
.then(x => {
32+
expect(inner.files).toEqual([]);
33+
expect(x.files).toEqual(['/a/b/c']);
34+
})
35+
.then(done, done.fail);
36+
});
37+
38+
it('works with a rule that returns an observable', done => {
39+
let inner: any = null;
40+
const desc: ResolvedSchematicDescription = {
41+
name: 'test',
42+
description: '',
43+
factory: '',
44+
path: 'a/b/c',
45+
rule: (fem: Tree) => {
46+
inner = fem;
47+
return Observable.of(empty());
48+
}
49+
};
50+
51+
52+
const schematic = new SchematicImpl(desc, null !);
53+
schematic.call(Observable.of(empty()), {})
54+
.toPromise()
55+
.then(x => {
56+
expect(inner.files).toEqual([]);
57+
expect(x.files).toEqual([]);
58+
expect(inner).not.toBe(x);
59+
})
60+
.then(done, done.fail);
61+
});
62+
63+
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Starting with TS 2.1, Error cannot be properly extended anymore, so we implement the same
2+
// interface but in a different package.
3+
export class BaseException extends Error {
4+
constructor(message = '') {
5+
super(message);
6+
}
7+
}
8+
9+
10+
// Exceptions
11+
export class FileDoesNotExistException extends BaseException {
12+
constructor(path: string) { super(`Path "${path}" does not exist.`); }
13+
}
14+
export class FileAlreadyExistException extends BaseException {
15+
constructor(path: string) { super(`Path "${path}" already exist.`); }
16+
}
17+
export class ContentHasMutatedException extends BaseException {
18+
constructor(path: string) {
19+
super(`Content at path "${path}" has changed between the start and the end of an update.`);
20+
}
21+
}
22+
export class InvalidUpdateRecordException extends BaseException {
23+
constructor() { super(`Invalid record instance.`); }
24+
}

0 commit comments

Comments
 (0)