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

Commit abf8cb2

Browse files
committed
refactor: build multiple engine-host to help with use cases
Also, separation of concern for engine host has been improved, and a simple base implementation has been added. Tools should use this (or any subclasses provided) to implement their own EngineHost. This includes Google, which will use the FileSystem based one, and the Workbench which will use the Registry one.
1 parent f12f94e commit abf8cb2

11 files changed

+412
-139
lines changed

packages/schematics/src/engine/engine.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {MergeStrategy} from '../tree/interface';
2020
import {NullTree} from '../tree/null';
2121
import {branch, empty} from '../tree/static';
2222

23-
import {Url, parse, format} from 'url';
23+
import {Url} from 'url';
2424
import 'rxjs/add/operator/map';
2525

2626

packages/schematics/src/exception/exception.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,8 @@ export class MergeConflictException extends BaseException {
3232
constructor(path: string) {
3333
super(`A merge conflicted on path "${path}".`);
3434
}
35-
}
35+
}
36+
37+
export class UnimplementedException extends BaseException {
38+
constructor() { super('This function is unimplemented.'); }
39+
}

packages/schematics/tooling/export-ref_spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ describe('ExportStringRef', () => {
3636

3737
it('works on package names', () => {
3838
// META
39-
const ref = new ExportStringRef('@angular/schematics-cli#ExportStringRef');
39+
const ref = new ExportStringRef('@angular/schematics/tooling#ExportStringRef');
4040
expect(ref.ref).toEqual(ExportStringRef);
4141
expect(ref.path).toBe(__dirname);
4242
expect(ref.module).toBe(path.join(__dirname, 'index.ts'));
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import {FileSystemHost} from './file-system-host';
2+
import {
3+
Collection,
4+
CollectionDescription,
5+
EngineHost,
6+
FileSystemTree,
7+
RuleFactory,
8+
SchematicDescription,
9+
Source,
10+
TypedSchematicContext,
11+
} from '../src/index';
12+
13+
import {dirname, join, resolve} from 'path';
14+
import {Url} from 'url';
15+
import {readJsonFile} from './file-system-utility';
16+
17+
18+
export interface FileSystemCollectionDescription {
19+
readonly path: string;
20+
readonly version?: string;
21+
readonly schematics: { [name: string]: FileSystemSchematicJsonDescription };
22+
}
23+
24+
25+
export interface FileSystemSchematicJsonDescription {
26+
readonly factory: string;
27+
readonly description: string;
28+
readonly schema?: string;
29+
}
30+
31+
export interface FileSystemSchematicDescription extends FileSystemSchematicJsonDescription {
32+
// Processed by the EngineHost.
33+
readonly path: string;
34+
readonly schemaJson?: Object;
35+
// Using `any` here is okay because the type isn't resolved when we read this value,
36+
// but rather when the Engine asks for it.
37+
readonly factoryFn: RuleFactory<any>;
38+
}
39+
40+
41+
/**
42+
* Used to simplify typings.
43+
*/
44+
export declare type FileSystemCollection
45+
= Collection<FileSystemCollectionDescription, FileSystemSchematicDescription>;
46+
export declare type FileSystemCollectionDesc
47+
= CollectionDescription<FileSystemCollectionDescription>;
48+
export declare type FileSystemSchematicDesc
49+
= SchematicDescription<FileSystemCollectionDescription, FileSystemSchematicDescription>;
50+
export declare type FileSystemSchematicContext
51+
= TypedSchematicContext<FileSystemCollectionDescription, FileSystemSchematicDescription>;
52+
53+
54+
/**
55+
* A EngineHost base class that uses the file system to resolve collections. This is the base of
56+
* all other EngineHost provided by the tooling part of the Schematics library.
57+
*/
58+
export abstract class FileSystemEngineHostBase implements
59+
EngineHost<FileSystemCollectionDescription, FileSystemSchematicDescription> {
60+
protected abstract _resolveCollectionPath(_name: string): string | null;
61+
protected abstract _resolveReferenceString(
62+
_name: string, _parentPath: string): { ref: RuleFactory<any>, path: string } | null;
63+
64+
listSchematics(collection: FileSystemCollection) {
65+
return Object.keys(collection.description.schematics);
66+
}
67+
68+
/**
69+
*
70+
* @param name
71+
* @return {{path: string}}
72+
*/
73+
createCollectionDescription(name: string): FileSystemCollectionDesc | null {
74+
try {
75+
const path = this._resolveCollectionPath(name);
76+
if (!path) {
77+
return null;
78+
}
79+
80+
const description: FileSystemCollectionDesc = readJsonFile(path);
81+
if (!description.name) {
82+
return null;
83+
}
84+
85+
return {
86+
...description,
87+
path,
88+
};
89+
} catch (e) {
90+
return null;
91+
}
92+
}
93+
94+
createSchematicDescription(
95+
name: string, collection: FileSystemCollectionDesc): FileSystemSchematicDesc | null {
96+
if (!(name in collection.schematics)) {
97+
return null;
98+
}
99+
100+
const collectionPath = dirname(collection.path);
101+
const description = collection.schematics[name];
102+
103+
if (!description) {
104+
return null;
105+
}
106+
107+
// Use any on this ref as we don't have the OptionT here, but we don't need it (we only need
108+
// the path).
109+
const resolvedRef = this._resolveReferenceString(description.factory, collectionPath);
110+
if (!resolvedRef) {
111+
return null;
112+
}
113+
114+
const { path } = resolvedRef;
115+
let schema = description.schema;
116+
let schemaJson = undefined;
117+
if (schema) {
118+
schema = join(collectionPath, schema);
119+
schemaJson = readJsonFile(schema);
120+
}
121+
122+
return {
123+
...description,
124+
schema,
125+
schemaJson,
126+
name,
127+
path,
128+
factoryFn: resolvedRef.ref,
129+
collection
130+
};
131+
}
132+
133+
createSourceFromUrl(url: Url): Source | null {
134+
switch (url.protocol) {
135+
case null:
136+
case 'file:':
137+
return (context: FileSystemSchematicContext) => {
138+
// Resolve all file:///a/b/c/d from the schematic's own path, and not the current
139+
// path.
140+
const root = resolve(dirname(context.schematic.description.path), url.path);
141+
return new FileSystemTree(new FileSystemHost(root), true);
142+
};
143+
}
144+
145+
return null;
146+
}
147+
148+
getSchematicRuleFactory<OptionT>(
149+
schematic: FileSystemSchematicDesc,
150+
_collection: FileSystemCollectionDesc): RuleFactory<OptionT> {
151+
return schematic.factoryFn;
152+
}
153+
154+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import {ExportStringRef} from './export-ref';
2+
import {FileSystemEngineHostBase} from './file-system-engine-host-base';
3+
import {RuleFactory} from '../src/engine/interface';
4+
5+
import {join} from 'path';
6+
import {existsSync} from 'fs';
7+
8+
9+
/**
10+
* A simple EngineHost that uses a root with one directory per collection inside of it. The
11+
* collection declaration follows the same rules as the regular FileSystemEngineHostBase.
12+
*/
13+
export class FileSystemEngineHost extends FileSystemEngineHostBase {
14+
constructor(protected _root: string) { super(); }
15+
16+
protected _resolveCollectionPath(name: string): string | null {
17+
// Allow `${_root}/${name}.json` as a collection.
18+
if (existsSync(join(this._root, name + '.json'))) {
19+
return join(this._root, name + '.json');
20+
}
21+
22+
// Allow `${_root}/${name}/collection.json.
23+
if (existsSync(join(this._root, name, 'collection.json'))) {
24+
return join(this._root, name, 'collection.json');
25+
}
26+
27+
return null;
28+
}
29+
30+
protected _resolveReferenceString(refString: string, parentPath: string) {
31+
// Use the same kind of export strings as NodeModule.
32+
const ref = new ExportStringRef<RuleFactory<any>>(refString, parentPath);
33+
return { ref: ref.ref, path: ref.module };
34+
}
35+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import {existsSync, readFileSync} from 'fs';
2+
3+
4+
/**
5+
* Read a file and returns its content. This supports different file encoding.
6+
*/
7+
export function readFile(fileName: string): string | null {
8+
if (!existsSync(fileName)) {
9+
return null;
10+
}
11+
const buffer = readFileSync(fileName);
12+
let len = buffer.length;
13+
if (len >= 2 && buffer[0] === 0xFE && buffer[1] === 0xFF) {
14+
// Big endian UTF-16 byte order mark detected. Since big endian is not supported by node.js,
15+
// flip all byte pairs and treat as little endian.
16+
len &= ~1;
17+
for (let i = 0; i < len; i += 2) {
18+
const temp = buffer[i];
19+
buffer[i] = buffer[i + 1];
20+
buffer[i + 1] = temp;
21+
}
22+
return buffer.toString('utf16le', 2);
23+
}
24+
if (len >= 2 && buffer[0] === 0xFF && buffer[1] === 0xFE) {
25+
// Little endian UTF-16 byte order mark detected
26+
return buffer.toString('utf16le', 2);
27+
}
28+
if (len >= 3 && buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) {
29+
// UTF-8 byte order mark detected
30+
return buffer.toString('utf8', 3);
31+
}
32+
// Default is UTF-8 with no byte order mark
33+
return buffer.toString('utf8');
34+
}
35+
36+
37+
/**
38+
* Parse a JSON string and optionally remove comments from it first.
39+
*/
40+
export function parseJson(content: string, stripComments = true): any {
41+
if (stripComments) {
42+
// Simple parser to ignore strings.
43+
let inString = false;
44+
for (let i = 0; i < content.length; i++) {
45+
if (inString) {
46+
// Skip `\X` characters. Since we only care about \", we don't need to fully process valid
47+
// escape sequences (e.g. unicode sequences). This algorithm could accept invalid sequences
48+
// but the JSON.parse call will throw on those so the output of the whole function will
49+
// still be valid.
50+
switch (content[i]) {
51+
case '\\': i++; break;
52+
case '"': inString = false; break;
53+
// Else ignore characters.
54+
}
55+
} else {
56+
switch (content[i]) {
57+
case '"': inString = true; break;
58+
case '/':
59+
let j = 0;
60+
switch (content[i + 1]) {
61+
case '/': // Strip until end-of-line.
62+
for (j = i + 2; j < content.length; j++) {
63+
if (content[j] == '\n' || content[j] == '\r') {
64+
break;
65+
}
66+
}
67+
break;
68+
case '*': // Strip until `*/`.
69+
for (j = i + 2; j < content.length; j++) {
70+
if (content[j - 1] == '*' && content[j] == '/') {
71+
break;
72+
}
73+
}
74+
break;
75+
}
76+
if (j) {
77+
// Use `j + 1` to skip the end-of-comment character.
78+
content = content.substr(0, i) + content.substr(j + 1);
79+
i--;
80+
}
81+
break;
82+
}
83+
}
84+
}
85+
// At this point we don't really care about the validity of the resulting string, since
86+
// JSON.parse will validate the JSON itself.
87+
}
88+
89+
return JSON.parse(content);
90+
}
91+
92+
93+
export function readJsonFile(path: string): any {
94+
return parseJson(readFile(path) || '{}');
95+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import {parseJson} from './file-system-utility';
2+
3+
4+
describe('parseJson', () => {
5+
it('works', () => {
6+
expect(parseJson('{ "a": 1 }')).toEqual({a: 1});
7+
expect(() => parseJson('{ 1 }')).toThrow();
8+
});
9+
10+
it('strips comments', () => {
11+
expect(parseJson(`
12+
// THIS IS A COMMENT
13+
{
14+
/* THIS IS ALSO A COMMENT */ // IGNORED BECAUSE COMMENT
15+
// AGAIN, COMMENT /* THIS SHOULD NOT BE WEIRD
16+
"a": "this // should not be a comment",
17+
"a2": "this /* should also not be a comment",
18+
/* MULTIPLE
19+
LINE
20+
COMMENT
21+
\o/ */
22+
"b" /* COMMENT */: /* YOU GUESSED IT */ 1 // COMMENT
23+
, /* STILL VALID */
24+
"c": 2
25+
}
26+
`)).toEqual({
27+
a: 'this // should not be a comment',
28+
a2: 'this /* should also not be a comment',
29+
b: 1,
30+
c: 2
31+
});
32+
});
33+
});

packages/schematics/tooling/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1-
export * from './export-ref';
21
export * from './file-system-host';
2+
export * from './file-system-engine-host';
33
export * from './node-module-engine-host';
4+
export * from './registry-engine-host';

0 commit comments

Comments
 (0)