Skip to content

Commit b5e86c9

Browse files
authoredAug 25, 2016
feature: splitting the ast into its own packages (angular#1828)
1 parent f9df8bb commit b5e86c9

24 files changed

+1540
-1363
lines changed
 

‎.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
dist/
12
node_modules/
23
npm-debug.log
34

‎.travis.yml

+8-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ env:
1212
- DBUS_SESSION_BUS_ADDRESS=/dev/null
1313
matrix:
1414
- SCRIPT=lint
15+
# - SCRIPT=build
1516
- SCRIPT=test
1617
# - TARGET=mobile SCRIPT=mobile_test
1718
matrix:
@@ -21,8 +22,13 @@ matrix:
2122
- os: osx
2223
node_js: "5"
2324
env: SCRIPT=lint
24-
# - os: osx
25-
# env: TARGET=mobile SCRIPT=mobile_test
25+
- node_js: "6"
26+
env: SCRIPT=build
27+
- os: osx
28+
node_js: "5"
29+
env: SCRIPT=build
30+
- os: osx
31+
env: TARGET=mobile SCRIPT=mobile_test
2632

2733
before_install:
2834
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi

‎addon/ng2/tsconfig.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313
"sourceMap": true,
1414
"sourceRoot": "/",
1515
"target": "es5",
16-
"lib": ["es6"]
16+
"lib": ["es6"],
17+
"paths": {
18+
"@angular-cli/ast-tools": [ "../../packages/ast-tools/src" ]
19+
}
1720
},
1821
"includes": [
1922
"./custom-typings.d.ts"

‎addon/ng2/utilities/ast-utils.ts

+12-304
Original file line numberDiff line numberDiff line change
@@ -1,304 +1,12 @@
1-
import * as ts from 'typescript';
2-
import * as fs from 'fs';
3-
import {Symbols} from '@angular/tsc-wrapped/src/symbols';
4-
import {
5-
isMetadataImportedSymbolReferenceExpression,
6-
isMetadataModuleReferenceExpression
7-
} from '@angular/tsc-wrapped';
8-
import {Change, InsertChange, NoopChange, MultiChange} from './change';
9-
import {insertImport} from './route-utils';
10-
11-
import {Observable} from 'rxjs/Observable';
12-
import {ReplaySubject} from 'rxjs/ReplaySubject';
13-
import 'rxjs/add/observable/of';
14-
import 'rxjs/add/operator/do';
15-
import 'rxjs/add/operator/filter';
16-
import 'rxjs/add/operator/last';
17-
import 'rxjs/add/operator/map';
18-
import 'rxjs/add/operator/mergeMap';
19-
import 'rxjs/add/operator/toArray';
20-
import 'rxjs/add/operator/toPromise';
21-
22-
23-
/**
24-
* Get TS source file based on path.
25-
* @param filePath
26-
* @return source file of ts.SourceFile kind
27-
*/
28-
export function getSource(filePath: string): ts.SourceFile {
29-
return ts.createSourceFile(filePath, fs.readFileSync(filePath).toString(),
30-
ts.ScriptTarget.ES6, true);
31-
}
32-
33-
34-
/**
35-
* Get all the nodes from a source, as an observable.
36-
* @param sourceFile The source file object.
37-
* @returns {Observable<ts.Node>} An observable of all the nodes in the source.
38-
*/
39-
export function getSourceNodes(sourceFile: ts.SourceFile): Observable<ts.Node> {
40-
const subject = new ReplaySubject<ts.Node>();
41-
let nodes: ts.Node[] = [sourceFile];
42-
43-
while(nodes.length > 0) {
44-
const node = nodes.shift();
45-
46-
if (node) {
47-
subject.next(node);
48-
if (node.getChildCount(sourceFile) >= 0) {
49-
nodes.unshift(...node.getChildren());
50-
}
51-
}
52-
}
53-
54-
subject.complete();
55-
return subject.asObservable();
56-
}
57-
58-
59-
/**
60-
* Find all nodes from the AST in the subtree of node of SyntaxKind kind.
61-
* @param node
62-
* @param kind
63-
* @param max The maximum number of items to return.
64-
* @return all nodes of kind, or [] if none is found
65-
*/
66-
export function findNodes(node: ts.Node, kind: ts.SyntaxKind, max: number = Infinity): ts.Node[] {
67-
if (!node || max == 0) {
68-
return [];
69-
}
70-
71-
let arr: ts.Node[] = [];
72-
if (node.kind === kind) {
73-
arr.push(node);
74-
max--;
75-
}
76-
if (max > 0) {
77-
for (const child of node.getChildren()) {
78-
findNodes(child, kind, max).forEach(node => {
79-
if (max > 0) {
80-
arr.push(node);
81-
}
82-
max--;
83-
});
84-
85-
if (max <= 0) {
86-
break;
87-
}
88-
}
89-
}
90-
return arr;
91-
}
92-
93-
94-
/**
95-
* Helper for sorting nodes.
96-
* @return function to sort nodes in increasing order of position in sourceFile
97-
*/
98-
function nodesByPosition(first: ts.Node, second: ts.Node): number {
99-
return first.pos - second.pos;
100-
}
101-
102-
103-
/**
104-
* Insert `toInsert` after the last occurence of `ts.SyntaxKind[nodes[i].kind]`
105-
* or after the last of occurence of `syntaxKind` if the last occurence is a sub child
106-
* of ts.SyntaxKind[nodes[i].kind] and save the changes in file.
107-
*
108-
* @param nodes insert after the last occurence of nodes
109-
* @param toInsert string to insert
110-
* @param file file to insert changes into
111-
* @param fallbackPos position to insert if toInsert happens to be the first occurence
112-
* @param syntaxKind the ts.SyntaxKind of the subchildren to insert after
113-
* @return Change instance
114-
* @throw Error if toInsert is first occurence but fall back is not set
115-
*/
116-
export function insertAfterLastOccurrence(nodes: ts.Node[], toInsert: string,
117-
file: string, fallbackPos?: number, syntaxKind?: ts.SyntaxKind): Change {
118-
var lastItem = nodes.sort(nodesByPosition).pop();
119-
if (syntaxKind) {
120-
lastItem = findNodes(lastItem, syntaxKind).sort(nodesByPosition).pop();
121-
}
122-
if (!lastItem && fallbackPos == undefined) {
123-
throw new Error(`tried to insert ${toInsert} as first occurence with no fallback position`);
124-
}
125-
let lastItemPosition: number = lastItem ? lastItem.end : fallbackPos;
126-
return new InsertChange(file, lastItemPosition, toInsert);
127-
}
128-
129-
130-
export function getContentOfKeyLiteral(source: ts.SourceFile, node: ts.Node): string {
131-
if (node.kind == ts.SyntaxKind.Identifier) {
132-
return (<ts.Identifier>node).text;
133-
} else if (node.kind == ts.SyntaxKind.StringLiteral) {
134-
try {
135-
return JSON.parse(node.getFullText(source))
136-
} catch (e) {
137-
return null;
138-
}
139-
} else {
140-
return null;
141-
}
142-
}
143-
144-
145-
export function getDecoratorMetadata(source: ts.SourceFile, identifier: string,
146-
module: string): Observable<ts.Node> {
147-
const symbols = new Symbols(source);
148-
149-
return getSourceNodes(source)
150-
.filter(node => {
151-
return node.kind == ts.SyntaxKind.Decorator
152-
&& (<ts.Decorator>node).expression.kind == ts.SyntaxKind.CallExpression;
153-
})
154-
.map(node => <ts.CallExpression>(<ts.Decorator>node).expression)
155-
.filter(expr => {
156-
if (expr.expression.kind == ts.SyntaxKind.Identifier) {
157-
const id = <ts.Identifier>expr.expression;
158-
const metaData = symbols.resolve(id.getFullText(source));
159-
if (isMetadataImportedSymbolReferenceExpression(metaData)) {
160-
return metaData.name == identifier && metaData.module == module;
161-
}
162-
} else if (expr.expression.kind == ts.SyntaxKind.PropertyAccessExpression) {
163-
// This covers foo.NgModule when importing * as foo.
164-
const paExpr = <ts.PropertyAccessExpression>expr.expression;
165-
// If the left expression is not an identifier, just give up at that point.
166-
if (paExpr.expression.kind !== ts.SyntaxKind.Identifier) {
167-
return false;
168-
}
169-
170-
const id = paExpr.name;
171-
const moduleId = <ts.Identifier>paExpr.expression;
172-
const moduleMetaData = symbols.resolve(moduleId.getFullText(source));
173-
if (isMetadataModuleReferenceExpression(moduleMetaData)) {
174-
return moduleMetaData.module == module && id.getFullText(source) == identifier;
175-
}
176-
}
177-
return false;
178-
})
179-
.filter(expr => expr.arguments[0]
180-
&& expr.arguments[0].kind == ts.SyntaxKind.ObjectLiteralExpression)
181-
.map(expr => <ts.ObjectLiteralExpression>expr.arguments[0]);
182-
}
183-
184-
185-
function _addSymbolToNgModuleMetadata(ngModulePath: string, metadataField: string,
186-
symbolName: string, importPath: string) {
187-
const source: ts.SourceFile = getSource(ngModulePath);
188-
let metadata = getDecoratorMetadata(source, 'NgModule', '@angular/core');
189-
190-
// Find the decorator declaration.
191-
return metadata
192-
.toPromise()
193-
.then((node: ts.ObjectLiteralExpression) => {
194-
if (!node) {
195-
return null;
196-
}
197-
198-
// Get all the children property assignment of object literals.
199-
return node.properties
200-
.filter(prop => prop.kind == ts.SyntaxKind.PropertyAssignment)
201-
// Filter out every fields that's not "metadataField". Also handles string literals
202-
// (but not expressions).
203-
.filter(prop => {
204-
switch (prop.name.kind) {
205-
case ts.SyntaxKind.Identifier:
206-
return prop.name.getText(source) == metadataField;
207-
case ts.SyntaxKind.StringLiteral:
208-
return prop.name.text == metadataField;
209-
}
210-
211-
return false;
212-
});
213-
})
214-
// Get the last node of the array literal.
215-
.then(matchingProperties => {
216-
if (!matchingProperties) {
217-
return;
218-
}
219-
if (matchingProperties.length == 0) {
220-
return metadata
221-
.toPromise();
222-
}
223-
224-
const assignment = <ts.PropertyAssignment>matchingProperties[0];
225-
226-
// If it's not an array, nothing we can do really.
227-
if (assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) {
228-
return Observable.empty();
229-
}
230-
231-
const arrLiteral = <ts.ArrayLiteralExpression>assignment.initializer;
232-
if (arrLiteral.elements.length == 0) {
233-
// Forward the property.
234-
return arrLiteral;
235-
}
236-
return arrLiteral.elements;
237-
})
238-
.then((node: ts.Node) => {
239-
if (!node) {
240-
console.log('No app module found. Please add your new class to your component.');
241-
return new NoopChange();
242-
}
243-
if (Array.isArray(node)) {
244-
node = node[node.length - 1];
245-
}
246-
247-
let toInsert;
248-
let position = node.getEnd();
249-
if (node.kind == ts.SyntaxKind.ObjectLiteralExpression) {
250-
// We haven't found the field in the metadata declaration. Insert a new
251-
// field.
252-
let expr = <ts.ObjectLiteralExpression>node;
253-
if (expr.properties.length == 0) {
254-
position = expr.getEnd() - 1;
255-
toInsert = ` ${metadataField}: [${symbolName}]\n`;
256-
} else {
257-
node = expr.properties[expr.properties.length - 1];
258-
position = node.getEnd();
259-
// Get the indentation of the last element, if any.
260-
const text = node.getFullText(source);
261-
if (text.startsWith('\n')) {
262-
toInsert = `,${text.match(/^\n(\r?)\s+/)[0]}${metadataField}: [${symbolName}]`;
263-
} else {
264-
toInsert = `, ${metadataField}: [${symbolName}]`;
265-
}
266-
}
267-
} else if (node.kind == ts.SyntaxKind.ArrayLiteralExpression) {
268-
// We found the field but it's empty. Insert it just before the `]`.
269-
position--;
270-
toInsert = `${symbolName}`;
271-
} else {
272-
// Get the indentation of the last element, if any.
273-
const text = node.getFullText(source);
274-
if (text.startsWith('\n')) {
275-
toInsert = `,${text.match(/^\n(\r?)\s+/)[0]}${symbolName}`;
276-
} else {
277-
toInsert = `, ${symbolName}`;
278-
}
279-
}
280-
281-
const insert = new InsertChange(ngModulePath, position, toInsert);
282-
const importInsert: Change = insertImport(ngModulePath, symbolName, importPath);
283-
return new MultiChange([insert, importInsert]);
284-
});
285-
}
286-
287-
/**
288-
* Custom function to insert a declaration (component, pipe, directive)
289-
* into NgModule declarations. It also imports the component.
290-
*/
291-
export function addComponentToModule(modulePath: string, classifiedName: string,
292-
importPath: string): Promise<Change> {
293-
294-
return _addSymbolToNgModuleMetadata(modulePath, 'declarations', classifiedName, importPath);
295-
}
296-
297-
/**
298-
* Custom function to insert a provider into NgModule. It also imports it.
299-
*/
300-
export function addProviderToModule(modulePath: string, classifiedName: string,
301-
importPath: string): Promise<Change> {
302-
return _addSymbolToNgModuleMetadata(modulePath, 'providers', classifiedName, importPath);
303-
}
304-
1+
// In order to keep refactoring low, simply export from ast-tools.
2+
// TODO: move all dependencies of this file to ast-tools directly.
3+
export {
4+
getSource,
5+
getSourceNodes,
6+
findNodes,
7+
insertAfterLastOccurrence,
8+
getContentOfKeyLiteral,
9+
getDecoratorMetadata,
10+
addComponentToModule,
11+
addProviderToModule
12+
} from '@angular-cli/ast-tools';

‎addon/ng2/utilities/change.ts

+8-166
Original file line numberDiff line numberDiff line change
@@ -1,166 +1,8 @@
1-
'use strict';
2-
3-
import * as Promise from 'ember-cli/lib/ext/promise';
4-
import fs = require('fs');
5-
6-
const readFile = Promise.denodeify(fs.readFile);
7-
const writeFile = Promise.denodeify(fs.writeFile);
8-
9-
export interface Change {
10-
11-
apply(): Promise<void>;
12-
13-
// The file this change should be applied to. Some changes might not apply to
14-
// a file (maybe the config).
15-
path: string | null;
16-
17-
// The order this change should be applied. Normally the position inside the file.
18-
// Changes are applied from the bottom of a file to the top.
19-
order: number;
20-
21-
// The description of this change. This will be outputted in a dry or verbose run.
22-
description: string;
23-
}
24-
25-
26-
/**
27-
* An operation that does nothing.
28-
*/
29-
export class NoopChange implements Change {
30-
get description() { return 'No operation.'; }
31-
get order() { return Infinity; }
32-
get path() { return null; }
33-
apply() { return Promise.resolve(); }
34-
}
35-
36-
/**
37-
* An operation that mixes two or more changes, and merge them (in order).
38-
* Can only apply to a single file. Use a ChangeManager to apply changes to multiple
39-
* files.
40-
*/
41-
export class MultiChange implements Change {
42-
private _path: string;
43-
private _changes: Change[];
44-
45-
constructor(...changes: Array<Change[], Change>) {
46-
this._changes = [];
47-
[].concat(...changes).forEach(change => this.appendChange(change));
48-
}
49-
50-
appendChange(change: Change) {
51-
// Validate that the path is the same for everyone of those.
52-
if (this._path === undefined) {
53-
this._path = change.path;
54-
} else if (change.path !== this._path) {
55-
throw new Error('Cannot apply a change to a different path.');
56-
}
57-
this._changes.push(change);
58-
}
59-
60-
get description() {
61-
return `Changes:\n ${this._changes.map(x => x.description).join('\n ')}`;
62-
}
63-
// Always apply as early as the highest change.
64-
get order() { return Math.max(...this._changes); }
65-
get path() { return this._path; }
66-
67-
apply() {
68-
return this._changes
69-
.sort((a: Change, b: Change) => b.order - a.order)
70-
.reduce((promise, change) => {
71-
return promise.then(() => change.apply())
72-
}, Promise.resolve());
73-
}
74-
}
75-
76-
77-
/**
78-
* Will add text to the source code.
79-
*/
80-
export class InsertChange implements Change {
81-
82-
const order: number;
83-
const description: string;
84-
85-
constructor(
86-
public path: string,
87-
private pos: number,
88-
private toAdd: string,
89-
) {
90-
if (pos < 0) {
91-
throw new Error('Negative positions are invalid');
92-
}
93-
this.description = `Inserted ${toAdd} into position ${pos} of ${path}`;
94-
this.order = pos;
95-
}
96-
97-
/**
98-
* This method does not insert spaces if there is none in the original string.
99-
*/
100-
apply(): Promise<any> {
101-
return readFile(this.path, 'utf8').then(content => {
102-
let prefix = content.substring(0, this.pos);
103-
let suffix = content.substring(this.pos);
104-
return writeFile(this.path, `${prefix}${this.toAdd}${suffix}`);
105-
});
106-
}
107-
}
108-
109-
/**
110-
* Will remove text from the source code.
111-
*/
112-
export class RemoveChange implements Change {
113-
114-
const order: number;
115-
const description: string;
116-
117-
constructor(
118-
public path: string,
119-
private pos: number,
120-
private toRemove: string) {
121-
if (pos < 0) {
122-
throw new Error('Negative positions are invalid');
123-
}
124-
this.description = `Removed ${toRemove} into position ${pos} of ${path}`;
125-
this.order = pos;
126-
}
127-
128-
apply(): Promise<any> {
129-
return readFile(this.path, 'utf8').then(content => {
130-
let prefix = content.substring(0, this.pos);
131-
let suffix = content.substring(this.pos + this.toRemove.length);
132-
// TODO: throw error if toRemove doesn't match removed string.
133-
return writeFile(this.path, `${prefix}${suffix}`);
134-
});
135-
}
136-
}
137-
138-
/**
139-
* Will replace text from the source code.
140-
*/
141-
export class ReplaceChange implements Change {
142-
143-
const order: number;
144-
const description: string;
145-
146-
constructor(
147-
public path: string,
148-
private pos: number,
149-
private oldText: string,
150-
private newText: string) {
151-
if (pos < 0) {
152-
throw new Error('Negative positions are invalid');
153-
}
154-
this.description = `Replaced ${oldText} into position ${pos} of ${path} with ${newText}`;
155-
this.order = pos;
156-
}
157-
158-
apply(): Promise<any> {
159-
return readFile(this.path, 'utf8').then(content => {
160-
let prefix = content.substring(0, this.pos);
161-
let suffix = content.substring(this.pos + this.oldText.length);
162-
// TODO: throw error if oldText doesn't match removed string.
163-
return writeFile(this.path, `${prefix}${this.newText}${suffix}`);
164-
});
165-
}
166-
}
1+
export {
2+
Change,
3+
NoopChange,
4+
MultiChange,
5+
InsertChange,
6+
RemoveChange,
7+
ReplaceChange
8+
} from '@angular-cli/ast-tools';

‎addon/ng2/utilities/route-utils.ts

+11-522
Large diffs are not rendered by default.

‎bin/ng

+3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ const exit = require('exit');
99
const packageJson = require('../package.json');
1010
const Leek = require('leek');
1111

12+
require('../lib/bootstrap-local');
13+
14+
1215
resolve('angular-cli', { basedir: process.cwd() },
1316
function (error, projectLocalCli) {
1417
var cli;

‎lib/bootstrap-local.js

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/* eslint-disable no-console */
2+
'use strict';
3+
4+
const fs = require('fs');
5+
const ts = require('typescript');
6+
7+
8+
const oldRequireTs = require.extensions['.ts'];
9+
require.extensions['.ts'] = function(m, filename) {
10+
// If we're in node module, either call the old hook or simply compile the
11+
// file without transpilation. We do not touch node_modules/**.
12+
// We do touch `angular-cli` files anywhere though.
13+
if (!filename.match(/angular-cli/) && filename.match(/node_modules/)) {
14+
if (oldRequireTs) {
15+
return oldRequireTs(m, filename);
16+
}
17+
return m._compile(fs.readFileSync(filename), filename);
18+
}
19+
20+
// Node requires all require hooks to be sync.
21+
const source = fs.readFileSync(filename).toString();
22+
23+
try {
24+
const result = ts.transpile(source, {
25+
target: ts.ScriptTarget.ES5,
26+
module: ts.ModuleKind.CommonJs
27+
});
28+
29+
// Send it to node to execute.
30+
return m._compile(result, filename);
31+
} catch (err) {
32+
console.error('Error while running script "' + filename + '":');
33+
console.error(err.stack);
34+
throw err;
35+
}
36+
};
37+
38+
39+
40+
// If we're running locally, meaning npm linked. This is basically "developer mode".
41+
if (!__dirname.match(/\/node_modules\//)) {
42+
const packages = require('./packages');
43+
44+
// We mock the module loader so that we can fake our packages when running locally.
45+
const Module = require('module');
46+
const oldLoad = Module._load;
47+
Module._load = function (request, parent) {
48+
if (request in packages) {
49+
return oldLoad.call(this, packages[request].main, parent);
50+
} else {
51+
return oldLoad.apply(this, arguments);
52+
}
53+
};
54+
}

‎lib/cli/index.js

-34
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,6 @@
11
/*eslint-disable no-console */
22

33
// This file hooks up on require calls to transpile TypeScript.
4-
const fs = require('fs');
5-
const ts = require('typescript');
6-
const old = require.extensions['.ts'];
7-
8-
require.extensions['.ts'] = function(m, filename) {
9-
// If we're in node module, either call the old hook or simply compile the
10-
// file without transpilation. We do not touch node_modules/**.
11-
// We do touch `angular-cli` files anywhere though.
12-
if (!filename.match(/angular-cli/) && filename.match(/node_modules/)) {
13-
if (old) {
14-
return old(m, filename);
15-
}
16-
return m._compile(fs.readFileSync(filename), filename);
17-
}
18-
19-
// Node requires all require hooks to be sync.
20-
const source = fs.readFileSync(filename).toString();
21-
22-
try {
23-
const result = ts.transpile(source, {
24-
target: ts.ScriptTarget.ES5,
25-
module: ts.ModuleKind.CommonJs
26-
});
27-
28-
// Send it to node to execute.
29-
return m._compile(result, filename);
30-
} catch (err) {
31-
console.error('Error while running script "' + filename + '":');
32-
console.error(err.stack);
33-
throw err;
34-
}
35-
};
36-
37-
384
const cli = require('ember-cli/lib/cli');
395
const path = require('path');
406

‎lib/packages.js

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
4+
const packageRoot = path.join(__dirname, '../packages');
5+
6+
// All the supported packages. Go through the packages directory and create a map of
7+
// name => fullPath.
8+
const packages = fs.readdirSync(packageRoot)
9+
.map(pkgName => ({ name: pkgName, root: path.join(packageRoot, pkgName) }))
10+
.filter(pkg => fs.statSync(pkg.root).isDirectory())
11+
.reduce((packages, pkg) => {
12+
packages[`@angular-cli/${pkg.name}`] = {
13+
root: pkg.root,
14+
main: path.resolve(pkg.root, 'src/index.ts')
15+
};
16+
return packages;
17+
}, {});
18+
19+
module.exports = packages;

‎package.json

+13-6
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@
99
},
1010
"keywords": [],
1111
"scripts": {
12-
"test": "node tests/runner",
12+
"build": "for PKG in packages/*; do echo Building $PKG...; tsc -P $PKG; done",
13+
"test": "npm run test:packages && npm run test:cli",
1314
"mobile_test": "mocha tests/e2e/e2e_workflow.spec.js",
15+
"test:cli": "node tests/runner",
1416
"test:inspect": "node --inspect --debug-brk tests/runner",
17+
"test:packages": "node scripts/run-packages-spec.js",
1518
"lint": "eslint .",
1619
"build-config-interface": "dtsgen lib/config/schema.json --out lib/config/schema.d.ts"
1720
},
@@ -35,9 +38,6 @@
3538
"dependencies": {
3639
"@angular/core": "^2.0.0-rc.5",
3740
"@angular/tsc-wrapped": "^0.2.2",
38-
"@types/lodash": "^4.0.25-alpha",
39-
"@types/rimraf": "0.0.25-alpha",
40-
"@types/webpack": "^1.12.22-alpha",
4141
"angular2-template-loader": "^0.5.0",
4242
"awesome-typescript-loader": "^2.2.1",
4343
"chalk": "^1.1.3",
@@ -90,7 +90,6 @@
9090
"stylus-loader": "^2.1.0",
9191
"symlink-or-copy": "^1.0.3",
9292
"ts-loader": "^0.8.2",
93-
"tslint": "^3.11.0",
9493
"tslint-loader": "^2.1.4",
9594
"typedoc": "^0.4.2",
9695
"typescript": "^2.0.0",
@@ -106,13 +105,21 @@
106105
]
107106
},
108107
"devDependencies": {
108+
"@types/denodeify": "^1.2.29",
109+
"@types/jasmine": "^2.2.32",
110+
"@types/lodash": "^4.0.25-alpha",
111+
"@types/mock-fs": "3.6.28",
109112
"@types/node": "^6.0.36",
113+
"@types/rimraf": "0.0.25-alpha",
114+
"@types/webpack": "^1.12.22-alpha",
110115
"chai": "^3.5.0",
111116
"conventional-changelog": "^1.1.0",
112117
"denodeify": "^1.2.1",
113118
"dtsgenerator": "^0.7.1",
114119
"eslint": "^2.8.0",
115120
"exists-sync": "0.0.3",
121+
"jasmine": "^2.4.1",
122+
"jasmine-spec-reporter": "^2.7.0",
116123
"minimatch": "^3.0.0",
117124
"mocha": "^2.4.5",
118125
"mock-fs": "3.10.0",
@@ -121,7 +128,7 @@
121128
"sinon": "^1.17.3",
122129
"through": "^2.3.8",
123130
"tree-kill": "^1.0.0",
124-
"tslint": "^3.8.1",
131+
"tslint": "^3.11.0",
125132
"walk-sync": "^0.2.6"
126133
}
127134
}

‎packages/ast-tools/package.json

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "angular-cli",
3+
"version": "1.0.0-beta.11-webpack.2",
4+
"description": "CLI tool for Angular",
5+
"main": "./index.js",
6+
"keywords": [
7+
"angular",
8+
"cli",
9+
"ast",
10+
"tool"
11+
],
12+
"repository": {
13+
"type": "git",
14+
"url": "https://github.com/angular/angular-cli.git"
15+
},
16+
"author": "angular",
17+
"license": "MIT",
18+
"bugs": {
19+
"url": "https://github.com/angular/angular-cli/issues"
20+
},
21+
"homepage": "https://github.com/angular/angular-cli",
22+
"dependencies": {
23+
"rxjs": "^5.0.0-beta.11",
24+
"denodeify": "^1.2.1",
25+
"typescript": "^2.0.0"
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
1-
import * as mockFs from 'mock-fs';
2-
import { expect } from 'chai';
3-
import * as ts from 'typescript';
4-
import * as fs from 'fs';
5-
import { InsertChange, RemoveChange } from '../../addon/ng2/utilities/change';
6-
import * as Promise from 'ember-cli/lib/ext/promise';
7-
import {
8-
findNodes,
9-
insertAfterLastOccurrence,
10-
addComponentToModule
11-
} from '../../addon/ng2/utilities/ast-utils';
1+
import denodeify = require('denodeify');
2+
import mockFs = require('mock-fs');
3+
import ts = require('typescript');
4+
import fs = require('fs');
5+
6+
import {InsertChange, RemoveChange} from './change';
7+
import {insertAfterLastOccurrence, addComponentToModule} from './ast-utils';
8+
import {findNodes} from './node';
9+
import {it} from './spec-utils';
10+
11+
const readFile = <any>denodeify(fs.readFile);
1212

13-
const readFile = Promise.denodeify(fs.readFile);
1413

1514
describe('ast-utils: findNodes', () => {
1615
const sourceFile = 'tmp/tmp.ts';
@@ -19,7 +18,7 @@ describe('ast-utils: findNodes', () => {
1918
let mockDrive = {
2019
'tmp': {
2120
'tmp.ts': `import * as myTest from 'tests' \n` +
22-
'hello.'
21+
'hello.'
2322
}
2423
};
2524
mockFs(mockDrive);
@@ -32,42 +31,42 @@ describe('ast-utils: findNodes', () => {
3231
it('finds no imports', () => {
3332
let editedFile = new RemoveChange(sourceFile, 0, `import * as myTest from 'tests' \n`);
3433
return editedFile
35-
.apply()
36-
.then(() => {
37-
let rootNode = getRootNode(sourceFile);
38-
let nodes = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
39-
expect(nodes).to.be.empty;
40-
});
34+
.apply()
35+
.then(() => {
36+
let rootNode = getRootNode(sourceFile);
37+
let nodes = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
38+
expect(nodes).toEqual([]);
39+
});
4140
});
4241
it('finds one import', () => {
4342
let rootNode = getRootNode(sourceFile);
4443
let nodes = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
45-
expect(nodes.length).to.equal(1);
44+
expect(nodes.length).toEqual(1);
4645
});
4746
it('finds two imports from inline declarations', () => {
4847
// remove new line and add an inline import
4948
let editedFile = new RemoveChange(sourceFile, 32, '\n');
5049
return editedFile
51-
.apply()
52-
.then(() => {
53-
let insert = new InsertChange(sourceFile, 32, `import {Routes} from '@angular/routes'`);
54-
return insert.apply();
55-
})
56-
.then(() => {
57-
let rootNode = getRootNode(sourceFile);
58-
let nodes = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
59-
expect(nodes.length).to.equal(2);
60-
});
50+
.apply()
51+
.then(() => {
52+
let insert = new InsertChange(sourceFile, 32, `import {Routes} from '@angular/routes'`);
53+
return insert.apply();
54+
})
55+
.then(() => {
56+
let rootNode = getRootNode(sourceFile);
57+
let nodes = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
58+
expect(nodes.length).toEqual(2);
59+
});
6160
});
6261
it('finds two imports from new line separated declarations', () => {
6362
let editedFile = new InsertChange(sourceFile, 33, `import {Routes} from '@angular/routes'`);
6463
return editedFile
65-
.apply()
66-
.then(() => {
67-
let rootNode = getRootNode(sourceFile);
68-
let nodes = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
69-
expect(nodes.length).to.equal(2);
70-
});
64+
.apply()
65+
.then(() => {
66+
let rootNode = getRootNode(sourceFile);
67+
let nodes = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
68+
expect(nodes.length).toEqual(2);
69+
});
7170
});
7271
});
7372

@@ -89,86 +88,93 @@ describe('ast-utils: insertAfterLastOccurrence', () => {
8988
it('inserts at beginning of file', () => {
9089
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
9190
return insertAfterLastOccurrence(imports, `\nimport { Router } from '@angular/router';`,
92-
sourceFile, 0)
93-
.apply()
94-
.then(() => {
95-
return readFile(sourceFile, 'utf8');
96-
}).then((content) => {
97-
let expected = '\nimport { Router } from \'@angular/router\';';
98-
expect(content).to.equal(expected);
99-
});
91+
sourceFile, 0)
92+
.apply()
93+
.then(() => {
94+
return readFile(sourceFile, 'utf8');
95+
}).then((content) => {
96+
let expected = '\nimport { Router } from \'@angular/router\';';
97+
expect(content).toEqual(expected);
98+
});
10099
});
101100
it('throws an error if first occurence with no fallback position', () => {
102101
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
103102
expect(() => insertAfterLastOccurrence(imports, `import { Router } from '@angular/router';`,
104-
sourceFile)).to.throw(Error);
103+
sourceFile)).toThrowError();
105104
});
106105
it('inserts after last import', () => {
107106
let content = `import { foo, bar } from 'fizz';`;
108107
let editedFile = new InsertChange(sourceFile, 0, content);
109108
return editedFile
110-
.apply()
111-
.then(() => {
112-
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
113-
return insertAfterLastOccurrence(imports, ', baz', sourceFile,
114-
0, ts.SyntaxKind.Identifier)
115-
.apply();
116-
}).then(() => {
117-
return readFile(sourceFile, 'utf8');
118-
}).then(newContent => expect(newContent).to.equal(`import { foo, bar, baz } from 'fizz';`));
109+
.apply()
110+
.then(() => {
111+
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
112+
return insertAfterLastOccurrence(imports, ', baz', sourceFile,
113+
0, ts.SyntaxKind.Identifier)
114+
.apply();
115+
})
116+
.then(() => {
117+
return readFile(sourceFile, 'utf8');
118+
})
119+
.then(newContent => expect(newContent).toEqual(`import { foo, bar, baz } from 'fizz';`));
119120
});
120121
it('inserts after last import declaration', () => {
121122
let content = `import * from 'foo' \n import { bar } from 'baz'`;
122123
let editedFile = new InsertChange(sourceFile, 0, content);
123124
return editedFile
124-
.apply()
125-
.then(() => {
126-
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
127-
return insertAfterLastOccurrence(imports, `\nimport Router from '@angular/router'`,
128-
sourceFile)
129-
.apply();
130-
}).then(() => {
131-
return readFile(sourceFile, 'utf8');
132-
}).then(newContent => {
133-
let expected = `import * from 'foo' \n import { bar } from 'baz'` +
134-
`\nimport Router from '@angular/router'`;
135-
expect(newContent).to.equal(expected);
136-
});
125+
.apply()
126+
.then(() => {
127+
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
128+
return insertAfterLastOccurrence(imports, `\nimport Router from '@angular/router'`,
129+
sourceFile)
130+
.apply();
131+
})
132+
.then(() => {
133+
return readFile(sourceFile, 'utf8');
134+
})
135+
.then(newContent => {
136+
let expected = `import * from 'foo' \n import { bar } from 'baz'` +
137+
`\nimport Router from '@angular/router'`;
138+
expect(newContent).toEqual(expected);
139+
});
137140
});
138141
it('inserts correctly if no imports', () => {
139142
let content = `import {} from 'foo'`;
140143
let editedFile = new InsertChange(sourceFile, 0, content);
141144
return editedFile
142-
.apply()
143-
.then(() => {
144-
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
145-
return insertAfterLastOccurrence(imports, ', bar', sourceFile, undefined,
146-
ts.SyntaxKind.Identifier)
147-
.apply();
148-
}).catch(() => {
149-
return readFile(sourceFile, 'utf8');
150-
})
151-
.then(newContent => {
152-
expect(newContent).to.equal(content);
153-
// use a fallback position for safety
154-
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
155-
let pos = findNodes(imports.sort((a, b) => a.pos - b.pos).pop(),
156-
ts.SyntaxKind.CloseBraceToken).pop().pos;
157-
return insertAfterLastOccurrence(imports, ' bar ',
158-
sourceFile, pos, ts.SyntaxKind.Identifier)
159-
.apply();
160-
}).then(() => {
161-
return readFile(sourceFile, 'utf8');
162-
}).then(newContent => {
163-
expect(newContent).to.equal(`import { bar } from 'foo'`);
164-
});
145+
.apply()
146+
.then(() => {
147+
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
148+
return insertAfterLastOccurrence(imports, ', bar', sourceFile, undefined,
149+
ts.SyntaxKind.Identifier)
150+
.apply();
151+
})
152+
.catch(() => {
153+
return readFile(sourceFile, 'utf8');
154+
})
155+
.then(newContent => {
156+
expect(newContent).toEqual(content);
157+
// use a fallback position for safety
158+
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
159+
let pos = findNodes(imports.sort((a, b) => a.pos - b.pos).pop(),
160+
ts.SyntaxKind.CloseBraceToken).pop().pos;
161+
return insertAfterLastOccurrence(imports, ' bar ',
162+
sourceFile, pos, ts.SyntaxKind.Identifier)
163+
.apply();
164+
})
165+
.then(() => {
166+
return readFile(sourceFile, 'utf8');
167+
})
168+
.then(newContent => {
169+
expect(newContent).toEqual(`import { bar } from 'foo'`);
170+
});
165171
});
166172
});
167173

168174

169175
describe('addComponentToModule', () => {
170176
beforeEach(() => {
171-
mockFs( {
177+
mockFs({
172178
'1.ts': `
173179
import {NgModule} from '@angular/core';
174180
@@ -208,7 +214,7 @@ class Module {}`
208214
.then(change => change.apply())
209215
.then(() => readFile('1.ts', 'utf-8'))
210216
.then(content => {
211-
expect(content).to.equal(
217+
expect(content).toEqual(
212218
'\n' +
213219
'import {NgModule} from \'@angular/core\';\n' +
214220
'import { MyClass } from \'MyImportPath\';\n' +
@@ -218,15 +224,15 @@ class Module {}`
218224
'})\n' +
219225
'class Module {}'
220226
);
221-
})
227+
});
222228
});
223229

224230
it('works with array with declarations', () => {
225231
return addComponentToModule('2.ts', 'MyClass', 'MyImportPath')
226232
.then(change => change.apply())
227233
.then(() => readFile('2.ts', 'utf-8'))
228234
.then(content => {
229-
expect(content).to.equal(
235+
expect(content).toEqual(
230236
'\n' +
231237
'import {NgModule} from \'@angular/core\';\n' +
232238
'import { MyClass } from \'MyImportPath\';\n' +
@@ -239,15 +245,15 @@ class Module {}`
239245
'})\n' +
240246
'class Module {}'
241247
);
242-
})
248+
});
243249
});
244250

245251
it('works without any declarations', () => {
246252
return addComponentToModule('3.ts', 'MyClass', 'MyImportPath')
247253
.then(change => change.apply())
248254
.then(() => readFile('3.ts', 'utf-8'))
249255
.then(content => {
250-
expect(content).to.equal(
256+
expect(content).toEqual(
251257
'\n' +
252258
'import {NgModule} from \'@angular/core\';\n' +
253259
'import { MyClass } from \'MyImportPath\';\n' +
@@ -257,15 +263,15 @@ class Module {}`
257263
'})\n' +
258264
'class Module {}'
259265
);
260-
})
266+
});
261267
});
262268

263269
it('works without a declaration field', () => {
264270
return addComponentToModule('4.ts', 'MyClass', 'MyImportPath')
265271
.then(change => change.apply())
266272
.then(() => readFile('4.ts', 'utf-8'))
267273
.then(content => {
268-
expect(content).to.equal(
274+
expect(content).toEqual(
269275
'\n' +
270276
'import {NgModule} from \'@angular/core\';\n' +
271277
'import { MyClass } from \'MyImportPath\';\n' +
@@ -277,11 +283,11 @@ class Module {}`
277283
'})\n' +
278284
'class Module {}'
279285
);
280-
})
286+
});
281287
});
282288
});
283289

284-
/**
290+
/**
285291
* Gets node of kind kind from sourceFile
286292
*/
287293
function getNodesOfKind(kind: ts.SyntaxKind, sourceFile: string) {
@@ -290,5 +296,5 @@ function getNodesOfKind(kind: ts.SyntaxKind, sourceFile: string) {
290296

291297
function getRootNode(sourceFile: string) {
292298
return ts.createSourceFile(sourceFile, fs.readFileSync(sourceFile).toString(),
293-
ts.ScriptTarget.ES6, true);
299+
ts.ScriptTarget.ES6, true);
294300
}

‎packages/ast-tools/src/ast-utils.ts

+271
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
import * as ts from 'typescript';
2+
import * as fs from 'fs';
3+
import {Symbols} from '@angular/tsc-wrapped/src/symbols';
4+
import {
5+
isMetadataImportedSymbolReferenceExpression,
6+
isMetadataModuleReferenceExpression
7+
} from '@angular/tsc-wrapped';
8+
import {Change, InsertChange, NoopChange, MultiChange} from './change';
9+
import {findNodes} from './node';
10+
import {insertImport} from './route-utils';
11+
12+
import {Observable} from 'rxjs/Observable';
13+
import {ReplaySubject} from 'rxjs/ReplaySubject';
14+
import 'rxjs/add/observable/empty';
15+
import 'rxjs/add/observable/of';
16+
import 'rxjs/add/operator/do';
17+
import 'rxjs/add/operator/filter';
18+
import 'rxjs/add/operator/last';
19+
import 'rxjs/add/operator/map';
20+
import 'rxjs/add/operator/mergeMap';
21+
import 'rxjs/add/operator/toArray';
22+
import 'rxjs/add/operator/toPromise';
23+
24+
25+
/**
26+
* Get TS source file based on path.
27+
* @param filePath
28+
* @return source file of ts.SourceFile kind
29+
*/
30+
export function getSource(filePath: string): ts.SourceFile {
31+
return ts.createSourceFile(filePath, fs.readFileSync(filePath).toString(),
32+
ts.ScriptTarget.ES6, true);
33+
}
34+
35+
36+
/**
37+
* Get all the nodes from a source, as an observable.
38+
* @param sourceFile The source file object.
39+
* @returns {Observable<ts.Node>} An observable of all the nodes in the source.
40+
*/
41+
export function getSourceNodes(sourceFile: ts.SourceFile): Observable<ts.Node> {
42+
const subject = new ReplaySubject<ts.Node>();
43+
let nodes: ts.Node[] = [sourceFile];
44+
45+
while(nodes.length > 0) {
46+
const node = nodes.shift();
47+
48+
if (node) {
49+
subject.next(node);
50+
if (node.getChildCount(sourceFile) >= 0) {
51+
nodes.unshift(...node.getChildren());
52+
}
53+
}
54+
}
55+
56+
subject.complete();
57+
return subject.asObservable();
58+
}
59+
60+
61+
/**
62+
* Helper for sorting nodes.
63+
* @return function to sort nodes in increasing order of position in sourceFile
64+
*/
65+
function nodesByPosition(first: ts.Node, second: ts.Node): number {
66+
return first.pos - second.pos;
67+
}
68+
69+
70+
/**
71+
* Insert `toInsert` after the last occurence of `ts.SyntaxKind[nodes[i].kind]`
72+
* or after the last of occurence of `syntaxKind` if the last occurence is a sub child
73+
* of ts.SyntaxKind[nodes[i].kind] and save the changes in file.
74+
*
75+
* @param nodes insert after the last occurence of nodes
76+
* @param toInsert string to insert
77+
* @param file file to insert changes into
78+
* @param fallbackPos position to insert if toInsert happens to be the first occurence
79+
* @param syntaxKind the ts.SyntaxKind of the subchildren to insert after
80+
* @return Change instance
81+
* @throw Error if toInsert is first occurence but fall back is not set
82+
*/
83+
export function insertAfterLastOccurrence(nodes: ts.Node[], toInsert: string,
84+
file: string, fallbackPos?: number, syntaxKind?: ts.SyntaxKind): Change {
85+
var lastItem = nodes.sort(nodesByPosition).pop();
86+
if (syntaxKind) {
87+
lastItem = findNodes(lastItem, syntaxKind).sort(nodesByPosition).pop();
88+
}
89+
if (!lastItem && fallbackPos == undefined) {
90+
throw new Error(`tried to insert ${toInsert} as first occurence with no fallback position`);
91+
}
92+
let lastItemPosition: number = lastItem ? lastItem.end : fallbackPos;
93+
return new InsertChange(file, lastItemPosition, toInsert);
94+
}
95+
96+
97+
export function getContentOfKeyLiteral(source: ts.SourceFile, node: ts.Node): string {
98+
if (node.kind == ts.SyntaxKind.Identifier) {
99+
return (<ts.Identifier>node).text;
100+
} else if (node.kind == ts.SyntaxKind.StringLiteral) {
101+
try {
102+
return JSON.parse(node.getFullText(source))
103+
} catch (e) {
104+
return null;
105+
}
106+
} else {
107+
return null;
108+
}
109+
}
110+
111+
112+
export function getDecoratorMetadata(source: ts.SourceFile, identifier: string,
113+
module: string): Observable<ts.Node> {
114+
const symbols = new Symbols(source as any);
115+
116+
return getSourceNodes(source)
117+
.filter(node => {
118+
return node.kind == ts.SyntaxKind.Decorator
119+
&& (<ts.Decorator>node).expression.kind == ts.SyntaxKind.CallExpression;
120+
})
121+
.map(node => <ts.CallExpression>(<ts.Decorator>node).expression)
122+
.filter(expr => {
123+
if (expr.expression.kind == ts.SyntaxKind.Identifier) {
124+
const id = <ts.Identifier>expr.expression;
125+
const metaData = symbols.resolve(id.getFullText(source));
126+
if (isMetadataImportedSymbolReferenceExpression(metaData)) {
127+
return metaData.name == identifier && metaData.module == module;
128+
}
129+
} else if (expr.expression.kind == ts.SyntaxKind.PropertyAccessExpression) {
130+
// This covers foo.NgModule when importing * as foo.
131+
const paExpr = <ts.PropertyAccessExpression>expr.expression;
132+
// If the left expression is not an identifier, just give up at that point.
133+
if (paExpr.expression.kind !== ts.SyntaxKind.Identifier) {
134+
return false;
135+
}
136+
137+
const id = paExpr.name;
138+
const moduleId = <ts.Identifier>paExpr.expression;
139+
const moduleMetaData = symbols.resolve(moduleId.getFullText(source));
140+
if (isMetadataModuleReferenceExpression(moduleMetaData)) {
141+
return moduleMetaData.module == module && id.getFullText(source) == identifier;
142+
}
143+
}
144+
return false;
145+
})
146+
.filter(expr => expr.arguments[0]
147+
&& expr.arguments[0].kind == ts.SyntaxKind.ObjectLiteralExpression)
148+
.map(expr => <ts.ObjectLiteralExpression>expr.arguments[0]);
149+
}
150+
151+
152+
function _addSymbolToNgModuleMetadata(ngModulePath: string, metadataField: string,
153+
symbolName: string, importPath: string) {
154+
const source: ts.SourceFile = getSource(ngModulePath);
155+
let metadata = getDecoratorMetadata(source, 'NgModule', '@angular/core');
156+
157+
// Find the decorator declaration.
158+
return metadata
159+
.toPromise()
160+
.then((node: ts.ObjectLiteralExpression) => {
161+
if (!node) {
162+
return null;
163+
}
164+
165+
// Get all the children property assignment of object literals.
166+
return node.properties
167+
.filter(prop => prop.kind == ts.SyntaxKind.PropertyAssignment)
168+
// Filter out every fields that's not "metadataField". Also handles string literals
169+
// (but not expressions).
170+
.filter((prop: ts.PropertyAssignment) => {
171+
const name = prop.name;
172+
switch (name.kind) {
173+
case ts.SyntaxKind.Identifier:
174+
return (name as ts.Identifier).getText(source) == metadataField;
175+
case ts.SyntaxKind.StringLiteral:
176+
return (name as ts.StringLiteral).text == metadataField;
177+
}
178+
179+
return false;
180+
});
181+
})
182+
// Get the last node of the array literal.
183+
.then((matchingProperties: ts.ObjectLiteralElement[]): any => {
184+
if (!matchingProperties) {
185+
return null;
186+
}
187+
if (matchingProperties.length == 0) {
188+
return metadata.toPromise();
189+
}
190+
191+
const assignment = <ts.PropertyAssignment>matchingProperties[0];
192+
193+
// If it's not an array, nothing we can do really.
194+
if (assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) {
195+
return null;
196+
}
197+
198+
const arrLiteral = <ts.ArrayLiteralExpression>assignment.initializer;
199+
if (arrLiteral.elements.length == 0) {
200+
// Forward the property.
201+
return arrLiteral;
202+
}
203+
return arrLiteral.elements;
204+
})
205+
.then((node: ts.Node) => {
206+
if (!node) {
207+
console.log('No app module found. Please add your new class to your component.');
208+
return new NoopChange();
209+
}
210+
if (Array.isArray(node)) {
211+
node = node[node.length - 1];
212+
}
213+
214+
let toInsert: string;
215+
let position = node.getEnd();
216+
if (node.kind == ts.SyntaxKind.ObjectLiteralExpression) {
217+
// We haven't found the field in the metadata declaration. Insert a new
218+
// field.
219+
let expr = <ts.ObjectLiteralExpression>node;
220+
if (expr.properties.length == 0) {
221+
position = expr.getEnd() - 1;
222+
toInsert = ` ${metadataField}: [${symbolName}]\n`;
223+
} else {
224+
node = expr.properties[expr.properties.length - 1];
225+
position = node.getEnd();
226+
// Get the indentation of the last element, if any.
227+
const text = node.getFullText(source);
228+
if (text.startsWith('\n')) {
229+
toInsert = `,${text.match(/^\n(\r?)\s+/)[0]}${metadataField}: [${symbolName}]`;
230+
} else {
231+
toInsert = `, ${metadataField}: [${symbolName}]`;
232+
}
233+
}
234+
} else if (node.kind == ts.SyntaxKind.ArrayLiteralExpression) {
235+
// We found the field but it's empty. Insert it just before the `]`.
236+
position--;
237+
toInsert = `${symbolName}`;
238+
} else {
239+
// Get the indentation of the last element, if any.
240+
const text = node.getFullText(source);
241+
if (text.startsWith('\n')) {
242+
toInsert = `,${text.match(/^\n(\r?)\s+/)[0]}${symbolName}`;
243+
} else {
244+
toInsert = `, ${symbolName}`;
245+
}
246+
}
247+
248+
const insert = new InsertChange(ngModulePath, position, toInsert);
249+
const importInsert: Change = insertImport(ngModulePath, symbolName, importPath);
250+
return new MultiChange([insert, importInsert]);
251+
});
252+
}
253+
254+
/**
255+
* Custom function to insert a declaration (component, pipe, directive)
256+
* into NgModule declarations. It also imports the component.
257+
*/
258+
export function addComponentToModule(modulePath: string, classifiedName: string,
259+
importPath: string): Promise<Change> {
260+
261+
return _addSymbolToNgModuleMetadata(modulePath, 'declarations', classifiedName, importPath);
262+
}
263+
264+
/**
265+
* Custom function to insert a provider into NgModule. It also imports it.
266+
*/
267+
export function addProviderToModule(modulePath: string, classifiedName: string,
268+
importPath: string): Promise<Change> {
269+
return _addSymbolToNgModuleMetadata(modulePath, 'providers', classifiedName, importPath);
270+
}
271+

‎tests/acceptance/change.spec.ts renamed to ‎packages/ast-tools/src/change.spec.ts

+12-12
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
// This needs to be first so fs module can be mocked correctly.
44
let mockFs = require('mock-fs');
55

6-
import {expect} from 'chai';
7-
import {InsertChange, RemoveChange, ReplaceChange} from '../../addon/ng2/utilities/change';
6+
import {it} from './spec-utils';
7+
import {InsertChange, RemoveChange, ReplaceChange} from './change';
88
import fs = require('fs');
99

1010
let path = require('path');
@@ -38,19 +38,19 @@ describe('Change', () => {
3838
.apply()
3939
.then(() => readFile(sourceFile, 'utf8'))
4040
.then(contents => {
41-
expect(contents).to.equal('hello world!');
41+
expect(contents).toEqual('hello world!');
4242
});
4343
});
4444
it('fails for negative position', () => {
45-
expect(() => new InsertChange(sourceFile, -6, ' world!')).to.throw(Error);
45+
expect(() => new InsertChange(sourceFile, -6, ' world!')).toThrowError();
4646
});
4747
it('adds nothing in the source code if empty string is inserted', () => {
4848
let changeInstance = new InsertChange(sourceFile, 6, '');
4949
return changeInstance
5050
.apply()
5151
.then(() => readFile(sourceFile, 'utf8'))
5252
.then(contents => {
53-
expect(contents).to.equal('hello');
53+
expect(contents).toEqual('hello');
5454
});
5555
});
5656
});
@@ -64,19 +64,19 @@ describe('Change', () => {
6464
.apply()
6565
.then(() => readFile(sourceFile, 'utf8'))
6666
.then(contents => {
67-
expect(contents).to.equal('import * from "./bar"');
67+
expect(contents).toEqual('import * from "./bar"');
6868
});
6969
});
7070
it('fails for negative position', () => {
71-
expect(() => new RemoveChange(sourceFile, -6, ' world!')).to.throw(Error);
71+
expect(() => new RemoveChange(sourceFile, -6, ' world!')).toThrow();
7272
});
7373
it('does not change the file if told to remove empty string', () => {
7474
let changeInstance = new RemoveChange(sourceFile, 9, '');
7575
return changeInstance
7676
.apply()
7777
.then(() => readFile(sourceFile, 'utf8'))
7878
.then(contents => {
79-
expect(contents).to.equal('import * as foo from "./bar"');
79+
expect(contents).toEqual('import * as foo from "./bar"');
8080
});
8181
});
8282
});
@@ -89,12 +89,12 @@ describe('Change', () => {
8989
.apply()
9090
.then(() => readFile(sourceFile, 'utf8'))
9191
.then(contents => {
92-
expect(contents).to.equal('import { fooComponent } from "./bar"');
92+
expect(contents).toEqual('import { fooComponent } from "./bar"');
9393
});
9494
});
9595
it('fails for negative position', () => {
9696
let sourceFile = path.join(sourcePath, 'remove-replace-file.txt');
97-
expect(() => new ReplaceChange(sourceFile, -6, 'hello', ' world!')).to.throw(Error);
97+
expect(() => new ReplaceChange(sourceFile, -6, 'hello', ' world!')).toThrow();
9898
});
9999
it('adds string to the position of an empty string', () => {
100100
let sourceFile = path.join(sourcePath, 'replace-file.txt');
@@ -103,7 +103,7 @@ describe('Change', () => {
103103
.apply()
104104
.then(() => readFile(sourceFile, 'utf8'))
105105
.then(contents => {
106-
expect(contents).to.equal('import { BarComponent, FooComponent } from "./baz"');
106+
expect(contents).toEqual('import { BarComponent, FooComponent } from "./baz"');
107107
});
108108
});
109109
it('removes the given string only if an empty string to add is given', () => {
@@ -113,7 +113,7 @@ describe('Change', () => {
113113
.apply()
114114
.then(() => readFile(sourceFile, 'utf8'))
115115
.then(contents => {
116-
expect(contents).to.equal('import * from "./bar"');
116+
expect(contents).toEqual('import * from "./bar"');
117117
});
118118
});
119119
});

‎packages/ast-tools/src/change.ts

+152
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import fs = require('fs');
2+
import denodeify = require('denodeify');
3+
4+
const readFile = (denodeify(fs.readFile) as (...args: any[]) => Promise<string>);
5+
const writeFile = (denodeify(fs.writeFile) as (...args: any[]) => Promise<string>);
6+
7+
export interface Change {
8+
apply(): Promise<void>;
9+
10+
// The file this change should be applied to. Some changes might not apply to
11+
// a file (maybe the config).
12+
readonly path: string | null;
13+
14+
// The order this change should be applied. Normally the position inside the file.
15+
// Changes are applied from the bottom of a file to the top.
16+
readonly order: number;
17+
18+
// The description of this change. This will be outputted in a dry or verbose run.
19+
readonly description: string;
20+
}
21+
22+
23+
/**
24+
* An operation that does nothing.
25+
*/
26+
export class NoopChange implements Change {
27+
description = 'No operation.';
28+
order = Infinity;
29+
path: string = null;
30+
apply() { return Promise.resolve(); }
31+
}
32+
33+
/**
34+
* An operation that mixes two or more changes, and merge them (in order).
35+
* Can only apply to a single file. Use a ChangeManager to apply changes to multiple
36+
* files.
37+
*/
38+
export class MultiChange implements Change {
39+
private _path: string;
40+
private _changes: Change[];
41+
42+
constructor(...changes: (Change[] | Change)[]) {
43+
this._changes = [];
44+
[].concat(...changes).forEach(change => this.appendChange(change));
45+
}
46+
47+
appendChange(change: Change) {
48+
// Validate that the path is the same for everyone of those.
49+
if (this._path === undefined) {
50+
this._path = change.path;
51+
} else if (change.path !== this._path) {
52+
throw new Error('Cannot apply a change to a different path.');
53+
}
54+
this._changes.push(change);
55+
}
56+
57+
get description() {
58+
return `Changes:\n ${this._changes.map(x => x.description).join('\n ')}`;
59+
}
60+
// Always apply as early as the highest change.
61+
get order() { return Math.max(...this._changes.map(c => c.order)); }
62+
get path() { return this._path; }
63+
64+
apply() {
65+
return this._changes
66+
.sort((a: Change, b: Change) => b.order - a.order)
67+
.reduce((promise, change) => {
68+
return promise.then(() => change.apply())
69+
}, Promise.resolve());
70+
}
71+
}
72+
73+
74+
/**
75+
* Will add text to the source code.
76+
*/
77+
export class InsertChange implements Change {
78+
79+
order: number;
80+
description: string;
81+
82+
constructor(public path: string, private pos: number, private toAdd: string) {
83+
if (pos < 0) {
84+
throw new Error('Negative positions are invalid');
85+
}
86+
this.description = `Inserted ${toAdd} into position ${pos} of ${path}`;
87+
this.order = pos;
88+
}
89+
90+
/**
91+
* This method does not insert spaces if there is none in the original string.
92+
*/
93+
apply(): Promise<any> {
94+
return readFile(this.path, 'utf8').then(content => {
95+
let prefix = content.substring(0, this.pos);
96+
let suffix = content.substring(this.pos);
97+
return writeFile(this.path, `${prefix}${this.toAdd}${suffix}`);
98+
});
99+
}
100+
}
101+
102+
/**
103+
* Will remove text from the source code.
104+
*/
105+
export class RemoveChange implements Change {
106+
107+
order: number;
108+
description: string;
109+
110+
constructor(public path: string, private pos: number, private toRemove: string) {
111+
if (pos < 0) {
112+
throw new Error('Negative positions are invalid');
113+
}
114+
this.description = `Removed ${toRemove} into position ${pos} of ${path}`;
115+
this.order = pos;
116+
}
117+
118+
apply(): Promise<any> {
119+
return readFile(this.path, 'utf8').then(content => {
120+
let prefix = content.substring(0, this.pos);
121+
let suffix = content.substring(this.pos + this.toRemove.length);
122+
// TODO: throw error if toRemove doesn't match removed string.
123+
return writeFile(this.path, `${prefix}${suffix}`);
124+
});
125+
}
126+
}
127+
128+
/**
129+
* Will replace text from the source code.
130+
*/
131+
export class ReplaceChange implements Change {
132+
order: number;
133+
description: string;
134+
135+
constructor(public path: string, private pos: number, private oldText: string,
136+
private newText: string) {
137+
if (pos < 0) {
138+
throw new Error('Negative positions are invalid');
139+
}
140+
this.description = `Replaced ${oldText} into position ${pos} of ${path} with ${newText}`;
141+
this.order = pos;
142+
}
143+
144+
apply(): Promise<any> {
145+
return readFile(this.path, 'utf8').then(content => {
146+
let prefix = content.substring(0, this.pos);
147+
let suffix = content.substring(this.pos + this.oldText.length);
148+
// TODO: throw error if oldText doesn't match removed string.
149+
return writeFile(this.path, `${prefix}${this.newText}${suffix}`);
150+
});
151+
}
152+
}

‎packages/ast-tools/src/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './ast-utils';
2+
export * from './change';
3+
export * from './node';

‎packages/ast-tools/src/node.ts

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import ts = require('typescript');
2+
import {RemoveChange, Change} from './change';
3+
4+
5+
/**
6+
* Find all nodes from the AST in the subtree of node of SyntaxKind kind.
7+
* @param node
8+
* @param kind
9+
* @param max The maximum number of items to return.
10+
* @return all nodes of kind, or [] if none is found
11+
*/
12+
export function findNodes(node: ts.Node, kind: ts.SyntaxKind, max: number = Infinity): ts.Node[] {
13+
if (!node || max == 0) {
14+
return [];
15+
}
16+
17+
let arr: ts.Node[] = [];
18+
if (node.kind === kind) {
19+
arr.push(node);
20+
max--;
21+
}
22+
if (max > 0) {
23+
for (const child of node.getChildren()) {
24+
findNodes(child, kind, max).forEach(node => {
25+
if (max > 0) {
26+
arr.push(node);
27+
}
28+
max--;
29+
});
30+
31+
if (max <= 0) {
32+
break;
33+
}
34+
}
35+
}
36+
return arr;
37+
}
38+
39+
40+
export function removeAstNode(node: ts.Node): Change {
41+
const source = node.getSourceFile();
42+
return new RemoveChange(
43+
source.path,
44+
node.getStart(source),
45+
node.getFullText(source)
46+
);
47+
}

‎tests/acceptance/route-utils.spec.ts renamed to ‎packages/ast-tools/src/route-utils.spec.ts

+188-187
Large diffs are not rendered by default.

‎packages/ast-tools/src/route-utils.ts

+529
Large diffs are not rendered by default.

‎packages/ast-tools/src/spec-utils.ts

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// This file exports a version of the Jasmine `it` that understands promises.
2+
// To use this, simply `import {it} from './spec-utils`.
3+
// TODO(hansl): move this to its own Jasmine-TypeScript package.
4+
5+
function async(fn: () => PromiseLike<any> | void) {
6+
return (done: DoneFn) => {
7+
let result: PromiseLike<any> | void = null;
8+
9+
try {
10+
result = fn();
11+
12+
if (result && 'then' in result) {
13+
(result as Promise<any>).then(done, done.fail);
14+
} else {
15+
done();
16+
}
17+
} catch (err) {
18+
done.fail(err);
19+
}
20+
};
21+
}
22+
23+
24+
export function it(description: string, fn: () => PromiseLike<any> | void) {
25+
return (global as any)['it'](description, async(fn));
26+
}

‎packages/ast-tools/tsconfig.json

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"compilerOptions": {
3+
"declaration": true,
4+
"experimentalDecorators": true,
5+
"mapRoot": "",
6+
"module": "commonjs",
7+
"moduleResolution": "node",
8+
"noEmitOnError": true,
9+
"noImplicitAny": true,
10+
"outDir": "../../dist/ast-tools",
11+
"rootDir": ".",
12+
"sourceMap": true,
13+
"sourceRoot": "/",
14+
"target": "es5",
15+
"lib": ["es6"],
16+
"typeRoots": [
17+
"../../node_modules/@types"
18+
],
19+
"types": [
20+
"jasmine",
21+
"node"
22+
]
23+
}
24+
}

‎scripts/run-packages-spec.js

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/usr/bin/env node
2+
'use strict';
3+
4+
require('../lib/bootstrap-local');
5+
6+
const path = require('path');
7+
const Jasmine = require('jasmine');
8+
const JasmineSpecReporter = require('jasmine-spec-reporter');
9+
10+
const projectBaseDir = path.join(__dirname, '../packages');
11+
12+
// Create a Jasmine runner and configure it.
13+
const jasmine = new Jasmine({ projectBaseDir: projectBaseDir });
14+
jasmine.loadConfig({
15+
spec_dir: projectBaseDir
16+
});
17+
jasmine.addReporter(new JasmineSpecReporter());
18+
19+
// Run the tests.
20+
jasmine.execute(['**/*.spec.ts']);

‎tests/runner.js

+1-28
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,7 @@
11
/* eslint-disable no-console */
22
'use strict';
33

4-
const fs = require('fs');
5-
const ts = require('typescript');
6-
const old = require.extensions['.ts'];
7-
8-
require.extensions['.ts'] = function(m, filename) {
9-
if (!filename.match(/angular-cli/) && filename.match(/node_modules/)) {
10-
if (old) {
11-
return old(m, filename);
12-
}
13-
return m._compile(fs.readFileSync(filename), filename);
14-
}
15-
16-
const source = fs.readFileSync(filename).toString();
17-
18-
try {
19-
const result = ts.transpile(source, {
20-
target: ts.ScriptTarget.ES5,
21-
module: ts.ModuleKind.CommonJs
22-
});
23-
24-
// Send it to node to execute.
25-
return m._compile(result, filename);
26-
} catch (err) {
27-
console.error('Error while running script "' + filename + '":');
28-
console.error(err.stack);
29-
throw err;
30-
}
31-
};
4+
require('../lib/bootstrap-local');
325

336
var Mocha = require('mocha');
347
var glob = require('glob');

0 commit comments

Comments
 (0)
Please sign in to comment.