mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-15 10:11:50 +08:00
feature: splitting the ast into its own packages (#1828)
This commit is contained in:
parent
f9df8bb173
commit
b5e86c9fa9
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
dist/
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
|
||||
|
10
.travis.yml
10
.travis.yml
@ -12,6 +12,7 @@ env:
|
||||
- DBUS_SESSION_BUS_ADDRESS=/dev/null
|
||||
matrix:
|
||||
- SCRIPT=lint
|
||||
# - SCRIPT=build
|
||||
- SCRIPT=test
|
||||
# - TARGET=mobile SCRIPT=mobile_test
|
||||
matrix:
|
||||
@ -21,8 +22,13 @@ matrix:
|
||||
- os: osx
|
||||
node_js: "5"
|
||||
env: SCRIPT=lint
|
||||
# - os: osx
|
||||
# env: TARGET=mobile SCRIPT=mobile_test
|
||||
- node_js: "6"
|
||||
env: SCRIPT=build
|
||||
- os: osx
|
||||
node_js: "5"
|
||||
env: SCRIPT=build
|
||||
- os: osx
|
||||
env: TARGET=mobile SCRIPT=mobile_test
|
||||
|
||||
before_install:
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi
|
||||
|
@ -13,7 +13,10 @@
|
||||
"sourceMap": true,
|
||||
"sourceRoot": "/",
|
||||
"target": "es5",
|
||||
"lib": ["es6"]
|
||||
"lib": ["es6"],
|
||||
"paths": {
|
||||
"@angular-cli/ast-tools": [ "../../packages/ast-tools/src" ]
|
||||
}
|
||||
},
|
||||
"includes": [
|
||||
"./custom-typings.d.ts"
|
||||
|
@ -1,304 +1,12 @@
|
||||
import * as ts from 'typescript';
|
||||
import * as fs from 'fs';
|
||||
import {Symbols} from '@angular/tsc-wrapped/src/symbols';
|
||||
import {
|
||||
isMetadataImportedSymbolReferenceExpression,
|
||||
isMetadataModuleReferenceExpression
|
||||
} from '@angular/tsc-wrapped';
|
||||
import {Change, InsertChange, NoopChange, MultiChange} from './change';
|
||||
import {insertImport} from './route-utils';
|
||||
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
import {ReplaySubject} from 'rxjs/ReplaySubject';
|
||||
import 'rxjs/add/observable/of';
|
||||
import 'rxjs/add/operator/do';
|
||||
import 'rxjs/add/operator/filter';
|
||||
import 'rxjs/add/operator/last';
|
||||
import 'rxjs/add/operator/map';
|
||||
import 'rxjs/add/operator/mergeMap';
|
||||
import 'rxjs/add/operator/toArray';
|
||||
import 'rxjs/add/operator/toPromise';
|
||||
|
||||
|
||||
/**
|
||||
* Get TS source file based on path.
|
||||
* @param filePath
|
||||
* @return source file of ts.SourceFile kind
|
||||
*/
|
||||
export function getSource(filePath: string): ts.SourceFile {
|
||||
return ts.createSourceFile(filePath, fs.readFileSync(filePath).toString(),
|
||||
ts.ScriptTarget.ES6, true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all the nodes from a source, as an observable.
|
||||
* @param sourceFile The source file object.
|
||||
* @returns {Observable<ts.Node>} An observable of all the nodes in the source.
|
||||
*/
|
||||
export function getSourceNodes(sourceFile: ts.SourceFile): Observable<ts.Node> {
|
||||
const subject = new ReplaySubject<ts.Node>();
|
||||
let nodes: ts.Node[] = [sourceFile];
|
||||
|
||||
while(nodes.length > 0) {
|
||||
const node = nodes.shift();
|
||||
|
||||
if (node) {
|
||||
subject.next(node);
|
||||
if (node.getChildCount(sourceFile) >= 0) {
|
||||
nodes.unshift(...node.getChildren());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subject.complete();
|
||||
return subject.asObservable();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Find all nodes from the AST in the subtree of node of SyntaxKind kind.
|
||||
* @param node
|
||||
* @param kind
|
||||
* @param max The maximum number of items to return.
|
||||
* @return all nodes of kind, or [] if none is found
|
||||
*/
|
||||
export function findNodes(node: ts.Node, kind: ts.SyntaxKind, max: number = Infinity): ts.Node[] {
|
||||
if (!node || max == 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let arr: ts.Node[] = [];
|
||||
if (node.kind === kind) {
|
||||
arr.push(node);
|
||||
max--;
|
||||
}
|
||||
if (max > 0) {
|
||||
for (const child of node.getChildren()) {
|
||||
findNodes(child, kind, max).forEach(node => {
|
||||
if (max > 0) {
|
||||
arr.push(node);
|
||||
}
|
||||
max--;
|
||||
});
|
||||
|
||||
if (max <= 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Helper for sorting nodes.
|
||||
* @return function to sort nodes in increasing order of position in sourceFile
|
||||
*/
|
||||
function nodesByPosition(first: ts.Node, second: ts.Node): number {
|
||||
return first.pos - second.pos;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Insert `toInsert` after the last occurence of `ts.SyntaxKind[nodes[i].kind]`
|
||||
* or after the last of occurence of `syntaxKind` if the last occurence is a sub child
|
||||
* of ts.SyntaxKind[nodes[i].kind] and save the changes in file.
|
||||
*
|
||||
* @param nodes insert after the last occurence of nodes
|
||||
* @param toInsert string to insert
|
||||
* @param file file to insert changes into
|
||||
* @param fallbackPos position to insert if toInsert happens to be the first occurence
|
||||
* @param syntaxKind the ts.SyntaxKind of the subchildren to insert after
|
||||
* @return Change instance
|
||||
* @throw Error if toInsert is first occurence but fall back is not set
|
||||
*/
|
||||
export function insertAfterLastOccurrence(nodes: ts.Node[], toInsert: string,
|
||||
file: string, fallbackPos?: number, syntaxKind?: ts.SyntaxKind): Change {
|
||||
var lastItem = nodes.sort(nodesByPosition).pop();
|
||||
if (syntaxKind) {
|
||||
lastItem = findNodes(lastItem, syntaxKind).sort(nodesByPosition).pop();
|
||||
}
|
||||
if (!lastItem && fallbackPos == undefined) {
|
||||
throw new Error(`tried to insert ${toInsert} as first occurence with no fallback position`);
|
||||
}
|
||||
let lastItemPosition: number = lastItem ? lastItem.end : fallbackPos;
|
||||
return new InsertChange(file, lastItemPosition, toInsert);
|
||||
}
|
||||
|
||||
|
||||
export function getContentOfKeyLiteral(source: ts.SourceFile, node: ts.Node): string {
|
||||
if (node.kind == ts.SyntaxKind.Identifier) {
|
||||
return (<ts.Identifier>node).text;
|
||||
} else if (node.kind == ts.SyntaxKind.StringLiteral) {
|
||||
try {
|
||||
return JSON.parse(node.getFullText(source))
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function getDecoratorMetadata(source: ts.SourceFile, identifier: string,
|
||||
module: string): Observable<ts.Node> {
|
||||
const symbols = new Symbols(source);
|
||||
|
||||
return getSourceNodes(source)
|
||||
.filter(node => {
|
||||
return node.kind == ts.SyntaxKind.Decorator
|
||||
&& (<ts.Decorator>node).expression.kind == ts.SyntaxKind.CallExpression;
|
||||
})
|
||||
.map(node => <ts.CallExpression>(<ts.Decorator>node).expression)
|
||||
.filter(expr => {
|
||||
if (expr.expression.kind == ts.SyntaxKind.Identifier) {
|
||||
const id = <ts.Identifier>expr.expression;
|
||||
const metaData = symbols.resolve(id.getFullText(source));
|
||||
if (isMetadataImportedSymbolReferenceExpression(metaData)) {
|
||||
return metaData.name == identifier && metaData.module == module;
|
||||
}
|
||||
} else if (expr.expression.kind == ts.SyntaxKind.PropertyAccessExpression) {
|
||||
// This covers foo.NgModule when importing * as foo.
|
||||
const paExpr = <ts.PropertyAccessExpression>expr.expression;
|
||||
// If the left expression is not an identifier, just give up at that point.
|
||||
if (paExpr.expression.kind !== ts.SyntaxKind.Identifier) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const id = paExpr.name;
|
||||
const moduleId = <ts.Identifier>paExpr.expression;
|
||||
const moduleMetaData = symbols.resolve(moduleId.getFullText(source));
|
||||
if (isMetadataModuleReferenceExpression(moduleMetaData)) {
|
||||
return moduleMetaData.module == module && id.getFullText(source) == identifier;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.filter(expr => expr.arguments[0]
|
||||
&& expr.arguments[0].kind == ts.SyntaxKind.ObjectLiteralExpression)
|
||||
.map(expr => <ts.ObjectLiteralExpression>expr.arguments[0]);
|
||||
}
|
||||
|
||||
|
||||
function _addSymbolToNgModuleMetadata(ngModulePath: string, metadataField: string,
|
||||
symbolName: string, importPath: string) {
|
||||
const source: ts.SourceFile = getSource(ngModulePath);
|
||||
let metadata = getDecoratorMetadata(source, 'NgModule', '@angular/core');
|
||||
|
||||
// Find the decorator declaration.
|
||||
return metadata
|
||||
.toPromise()
|
||||
.then((node: ts.ObjectLiteralExpression) => {
|
||||
if (!node) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get all the children property assignment of object literals.
|
||||
return node.properties
|
||||
.filter(prop => prop.kind == ts.SyntaxKind.PropertyAssignment)
|
||||
// Filter out every fields that's not "metadataField". Also handles string literals
|
||||
// (but not expressions).
|
||||
.filter(prop => {
|
||||
switch (prop.name.kind) {
|
||||
case ts.SyntaxKind.Identifier:
|
||||
return prop.name.getText(source) == metadataField;
|
||||
case ts.SyntaxKind.StringLiteral:
|
||||
return prop.name.text == metadataField;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
})
|
||||
// Get the last node of the array literal.
|
||||
.then(matchingProperties => {
|
||||
if (!matchingProperties) {
|
||||
return;
|
||||
}
|
||||
if (matchingProperties.length == 0) {
|
||||
return metadata
|
||||
.toPromise();
|
||||
}
|
||||
|
||||
const assignment = <ts.PropertyAssignment>matchingProperties[0];
|
||||
|
||||
// If it's not an array, nothing we can do really.
|
||||
if (assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) {
|
||||
return Observable.empty();
|
||||
}
|
||||
|
||||
const arrLiteral = <ts.ArrayLiteralExpression>assignment.initializer;
|
||||
if (arrLiteral.elements.length == 0) {
|
||||
// Forward the property.
|
||||
return arrLiteral;
|
||||
}
|
||||
return arrLiteral.elements;
|
||||
})
|
||||
.then((node: ts.Node) => {
|
||||
if (!node) {
|
||||
console.log('No app module found. Please add your new class to your component.');
|
||||
return new NoopChange();
|
||||
}
|
||||
if (Array.isArray(node)) {
|
||||
node = node[node.length - 1];
|
||||
}
|
||||
|
||||
let toInsert;
|
||||
let position = node.getEnd();
|
||||
if (node.kind == ts.SyntaxKind.ObjectLiteralExpression) {
|
||||
// We haven't found the field in the metadata declaration. Insert a new
|
||||
// field.
|
||||
let expr = <ts.ObjectLiteralExpression>node;
|
||||
if (expr.properties.length == 0) {
|
||||
position = expr.getEnd() - 1;
|
||||
toInsert = ` ${metadataField}: [${symbolName}]\n`;
|
||||
} else {
|
||||
node = expr.properties[expr.properties.length - 1];
|
||||
position = node.getEnd();
|
||||
// Get the indentation of the last element, if any.
|
||||
const text = node.getFullText(source);
|
||||
if (text.startsWith('\n')) {
|
||||
toInsert = `,${text.match(/^\n(\r?)\s+/)[0]}${metadataField}: [${symbolName}]`;
|
||||
} else {
|
||||
toInsert = `, ${metadataField}: [${symbolName}]`;
|
||||
}
|
||||
}
|
||||
} else if (node.kind == ts.SyntaxKind.ArrayLiteralExpression) {
|
||||
// We found the field but it's empty. Insert it just before the `]`.
|
||||
position--;
|
||||
toInsert = `${symbolName}`;
|
||||
} else {
|
||||
// Get the indentation of the last element, if any.
|
||||
const text = node.getFullText(source);
|
||||
if (text.startsWith('\n')) {
|
||||
toInsert = `,${text.match(/^\n(\r?)\s+/)[0]}${symbolName}`;
|
||||
} else {
|
||||
toInsert = `, ${symbolName}`;
|
||||
}
|
||||
}
|
||||
|
||||
const insert = new InsertChange(ngModulePath, position, toInsert);
|
||||
const importInsert: Change = insertImport(ngModulePath, symbolName, importPath);
|
||||
return new MultiChange([insert, importInsert]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom function to insert a declaration (component, pipe, directive)
|
||||
* into NgModule declarations. It also imports the component.
|
||||
*/
|
||||
export function addComponentToModule(modulePath: string, classifiedName: string,
|
||||
importPath: string): Promise<Change> {
|
||||
|
||||
return _addSymbolToNgModuleMetadata(modulePath, 'declarations', classifiedName, importPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom function to insert a provider into NgModule. It also imports it.
|
||||
*/
|
||||
export function addProviderToModule(modulePath: string, classifiedName: string,
|
||||
importPath: string): Promise<Change> {
|
||||
return _addSymbolToNgModuleMetadata(modulePath, 'providers', classifiedName, importPath);
|
||||
}
|
||||
|
||||
// In order to keep refactoring low, simply export from ast-tools.
|
||||
// TODO: move all dependencies of this file to ast-tools directly.
|
||||
export {
|
||||
getSource,
|
||||
getSourceNodes,
|
||||
findNodes,
|
||||
insertAfterLastOccurrence,
|
||||
getContentOfKeyLiteral,
|
||||
getDecoratorMetadata,
|
||||
addComponentToModule,
|
||||
addProviderToModule
|
||||
} from '@angular-cli/ast-tools';
|
||||
|
@ -1,166 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
import * as Promise from 'ember-cli/lib/ext/promise';
|
||||
import fs = require('fs');
|
||||
|
||||
const readFile = Promise.denodeify(fs.readFile);
|
||||
const writeFile = Promise.denodeify(fs.writeFile);
|
||||
|
||||
export interface Change {
|
||||
|
||||
apply(): Promise<void>;
|
||||
|
||||
// The file this change should be applied to. Some changes might not apply to
|
||||
// a file (maybe the config).
|
||||
path: string | null;
|
||||
|
||||
// The order this change should be applied. Normally the position inside the file.
|
||||
// Changes are applied from the bottom of a file to the top.
|
||||
order: number;
|
||||
|
||||
// The description of this change. This will be outputted in a dry or verbose run.
|
||||
description: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* An operation that does nothing.
|
||||
*/
|
||||
export class NoopChange implements Change {
|
||||
get description() { return 'No operation.'; }
|
||||
get order() { return Infinity; }
|
||||
get path() { return null; }
|
||||
apply() { return Promise.resolve(); }
|
||||
}
|
||||
|
||||
/**
|
||||
* An operation that mixes two or more changes, and merge them (in order).
|
||||
* Can only apply to a single file. Use a ChangeManager to apply changes to multiple
|
||||
* files.
|
||||
*/
|
||||
export class MultiChange implements Change {
|
||||
private _path: string;
|
||||
private _changes: Change[];
|
||||
|
||||
constructor(...changes: Array<Change[], Change>) {
|
||||
this._changes = [];
|
||||
[].concat(...changes).forEach(change => this.appendChange(change));
|
||||
}
|
||||
|
||||
appendChange(change: Change) {
|
||||
// Validate that the path is the same for everyone of those.
|
||||
if (this._path === undefined) {
|
||||
this._path = change.path;
|
||||
} else if (change.path !== this._path) {
|
||||
throw new Error('Cannot apply a change to a different path.');
|
||||
}
|
||||
this._changes.push(change);
|
||||
}
|
||||
|
||||
get description() {
|
||||
return `Changes:\n ${this._changes.map(x => x.description).join('\n ')}`;
|
||||
}
|
||||
// Always apply as early as the highest change.
|
||||
get order() { return Math.max(...this._changes); }
|
||||
get path() { return this._path; }
|
||||
|
||||
apply() {
|
||||
return this._changes
|
||||
.sort((a: Change, b: Change) => b.order - a.order)
|
||||
.reduce((promise, change) => {
|
||||
return promise.then(() => change.apply())
|
||||
}, Promise.resolve());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Will add text to the source code.
|
||||
*/
|
||||
export class InsertChange implements Change {
|
||||
|
||||
const order: number;
|
||||
const description: string;
|
||||
|
||||
constructor(
|
||||
public path: string,
|
||||
private pos: number,
|
||||
private toAdd: string,
|
||||
) {
|
||||
if (pos < 0) {
|
||||
throw new Error('Negative positions are invalid');
|
||||
}
|
||||
this.description = `Inserted ${toAdd} into position ${pos} of ${path}`;
|
||||
this.order = pos;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method does not insert spaces if there is none in the original string.
|
||||
*/
|
||||
apply(): Promise<any> {
|
||||
return readFile(this.path, 'utf8').then(content => {
|
||||
let prefix = content.substring(0, this.pos);
|
||||
let suffix = content.substring(this.pos);
|
||||
return writeFile(this.path, `${prefix}${this.toAdd}${suffix}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will remove text from the source code.
|
||||
*/
|
||||
export class RemoveChange implements Change {
|
||||
|
||||
const order: number;
|
||||
const description: string;
|
||||
|
||||
constructor(
|
||||
public path: string,
|
||||
private pos: number,
|
||||
private toRemove: string) {
|
||||
if (pos < 0) {
|
||||
throw new Error('Negative positions are invalid');
|
||||
}
|
||||
this.description = `Removed ${toRemove} into position ${pos} of ${path}`;
|
||||
this.order = pos;
|
||||
}
|
||||
|
||||
apply(): Promise<any> {
|
||||
return readFile(this.path, 'utf8').then(content => {
|
||||
let prefix = content.substring(0, this.pos);
|
||||
let suffix = content.substring(this.pos + this.toRemove.length);
|
||||
// TODO: throw error if toRemove doesn't match removed string.
|
||||
return writeFile(this.path, `${prefix}${suffix}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will replace text from the source code.
|
||||
*/
|
||||
export class ReplaceChange implements Change {
|
||||
|
||||
const order: number;
|
||||
const description: string;
|
||||
|
||||
constructor(
|
||||
public path: string,
|
||||
private pos: number,
|
||||
private oldText: string,
|
||||
private newText: string) {
|
||||
if (pos < 0) {
|
||||
throw new Error('Negative positions are invalid');
|
||||
}
|
||||
this.description = `Replaced ${oldText} into position ${pos} of ${path} with ${newText}`;
|
||||
this.order = pos;
|
||||
}
|
||||
|
||||
apply(): Promise<any> {
|
||||
return readFile(this.path, 'utf8').then(content => {
|
||||
let prefix = content.substring(0, this.pos);
|
||||
let suffix = content.substring(this.pos + this.oldText.length);
|
||||
// TODO: throw error if oldText doesn't match removed string.
|
||||
return writeFile(this.path, `${prefix}${this.newText}${suffix}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
export {
|
||||
Change,
|
||||
NoopChange,
|
||||
MultiChange,
|
||||
InsertChange,
|
||||
RemoveChange,
|
||||
ReplaceChange
|
||||
} from '@angular-cli/ast-tools';
|
||||
|
@ -1,522 +1,11 @@
|
||||
import * as ts from 'typescript';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { Change, InsertChange } from './change';
|
||||
import * as Promise from 'ember-cli/lib/ext/promise';
|
||||
import {findNodes, insertAfterLastOccurrence } from './ast-utils';
|
||||
|
||||
/**
|
||||
* Adds imports to mainFile and adds toBootstrap to the array of providers
|
||||
* in bootstrap, if not present
|
||||
* @param mainFile main.ts
|
||||
* @param imports Object { importedClass: ['path/to/import/from', defaultStyleImport?] }
|
||||
* @param toBootstrap
|
||||
*/
|
||||
export function bootstrapItem(mainFile, imports: {[key: string]: [string, boolean?]}, toBootstrap: string ) {
|
||||
let changes = Object.keys(imports).map(importedClass => {
|
||||
var defaultStyleImport = imports[importedClass].length === 2 && imports[importedClass][1];
|
||||
return insertImport(mainFile, importedClass, imports[importedClass][0], defaultStyleImport);
|
||||
});
|
||||
let rootNode = getRootNode(mainFile);
|
||||
// get ExpressionStatements from the top level syntaxList of the sourceFile
|
||||
let bootstrapNodes = rootNode.getChildAt(0).getChildren().filter(node => {
|
||||
// get bootstrap expressions
|
||||
return node.kind === ts.SyntaxKind.ExpressionStatement &&
|
||||
node.getChildAt(0).getChildAt(0).text.toLowerCase() === 'bootstrap';
|
||||
});
|
||||
if (bootstrapNodes.length !== 1) {
|
||||
throw new Error(`Did not bootstrap provideRouter in ${mainFile}` +
|
||||
' because of multiple or no bootstrap calls');
|
||||
}
|
||||
let bootstrapNode = bootstrapNodes[0].getChildAt(0);
|
||||
let isBootstraped = findNodes(bootstrapNode, ts.SyntaxKind.SyntaxList) // get bootstrapped items
|
||||
.reduce((a, b) => a.concat(b.getChildren().map(n => n.getText())), [])
|
||||
.filter(n => n !== ',')
|
||||
.indexOf(toBootstrap) !== -1;
|
||||
if (isBootstraped) {
|
||||
return changes;
|
||||
}
|
||||
// if bracket exitst already, add configuration template,
|
||||
// otherwise, insert into bootstrap parens
|
||||
var fallBackPos: number, configurePathsTemplate: string, separator: string;
|
||||
var syntaxListNodes: any;
|
||||
let bootstrapProviders = bootstrapNode.getChildAt(2).getChildAt(2); // array of providers
|
||||
|
||||
if ( bootstrapProviders ) {
|
||||
syntaxListNodes = bootstrapProviders.getChildAt(1).getChildren();
|
||||
fallBackPos = bootstrapProviders.getChildAt(2).pos; // closeBracketLiteral
|
||||
separator = syntaxListNodes.length === 0 ? '' : ', ';
|
||||
configurePathsTemplate = `${separator}${toBootstrap}`;
|
||||
} else {
|
||||
fallBackPos = bootstrapNode.getChildAt(3).pos; // closeParenLiteral
|
||||
syntaxListNodes = bootstrapNode.getChildAt(2).getChildren();
|
||||
configurePathsTemplate = `, [ ${toBootstrap} ]`;
|
||||
}
|
||||
|
||||
changes.push(insertAfterLastOccurrence(syntaxListNodes, configurePathsTemplate,
|
||||
mainFile, fallBackPos));
|
||||
return changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Import `import { symbolName } from fileName` if the import doesn't exit
|
||||
* already. Assumes fileToEdit can be resolved and accessed.
|
||||
* @param fileToEdit (file we want to add import to)
|
||||
* @param symbolName (item to import)
|
||||
* @param fileName (path to the file)
|
||||
* @param isDefault (if true, import follows style for importing default exports)
|
||||
* @return Change
|
||||
*/
|
||||
|
||||
export function insertImport(fileToEdit: string, symbolName: string,
|
||||
fileName: string, isDefault = false): Change {
|
||||
let rootNode = getRootNode(fileToEdit);
|
||||
let allImports = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
|
||||
|
||||
// get nodes that map to import statements from the file fileName
|
||||
let relevantImports = allImports.filter(node => {
|
||||
// StringLiteral of the ImportDeclaration is the import file (fileName in this case).
|
||||
let importFiles = node.getChildren().filter(child => child.kind === ts.SyntaxKind.StringLiteral)
|
||||
.map(n => (<ts.StringLiteralTypeNode>n).text);
|
||||
return importFiles.filter(file => file === fileName).length === 1;
|
||||
});
|
||||
|
||||
if (relevantImports.length > 0) {
|
||||
|
||||
var importsAsterisk = false;
|
||||
// imports from import file
|
||||
let imports: ts.Node[] = [];
|
||||
relevantImports.forEach(n => {
|
||||
Array.prototype.push.apply(imports, findNodes(n, ts.SyntaxKind.Identifier));
|
||||
if (findNodes(n, ts.SyntaxKind.AsteriskToken).length > 0) {
|
||||
importsAsterisk = true;
|
||||
}
|
||||
});
|
||||
|
||||
// if imports * from fileName, don't add symbolName
|
||||
if (importsAsterisk) {
|
||||
return;
|
||||
}
|
||||
|
||||
let importTextNodes = imports.filter(n => (<ts.Identifier>n).text === symbolName);
|
||||
|
||||
// insert import if it's not there
|
||||
if (importTextNodes.length === 0) {
|
||||
let fallbackPos = findNodes(relevantImports[0], ts.SyntaxKind.CloseBraceToken)[0].pos ||
|
||||
findNodes(relevantImports[0], ts.SyntaxKind.FromKeyword)[0].pos;
|
||||
return insertAfterLastOccurrence(imports, `, ${symbolName}`, fileToEdit, fallbackPos);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// no such import declaration exists
|
||||
let useStrict = findNodes(rootNode, ts.SyntaxKind.StringLiteral)
|
||||
.filter(n => n.text === 'use strict');
|
||||
let fallbackPos = 0;
|
||||
if (useStrict.length > 0) {
|
||||
fallbackPos = useStrict[0].end;
|
||||
}
|
||||
let open = isDefault ? '' : '{ ';
|
||||
let close = isDefault ? '' : ' }';
|
||||
// if there are no imports or 'use strict' statement, insert import at beginning of file
|
||||
let insertAtBeginning = allImports.length === 0 && useStrict.length === 0;
|
||||
let separator = insertAtBeginning ? '' : ';\n';
|
||||
let toInsert = `${separator}import ${open}${symbolName}${close}` +
|
||||
` from '${fileName}'${insertAtBeginning ? ';\n' : ''}`;
|
||||
return insertAfterLastOccurrence(allImports, toInsert, fileToEdit, fallbackPos, ts.SyntaxKind.StringLiteral);
|
||||
};
|
||||
|
||||
/**
|
||||
* Inserts a path to the new route into src/routes.ts if it doesn't exist
|
||||
* @param routesFile
|
||||
* @param pathOptions
|
||||
* @return Change[]
|
||||
* @throws Error if routesFile has multiple export default or none.
|
||||
*/
|
||||
export function addPathToRoutes(routesFile: string, pathOptions: {[key: string]: any}): Change[] {
|
||||
let route = pathOptions.route.split('/')
|
||||
.filter(n => n !== '').join('/'); // change say `/about/:id/` to `about/:id`
|
||||
let isDefault = pathOptions.isDefault ? ', useAsDefault: true' : '';
|
||||
let outlet = pathOptions.outlet ? `, outlet: '${pathOptions.outlet}'` : '';
|
||||
|
||||
// create route path and resolve component import
|
||||
let positionalRoutes = /\/:[^/]*/g;
|
||||
let routePath = route.replace(positionalRoutes, '');
|
||||
routePath = `./app/${routePath}/${pathOptions.dasherizedName}.component`;
|
||||
let originalComponent = pathOptions.component;
|
||||
pathOptions.component = resolveImportName(pathOptions.component, routePath, pathOptions.routesFile);
|
||||
|
||||
var content = `{ path: '${route}', component: ${pathOptions.component}${isDefault}${outlet} }`;
|
||||
let rootNode = getRootNode(routesFile);
|
||||
let routesNode = rootNode.getChildAt(0).getChildren().filter(n => {
|
||||
// get export statement
|
||||
return n.kind === ts.SyntaxKind.ExportAssignment &&
|
||||
n.getFullText().indexOf('export default') !== -1;
|
||||
});
|
||||
if (routesNode.length !== 1) {
|
||||
throw new Error('Did not insert path in routes.ts because ' +
|
||||
`there were multiple or no 'export default' statements`);
|
||||
}
|
||||
var pos = routesNode[0].getChildAt(2).getChildAt(0).end; // openBracketLiteral
|
||||
// all routes in export route array
|
||||
let routesArray = routesNode[0].getChildAt(2).getChildAt(1)
|
||||
.getChildren()
|
||||
.filter(n => n.kind === ts.SyntaxKind.ObjectLiteralExpression);
|
||||
|
||||
if (pathExists(routesArray, route, pathOptions.component)) {
|
||||
// don't duplicate routes
|
||||
throw new Error('Route was not added since it is a duplicate');
|
||||
}
|
||||
var isChild = false;
|
||||
// get parent to insert under
|
||||
let parent;
|
||||
if (pathOptions.parent) {
|
||||
// append '_' to route to find the actual parent (not parent of the parent)
|
||||
parent = getParent(routesArray, `${pathOptions.parent}/_`);
|
||||
if (!parent) {
|
||||
throw new Error(`You specified parent '${pathOptions.parent}'' which was not found in routes.ts`);
|
||||
}
|
||||
if (route.indexOf(pathOptions.parent) === 0) {
|
||||
route = route.substring(pathOptions.parent.length);
|
||||
}
|
||||
} else {
|
||||
parent = getParent(routesArray, route);
|
||||
}
|
||||
|
||||
if (parent) {
|
||||
let childrenInfo = addChildPath(parent, pathOptions, route);
|
||||
if (!childrenInfo) {
|
||||
// path exists already
|
||||
throw new Error('Route was not added since it is a duplicate');
|
||||
}
|
||||
content = childrenInfo.newContent;
|
||||
pos = childrenInfo.pos;
|
||||
isChild = true;
|
||||
}
|
||||
|
||||
let isFirstElement = routesArray.length === 0;
|
||||
if (!isChild) {
|
||||
let separator = isFirstElement ? '\n' : ',';
|
||||
content = `\n ${content}${separator}`;
|
||||
}
|
||||
let changes: Change[] = [new InsertChange(routesFile, pos, content)];
|
||||
let component = originalComponent === pathOptions.component ? originalComponent :
|
||||
`${originalComponent} as ${pathOptions.component}`;
|
||||
routePath = routePath.replace(/\\/, '/'); // correction in windows
|
||||
changes.push(insertImport(routesFile, component, routePath));
|
||||
return changes;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add more properties to the route object in routes.ts
|
||||
* @param routesFile routes.ts
|
||||
* @param route Object {route: [key, value]}
|
||||
*/
|
||||
export function addItemsToRouteProperties(routesFile: string, routes: {[key: string]: [string, string]}) {
|
||||
let rootNode = getRootNode(routesFile);
|
||||
let routesNode = rootNode.getChildAt(0).getChildren().filter(n => {
|
||||
// get export statement
|
||||
return n.kind === ts.SyntaxKind.ExportAssignment &&
|
||||
n.getFullText().indexOf('export default') !== -1;
|
||||
});
|
||||
if (routesNode.length !== 1) {
|
||||
throw new Error('Did not insert path in routes.ts because ' +
|
||||
`there were multiple or no 'export default' statements`);
|
||||
}
|
||||
let routesArray = routesNode[0].getChildAt(2).getChildAt(1)
|
||||
.getChildren()
|
||||
.filter(n => n.kind === ts.SyntaxKind.ObjectLiteralExpression);
|
||||
let changes: Change[] = Object.keys(routes).reduce((result, route) => {
|
||||
// let route = routes[guardName][0];
|
||||
let itemKey = routes[route][0];
|
||||
let itemValue = routes[route][1];
|
||||
let currRouteNode = getParent(routesArray, `${route}/_`);
|
||||
if (!currRouteNode) {
|
||||
throw new Error(`Could not find '${route}' in routes.ts`);
|
||||
}
|
||||
let fallBackPos = findNodes(currRouteNode, ts.SyntaxKind.CloseBraceToken).pop().pos;
|
||||
let pathPropertiesNodes = currRouteNode.getChildAt(1).getChildren()
|
||||
.filter(n => n.kind === ts.SyntaxKind.PropertyAssignment);
|
||||
return result.concat([insertAfterLastOccurrence(pathPropertiesNodes,
|
||||
`, ${itemKey}: ${itemValue}`, routesFile, fallBackPos)]);
|
||||
}, []);
|
||||
return changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that a component file exports a class of the component
|
||||
* @param file
|
||||
* @param componentName
|
||||
* @return whether file exports componentName
|
||||
*/
|
||||
export function confirmComponentExport (file: string, componentName: string): boolean {
|
||||
const rootNode = getRootNode(file);
|
||||
let exportNodes = rootNode.getChildAt(0).getChildren().filter(n => {
|
||||
return n.kind === ts.SyntaxKind.ClassDeclaration &&
|
||||
(n.getChildren().filter(p => p.text === componentName).length !== 0);
|
||||
});
|
||||
return exportNodes.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures there is no collision between import names. If a collision occurs, resolve by adding
|
||||
* underscore number to the name
|
||||
* @param importName
|
||||
* @param importPath path to import component from
|
||||
* @param fileName (file to add import to)
|
||||
* @return resolved importName
|
||||
*/
|
||||
function resolveImportName (importName: string, importPath: string, fileName: string): string {
|
||||
const rootNode = getRootNode(fileName);
|
||||
// get all the import names
|
||||
let importNodes = rootNode.getChildAt(0).getChildren()
|
||||
.filter(n => n.kind === ts.SyntaxKind.ImportDeclaration);
|
||||
// check if imported file is same as current one before updating component name
|
||||
let importNames = importNodes
|
||||
.reduce((a, b) => {
|
||||
let importFrom = findNodes(b, ts.SyntaxKind.StringLiteral); // there's only one
|
||||
if (importFrom.pop().text !== importPath) {
|
||||
// importing from different file, add to imported components to inspect
|
||||
// if only one identifier { FooComponent }, if two { FooComponent as FooComponent_1 }
|
||||
// choose last element of identifier array in both cases
|
||||
return a.concat([findNodes(b, ts.SyntaxKind.Identifier).pop()]);
|
||||
}
|
||||
return a;
|
||||
}, [])
|
||||
.map(n => n.text);
|
||||
|
||||
const index = importNames.indexOf(importName);
|
||||
if (index === -1) {
|
||||
return importName;
|
||||
}
|
||||
const baseName = importNames[index].split('_')[0];
|
||||
var newName = baseName;
|
||||
var resolutionNumber = 1;
|
||||
while (importNames.indexOf(newName) !== -1) {
|
||||
newName = `${baseName}_${resolutionNumber}`;
|
||||
resolutionNumber++;
|
||||
}
|
||||
return newName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a path to a component file. If the path begins with path.sep, it is treated to be
|
||||
* absolute from the app/ directory. Otherwise, it is relative to currDir
|
||||
* @param projectRoot
|
||||
* @param currentDir
|
||||
* @param filePath componentName or path to componentName
|
||||
* @return component file name
|
||||
* @throw Error if component file referenced by path is not found
|
||||
*/
|
||||
export function resolveComponentPath(projectRoot: string, currentDir: string, filePath: string) {
|
||||
|
||||
let parsedPath = path.parse(filePath);
|
||||
let componentName = parsedPath.base.split('.')[0];
|
||||
let componentDir = path.parse(parsedPath.dir).base;
|
||||
|
||||
// correction for a case where path is /**/componentName/componentName(.component.ts)
|
||||
if ( componentName === componentDir) {
|
||||
filePath = parsedPath.dir;
|
||||
}
|
||||
if (parsedPath.dir === '') {
|
||||
// only component file name is given
|
||||
filePath = componentName;
|
||||
}
|
||||
var directory = filePath[0] === path.sep ?
|
||||
path.resolve(path.join(projectRoot, 'src', 'app', filePath)) : path.resolve(currentDir, filePath);
|
||||
|
||||
if (!fs.existsSync(directory)) {
|
||||
throw new Error(`path '${filePath}' must be relative to current directory` +
|
||||
` or absolute from project root`);
|
||||
}
|
||||
if (directory.indexOf('src' + path.sep + 'app') === -1) {
|
||||
throw new Error('Route must be within app');
|
||||
}
|
||||
let componentFile = path.join(directory, `${componentName}.component.ts`);
|
||||
if (!fs.existsSync(componentFile)) {
|
||||
throw new Error(`could not find component file referenced by ${filePath}`);
|
||||
}
|
||||
return componentFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort changes in decreasing order and apply them.
|
||||
* @param changes
|
||||
* @return Promise
|
||||
*/
|
||||
export function applyChanges(changes: Change[]): Promise<void> {
|
||||
return changes
|
||||
.filter(change => !!change)
|
||||
.sort((curr, next) => next.pos - curr.pos)
|
||||
.reduce((newChange, change) => newChange.then(() => change.apply()), Promise.resolve());
|
||||
}
|
||||
/**
|
||||
* Helper for addPathToRoutes. Adds child array to the appropriate position in the routes.ts file
|
||||
* @return Object (pos, newContent)
|
||||
*/
|
||||
function addChildPath (parentObject: ts.Node, pathOptions: {[key: string]: any}, route: string) {
|
||||
if (!parentObject) {
|
||||
return;
|
||||
}
|
||||
var pos: number;
|
||||
var newContent: string;
|
||||
|
||||
// get object with 'children' property
|
||||
let childrenNode = parentObject.getChildAt(1).getChildren()
|
||||
.filter(n => n.kind === ts.SyntaxKind.PropertyAssignment
|
||||
&& n.getChildAt(0).text === 'children');
|
||||
// find number of spaces to pad nested paths
|
||||
let nestingLevel = 1; // for indenting route object in the `children` array
|
||||
let n = parentObject;
|
||||
while (n.parent) {
|
||||
if (n.kind === ts.SyntaxKind.ObjectLiteralExpression
|
||||
|| n.kind === ts.SyntaxKind.ArrayLiteralExpression) {
|
||||
nestingLevel ++;
|
||||
}
|
||||
n = n.parent;
|
||||
}
|
||||
|
||||
// strip parent route
|
||||
let parentRoute = parentObject.getChildAt(1).getChildAt(0).getChildAt(2).text;
|
||||
let childRoute = route.substring(route.indexOf(parentRoute) + parentRoute.length + 1);
|
||||
|
||||
let isDefault = pathOptions.isDefault ? ', useAsDefault: true' : '';
|
||||
let outlet = pathOptions.outlet ? `, outlet: '${pathOptions.outlet}'` : '';
|
||||
let content = `{ path: '${childRoute}', component: ${pathOptions.component}` +
|
||||
`${isDefault}${outlet} }`;
|
||||
let spaces = Array(2 * nestingLevel + 1).join(' ');
|
||||
|
||||
if (childrenNode.length !== 0) {
|
||||
// add to beginning of children array
|
||||
pos = childrenNode[0].getChildAt(2).getChildAt(1).pos; // open bracket
|
||||
newContent = `\n${spaces}${content}, `;
|
||||
} else {
|
||||
// no children array, add one
|
||||
pos = parentObject.getChildAt(2).pos; // close brace
|
||||
newContent = `,\n${spaces.substring(2)}children: [\n${spaces}${content} ` +
|
||||
`\n${spaces.substring(2)}]\n${spaces.substring(5)}`;
|
||||
}
|
||||
return {newContent: newContent, pos: pos};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for addPathToRoutes.
|
||||
* @return parentNode which contains the children array to add a new path to or
|
||||
* undefined if none or the entire route was matched.
|
||||
*/
|
||||
function getParent(routesArray: ts.Node[], route: string, parent?: ts.Node): ts.Node {
|
||||
if (routesArray.length === 0 && !parent) {
|
||||
return; // no children array and no parent found
|
||||
}
|
||||
if (route.length === 0) {
|
||||
return; // route has been completely matched
|
||||
}
|
||||
var splitRoute = route.split('/');
|
||||
// don't treat positional parameters separately
|
||||
if (splitRoute.length > 1 && splitRoute[1].indexOf(':') !== -1) {
|
||||
let actualRoute = splitRoute.shift();
|
||||
splitRoute[0] = `${actualRoute}/${splitRoute[0]}`;
|
||||
}
|
||||
let potentialParents: ts.Node[] = routesArray // route nodes with same path as current route
|
||||
.filter(n => getValueForKey(n, 'path') === splitRoute[0]);
|
||||
if (potentialParents.length !== 0) {
|
||||
splitRoute.shift(); // matched current parent, move on
|
||||
route = splitRoute.join('/');
|
||||
}
|
||||
// get all children paths
|
||||
let newRouteArray = getChildrenArray(routesArray);
|
||||
if (route && parent && potentialParents.length === 0) {
|
||||
return parent; // final route is not matched. assign parent from here
|
||||
}
|
||||
parent = potentialParents.sort((a, b) => a.pos - b.pos).shift();
|
||||
return getParent(newRouteArray, route, parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for addPathToRoutes.
|
||||
* @return whether path with same route and component exists
|
||||
*/
|
||||
function pathExists(routesArray: ts.Node[], route: string, component: string, fullRoute?: string): boolean {
|
||||
if (routesArray.length === 0) {
|
||||
return false;
|
||||
}
|
||||
fullRoute = fullRoute ? fullRoute : route;
|
||||
var sameRoute = false;
|
||||
var splitRoute = route.split('/');
|
||||
// don't treat positional parameters separately
|
||||
if (splitRoute.length > 1 && splitRoute[1].indexOf(':') !== -1) {
|
||||
let actualRoute = splitRoute.shift();
|
||||
splitRoute[0] = `${actualRoute}/${splitRoute[0]}`;
|
||||
}
|
||||
let repeatedRoutes: ts.Node[] = routesArray.filter(n => {
|
||||
let currentRoute = getValueForKey(n, 'path');
|
||||
let sameComponent = getValueForKey(n, 'component') === component;
|
||||
|
||||
sameRoute = currentRoute === splitRoute[0];
|
||||
// Confirm that it's parents are the same
|
||||
if (sameRoute && sameComponent) {
|
||||
var path = currentRoute;
|
||||
let objExp = n.parent;
|
||||
while (objExp) {
|
||||
if (objExp.kind === ts.SyntaxKind.ObjectLiteralExpression) {
|
||||
let currentParentPath = getValueForKey(objExp, 'path');
|
||||
path = currentParentPath ? `${currentParentPath}/${path}` : path;
|
||||
}
|
||||
objExp = objExp.parent;
|
||||
}
|
||||
return path === fullRoute;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (sameRoute) {
|
||||
splitRoute.shift(); // matched current parent, move on
|
||||
route = splitRoute.join('/');
|
||||
}
|
||||
if (repeatedRoutes.length !== 0) {
|
||||
return true; // new path will be repeating if inserted. report that path already exists
|
||||
}
|
||||
|
||||
// all children paths
|
||||
let newRouteArray = getChildrenArray(routesArray);
|
||||
return pathExists(newRouteArray, route, component, fullRoute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for getParent and pathExists
|
||||
* @return array with all nodes holding children array under routes
|
||||
* in routesArray
|
||||
*/
|
||||
function getChildrenArray(routesArray: ts.Node[]): ts.Node[] {
|
||||
return routesArray.reduce((allRoutes, currRoute) => allRoutes.concat(
|
||||
currRoute.getChildAt(1).getChildren()
|
||||
.filter(n => n.kind === ts.SyntaxKind.PropertyAssignment
|
||||
&& n.getChildAt(0).text === 'children')
|
||||
.map(n => n.getChildAt(2).getChildAt(1)) // syntaxList containing chilren paths
|
||||
.reduce((childrenArray, currChild) => childrenArray.concat(currChild.getChildren()
|
||||
.filter(p => p.kind === ts.SyntaxKind.ObjectLiteralExpression)
|
||||
), [])
|
||||
), []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get the path text or component
|
||||
* @param objectLiteralNode
|
||||
* @param key 'path' or 'component'
|
||||
*/
|
||||
function getValueForKey(objectLiteralNode: ts.TypeNode.ObjectLiteralExpression, key: string) {
|
||||
let currentNode = key === 'component' ? objectLiteralNode.getChildAt(1).getChildAt(2) :
|
||||
objectLiteralNode.getChildAt(1).getChildAt(0);
|
||||
return currentNode && currentNode.getChildAt(0)
|
||||
&& currentNode.getChildAt(0).text === key && currentNode.getChildAt(2)
|
||||
&& currentNode.getChildAt(2).text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get AST from file
|
||||
* @param file
|
||||
*/
|
||||
function getRootNode(file: string) {
|
||||
return ts.createSourceFile(file, fs.readFileSync(file).toString(), ts.ScriptTarget.ES6, true);
|
||||
}
|
||||
// In order to keep refactoring low, simply export from ast-tools.
|
||||
// TODO: move all dependencies of this file to ast-tools directly.
|
||||
export {
|
||||
bootstrapItem,
|
||||
insertImport,
|
||||
addPathToRoutes,
|
||||
addItemsToRouteProperties,
|
||||
confirmComponentExport,
|
||||
resolveComponentPath,
|
||||
applyChanges
|
||||
} from '@angular-cli/ast-tools';
|
||||
|
3
bin/ng
3
bin/ng
@ -9,6 +9,9 @@ const exit = require('exit');
|
||||
const packageJson = require('../package.json');
|
||||
const Leek = require('leek');
|
||||
|
||||
require('../lib/bootstrap-local');
|
||||
|
||||
|
||||
resolve('angular-cli', { basedir: process.cwd() },
|
||||
function (error, projectLocalCli) {
|
||||
var cli;
|
||||
|
54
lib/bootstrap-local.js
vendored
Normal file
54
lib/bootstrap-local.js
vendored
Normal file
@ -0,0 +1,54 @@
|
||||
/* eslint-disable no-console */
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const ts = require('typescript');
|
||||
|
||||
|
||||
const oldRequireTs = require.extensions['.ts'];
|
||||
require.extensions['.ts'] = function(m, filename) {
|
||||
// If we're in node module, either call the old hook or simply compile the
|
||||
// file without transpilation. We do not touch node_modules/**.
|
||||
// We do touch `angular-cli` files anywhere though.
|
||||
if (!filename.match(/angular-cli/) && filename.match(/node_modules/)) {
|
||||
if (oldRequireTs) {
|
||||
return oldRequireTs(m, filename);
|
||||
}
|
||||
return m._compile(fs.readFileSync(filename), filename);
|
||||
}
|
||||
|
||||
// Node requires all require hooks to be sync.
|
||||
const source = fs.readFileSync(filename).toString();
|
||||
|
||||
try {
|
||||
const result = ts.transpile(source, {
|
||||
target: ts.ScriptTarget.ES5,
|
||||
module: ts.ModuleKind.CommonJs
|
||||
});
|
||||
|
||||
// Send it to node to execute.
|
||||
return m._compile(result, filename);
|
||||
} catch (err) {
|
||||
console.error('Error while running script "' + filename + '":');
|
||||
console.error(err.stack);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
// If we're running locally, meaning npm linked. This is basically "developer mode".
|
||||
if (!__dirname.match(/\/node_modules\//)) {
|
||||
const packages = require('./packages');
|
||||
|
||||
// We mock the module loader so that we can fake our packages when running locally.
|
||||
const Module = require('module');
|
||||
const oldLoad = Module._load;
|
||||
Module._load = function (request, parent) {
|
||||
if (request in packages) {
|
||||
return oldLoad.call(this, packages[request].main, parent);
|
||||
} else {
|
||||
return oldLoad.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
}
|
@ -1,40 +1,6 @@
|
||||
/*eslint-disable no-console */
|
||||
|
||||
// This file hooks up on require calls to transpile TypeScript.
|
||||
const fs = require('fs');
|
||||
const ts = require('typescript');
|
||||
const old = require.extensions['.ts'];
|
||||
|
||||
require.extensions['.ts'] = function(m, filename) {
|
||||
// If we're in node module, either call the old hook or simply compile the
|
||||
// file without transpilation. We do not touch node_modules/**.
|
||||
// We do touch `angular-cli` files anywhere though.
|
||||
if (!filename.match(/angular-cli/) && filename.match(/node_modules/)) {
|
||||
if (old) {
|
||||
return old(m, filename);
|
||||
}
|
||||
return m._compile(fs.readFileSync(filename), filename);
|
||||
}
|
||||
|
||||
// Node requires all require hooks to be sync.
|
||||
const source = fs.readFileSync(filename).toString();
|
||||
|
||||
try {
|
||||
const result = ts.transpile(source, {
|
||||
target: ts.ScriptTarget.ES5,
|
||||
module: ts.ModuleKind.CommonJs
|
||||
});
|
||||
|
||||
// Send it to node to execute.
|
||||
return m._compile(result, filename);
|
||||
} catch (err) {
|
||||
console.error('Error while running script "' + filename + '":');
|
||||
console.error(err.stack);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const cli = require('ember-cli/lib/cli');
|
||||
const path = require('path');
|
||||
|
||||
|
19
lib/packages.js
Normal file
19
lib/packages.js
Normal file
@ -0,0 +1,19 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const packageRoot = path.join(__dirname, '../packages');
|
||||
|
||||
// All the supported packages. Go through the packages directory and create a map of
|
||||
// name => fullPath.
|
||||
const packages = fs.readdirSync(packageRoot)
|
||||
.map(pkgName => ({ name: pkgName, root: path.join(packageRoot, pkgName) }))
|
||||
.filter(pkg => fs.statSync(pkg.root).isDirectory())
|
||||
.reduce((packages, pkg) => {
|
||||
packages[`@angular-cli/${pkg.name}`] = {
|
||||
root: pkg.root,
|
||||
main: path.resolve(pkg.root, 'src/index.ts')
|
||||
};
|
||||
return packages;
|
||||
}, {});
|
||||
|
||||
module.exports = packages;
|
19
package.json
19
package.json
@ -9,9 +9,12 @@
|
||||
},
|
||||
"keywords": [],
|
||||
"scripts": {
|
||||
"test": "node tests/runner",
|
||||
"build": "for PKG in packages/*; do echo Building $PKG...; tsc -P $PKG; done",
|
||||
"test": "npm run test:packages && npm run test:cli",
|
||||
"mobile_test": "mocha tests/e2e/e2e_workflow.spec.js",
|
||||
"test:cli": "node tests/runner",
|
||||
"test:inspect": "node --inspect --debug-brk tests/runner",
|
||||
"test:packages": "node scripts/run-packages-spec.js",
|
||||
"lint": "eslint .",
|
||||
"build-config-interface": "dtsgen lib/config/schema.json --out lib/config/schema.d.ts"
|
||||
},
|
||||
@ -35,9 +38,6 @@
|
||||
"dependencies": {
|
||||
"@angular/core": "^2.0.0-rc.5",
|
||||
"@angular/tsc-wrapped": "^0.2.2",
|
||||
"@types/lodash": "^4.0.25-alpha",
|
||||
"@types/rimraf": "0.0.25-alpha",
|
||||
"@types/webpack": "^1.12.22-alpha",
|
||||
"angular2-template-loader": "^0.5.0",
|
||||
"awesome-typescript-loader": "^2.2.1",
|
||||
"chalk": "^1.1.3",
|
||||
@ -90,7 +90,6 @@
|
||||
"stylus-loader": "^2.1.0",
|
||||
"symlink-or-copy": "^1.0.3",
|
||||
"ts-loader": "^0.8.2",
|
||||
"tslint": "^3.11.0",
|
||||
"tslint-loader": "^2.1.4",
|
||||
"typedoc": "^0.4.2",
|
||||
"typescript": "^2.0.0",
|
||||
@ -106,13 +105,21 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/denodeify": "^1.2.29",
|
||||
"@types/jasmine": "^2.2.32",
|
||||
"@types/lodash": "^4.0.25-alpha",
|
||||
"@types/mock-fs": "3.6.28",
|
||||
"@types/node": "^6.0.36",
|
||||
"@types/rimraf": "0.0.25-alpha",
|
||||
"@types/webpack": "^1.12.22-alpha",
|
||||
"chai": "^3.5.0",
|
||||
"conventional-changelog": "^1.1.0",
|
||||
"denodeify": "^1.2.1",
|
||||
"dtsgenerator": "^0.7.1",
|
||||
"eslint": "^2.8.0",
|
||||
"exists-sync": "0.0.3",
|
||||
"jasmine": "^2.4.1",
|
||||
"jasmine-spec-reporter": "^2.7.0",
|
||||
"minimatch": "^3.0.0",
|
||||
"mocha": "^2.4.5",
|
||||
"mock-fs": "3.10.0",
|
||||
@ -121,7 +128,7 @@
|
||||
"sinon": "^1.17.3",
|
||||
"through": "^2.3.8",
|
||||
"tree-kill": "^1.0.0",
|
||||
"tslint": "^3.8.1",
|
||||
"tslint": "^3.11.0",
|
||||
"walk-sync": "^0.2.6"
|
||||
}
|
||||
}
|
||||
|
27
packages/ast-tools/package.json
Normal file
27
packages/ast-tools/package.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "angular-cli",
|
||||
"version": "1.0.0-beta.11-webpack.2",
|
||||
"description": "CLI tool for Angular",
|
||||
"main": "./index.js",
|
||||
"keywords": [
|
||||
"angular",
|
||||
"cli",
|
||||
"ast",
|
||||
"tool"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/angular/angular-cli.git"
|
||||
},
|
||||
"author": "angular",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/angular/angular-cli/issues"
|
||||
},
|
||||
"homepage": "https://github.com/angular/angular-cli",
|
||||
"dependencies": {
|
||||
"rxjs": "^5.0.0-beta.11",
|
||||
"denodeify": "^1.2.1",
|
||||
"typescript": "^2.0.0"
|
||||
}
|
||||
}
|
@ -1,16 +1,15 @@
|
||||
import * as mockFs from 'mock-fs';
|
||||
import { expect } from 'chai';
|
||||
import * as ts from 'typescript';
|
||||
import * as fs from 'fs';
|
||||
import { InsertChange, RemoveChange } from '../../addon/ng2/utilities/change';
|
||||
import * as Promise from 'ember-cli/lib/ext/promise';
|
||||
import {
|
||||
findNodes,
|
||||
insertAfterLastOccurrence,
|
||||
addComponentToModule
|
||||
} from '../../addon/ng2/utilities/ast-utils';
|
||||
import denodeify = require('denodeify');
|
||||
import mockFs = require('mock-fs');
|
||||
import ts = require('typescript');
|
||||
import fs = require('fs');
|
||||
|
||||
import {InsertChange, RemoveChange} from './change';
|
||||
import {insertAfterLastOccurrence, addComponentToModule} from './ast-utils';
|
||||
import {findNodes} from './node';
|
||||
import {it} from './spec-utils';
|
||||
|
||||
const readFile = <any>denodeify(fs.readFile);
|
||||
|
||||
const readFile = Promise.denodeify(fs.readFile);
|
||||
|
||||
describe('ast-utils: findNodes', () => {
|
||||
const sourceFile = 'tmp/tmp.ts';
|
||||
@ -19,7 +18,7 @@ describe('ast-utils: findNodes', () => {
|
||||
let mockDrive = {
|
||||
'tmp': {
|
||||
'tmp.ts': `import * as myTest from 'tests' \n` +
|
||||
'hello.'
|
||||
'hello.'
|
||||
}
|
||||
};
|
||||
mockFs(mockDrive);
|
||||
@ -32,42 +31,42 @@ describe('ast-utils: findNodes', () => {
|
||||
it('finds no imports', () => {
|
||||
let editedFile = new RemoveChange(sourceFile, 0, `import * as myTest from 'tests' \n`);
|
||||
return editedFile
|
||||
.apply()
|
||||
.then(() => {
|
||||
let rootNode = getRootNode(sourceFile);
|
||||
let nodes = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
|
||||
expect(nodes).to.be.empty;
|
||||
});
|
||||
.apply()
|
||||
.then(() => {
|
||||
let rootNode = getRootNode(sourceFile);
|
||||
let nodes = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
|
||||
expect(nodes).toEqual([]);
|
||||
});
|
||||
});
|
||||
it('finds one import', () => {
|
||||
let rootNode = getRootNode(sourceFile);
|
||||
let nodes = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
|
||||
expect(nodes.length).to.equal(1);
|
||||
expect(nodes.length).toEqual(1);
|
||||
});
|
||||
it('finds two imports from inline declarations', () => {
|
||||
// remove new line and add an inline import
|
||||
let editedFile = new RemoveChange(sourceFile, 32, '\n');
|
||||
return editedFile
|
||||
.apply()
|
||||
.then(() => {
|
||||
let insert = new InsertChange(sourceFile, 32, `import {Routes} from '@angular/routes'`);
|
||||
return insert.apply();
|
||||
})
|
||||
.then(() => {
|
||||
let rootNode = getRootNode(sourceFile);
|
||||
let nodes = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
|
||||
expect(nodes.length).to.equal(2);
|
||||
});
|
||||
.apply()
|
||||
.then(() => {
|
||||
let insert = new InsertChange(sourceFile, 32, `import {Routes} from '@angular/routes'`);
|
||||
return insert.apply();
|
||||
})
|
||||
.then(() => {
|
||||
let rootNode = getRootNode(sourceFile);
|
||||
let nodes = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
|
||||
expect(nodes.length).toEqual(2);
|
||||
});
|
||||
});
|
||||
it('finds two imports from new line separated declarations', () => {
|
||||
let editedFile = new InsertChange(sourceFile, 33, `import {Routes} from '@angular/routes'`);
|
||||
return editedFile
|
||||
.apply()
|
||||
.then(() => {
|
||||
let rootNode = getRootNode(sourceFile);
|
||||
let nodes = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
|
||||
expect(nodes.length).to.equal(2);
|
||||
});
|
||||
.apply()
|
||||
.then(() => {
|
||||
let rootNode = getRootNode(sourceFile);
|
||||
let nodes = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
|
||||
expect(nodes.length).toEqual(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -89,86 +88,93 @@ describe('ast-utils: insertAfterLastOccurrence', () => {
|
||||
it('inserts at beginning of file', () => {
|
||||
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
|
||||
return insertAfterLastOccurrence(imports, `\nimport { Router } from '@angular/router';`,
|
||||
sourceFile, 0)
|
||||
.apply()
|
||||
.then(() => {
|
||||
return readFile(sourceFile, 'utf8');
|
||||
}).then((content) => {
|
||||
let expected = '\nimport { Router } from \'@angular/router\';';
|
||||
expect(content).to.equal(expected);
|
||||
});
|
||||
sourceFile, 0)
|
||||
.apply()
|
||||
.then(() => {
|
||||
return readFile(sourceFile, 'utf8');
|
||||
}).then((content) => {
|
||||
let expected = '\nimport { Router } from \'@angular/router\';';
|
||||
expect(content).toEqual(expected);
|
||||
});
|
||||
});
|
||||
it('throws an error if first occurence with no fallback position', () => {
|
||||
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
|
||||
expect(() => insertAfterLastOccurrence(imports, `import { Router } from '@angular/router';`,
|
||||
sourceFile)).to.throw(Error);
|
||||
sourceFile)).toThrowError();
|
||||
});
|
||||
it('inserts after last import', () => {
|
||||
let content = `import { foo, bar } from 'fizz';`;
|
||||
let editedFile = new InsertChange(sourceFile, 0, content);
|
||||
return editedFile
|
||||
.apply()
|
||||
.then(() => {
|
||||
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
|
||||
return insertAfterLastOccurrence(imports, ', baz', sourceFile,
|
||||
0, ts.SyntaxKind.Identifier)
|
||||
.apply();
|
||||
}).then(() => {
|
||||
return readFile(sourceFile, 'utf8');
|
||||
}).then(newContent => expect(newContent).to.equal(`import { foo, bar, baz } from 'fizz';`));
|
||||
.apply()
|
||||
.then(() => {
|
||||
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
|
||||
return insertAfterLastOccurrence(imports, ', baz', sourceFile,
|
||||
0, ts.SyntaxKind.Identifier)
|
||||
.apply();
|
||||
})
|
||||
.then(() => {
|
||||
return readFile(sourceFile, 'utf8');
|
||||
})
|
||||
.then(newContent => expect(newContent).toEqual(`import { foo, bar, baz } from 'fizz';`));
|
||||
});
|
||||
it('inserts after last import declaration', () => {
|
||||
let content = `import * from 'foo' \n import { bar } from 'baz'`;
|
||||
let editedFile = new InsertChange(sourceFile, 0, content);
|
||||
return editedFile
|
||||
.apply()
|
||||
.then(() => {
|
||||
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
|
||||
return insertAfterLastOccurrence(imports, `\nimport Router from '@angular/router'`,
|
||||
sourceFile)
|
||||
.apply();
|
||||
}).then(() => {
|
||||
return readFile(sourceFile, 'utf8');
|
||||
}).then(newContent => {
|
||||
let expected = `import * from 'foo' \n import { bar } from 'baz'` +
|
||||
`\nimport Router from '@angular/router'`;
|
||||
expect(newContent).to.equal(expected);
|
||||
});
|
||||
.apply()
|
||||
.then(() => {
|
||||
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
|
||||
return insertAfterLastOccurrence(imports, `\nimport Router from '@angular/router'`,
|
||||
sourceFile)
|
||||
.apply();
|
||||
})
|
||||
.then(() => {
|
||||
return readFile(sourceFile, 'utf8');
|
||||
})
|
||||
.then(newContent => {
|
||||
let expected = `import * from 'foo' \n import { bar } from 'baz'` +
|
||||
`\nimport Router from '@angular/router'`;
|
||||
expect(newContent).toEqual(expected);
|
||||
});
|
||||
});
|
||||
it('inserts correctly if no imports', () => {
|
||||
let content = `import {} from 'foo'`;
|
||||
let editedFile = new InsertChange(sourceFile, 0, content);
|
||||
return editedFile
|
||||
.apply()
|
||||
.then(() => {
|
||||
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
|
||||
return insertAfterLastOccurrence(imports, ', bar', sourceFile, undefined,
|
||||
ts.SyntaxKind.Identifier)
|
||||
.apply();
|
||||
}).catch(() => {
|
||||
return readFile(sourceFile, 'utf8');
|
||||
})
|
||||
.then(newContent => {
|
||||
expect(newContent).to.equal(content);
|
||||
// use a fallback position for safety
|
||||
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
|
||||
let pos = findNodes(imports.sort((a, b) => a.pos - b.pos).pop(),
|
||||
ts.SyntaxKind.CloseBraceToken).pop().pos;
|
||||
return insertAfterLastOccurrence(imports, ' bar ',
|
||||
sourceFile, pos, ts.SyntaxKind.Identifier)
|
||||
.apply();
|
||||
}).then(() => {
|
||||
return readFile(sourceFile, 'utf8');
|
||||
}).then(newContent => {
|
||||
expect(newContent).to.equal(`import { bar } from 'foo'`);
|
||||
});
|
||||
.apply()
|
||||
.then(() => {
|
||||
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
|
||||
return insertAfterLastOccurrence(imports, ', bar', sourceFile, undefined,
|
||||
ts.SyntaxKind.Identifier)
|
||||
.apply();
|
||||
})
|
||||
.catch(() => {
|
||||
return readFile(sourceFile, 'utf8');
|
||||
})
|
||||
.then(newContent => {
|
||||
expect(newContent).toEqual(content);
|
||||
// use a fallback position for safety
|
||||
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
|
||||
let pos = findNodes(imports.sort((a, b) => a.pos - b.pos).pop(),
|
||||
ts.SyntaxKind.CloseBraceToken).pop().pos;
|
||||
return insertAfterLastOccurrence(imports, ' bar ',
|
||||
sourceFile, pos, ts.SyntaxKind.Identifier)
|
||||
.apply();
|
||||
})
|
||||
.then(() => {
|
||||
return readFile(sourceFile, 'utf8');
|
||||
})
|
||||
.then(newContent => {
|
||||
expect(newContent).toEqual(`import { bar } from 'foo'`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('addComponentToModule', () => {
|
||||
beforeEach(() => {
|
||||
mockFs( {
|
||||
mockFs({
|
||||
'1.ts': `
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
@ -208,7 +214,7 @@ class Module {}`
|
||||
.then(change => change.apply())
|
||||
.then(() => readFile('1.ts', 'utf-8'))
|
||||
.then(content => {
|
||||
expect(content).to.equal(
|
||||
expect(content).toEqual(
|
||||
'\n' +
|
||||
'import {NgModule} from \'@angular/core\';\n' +
|
||||
'import { MyClass } from \'MyImportPath\';\n' +
|
||||
@ -218,7 +224,7 @@ class Module {}`
|
||||
'})\n' +
|
||||
'class Module {}'
|
||||
);
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
it('works with array with declarations', () => {
|
||||
@ -226,7 +232,7 @@ class Module {}`
|
||||
.then(change => change.apply())
|
||||
.then(() => readFile('2.ts', 'utf-8'))
|
||||
.then(content => {
|
||||
expect(content).to.equal(
|
||||
expect(content).toEqual(
|
||||
'\n' +
|
||||
'import {NgModule} from \'@angular/core\';\n' +
|
||||
'import { MyClass } from \'MyImportPath\';\n' +
|
||||
@ -239,7 +245,7 @@ class Module {}`
|
||||
'})\n' +
|
||||
'class Module {}'
|
||||
);
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
it('works without any declarations', () => {
|
||||
@ -247,7 +253,7 @@ class Module {}`
|
||||
.then(change => change.apply())
|
||||
.then(() => readFile('3.ts', 'utf-8'))
|
||||
.then(content => {
|
||||
expect(content).to.equal(
|
||||
expect(content).toEqual(
|
||||
'\n' +
|
||||
'import {NgModule} from \'@angular/core\';\n' +
|
||||
'import { MyClass } from \'MyImportPath\';\n' +
|
||||
@ -257,7 +263,7 @@ class Module {}`
|
||||
'})\n' +
|
||||
'class Module {}'
|
||||
);
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
it('works without a declaration field', () => {
|
||||
@ -265,7 +271,7 @@ class Module {}`
|
||||
.then(change => change.apply())
|
||||
.then(() => readFile('4.ts', 'utf-8'))
|
||||
.then(content => {
|
||||
expect(content).to.equal(
|
||||
expect(content).toEqual(
|
||||
'\n' +
|
||||
'import {NgModule} from \'@angular/core\';\n' +
|
||||
'import { MyClass } from \'MyImportPath\';\n' +
|
||||
@ -277,11 +283,11 @@ class Module {}`
|
||||
'})\n' +
|
||||
'class Module {}'
|
||||
);
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
/**
|
||||
* Gets node of kind kind from sourceFile
|
||||
*/
|
||||
function getNodesOfKind(kind: ts.SyntaxKind, sourceFile: string) {
|
||||
@ -290,5 +296,5 @@ function getNodesOfKind(kind: ts.SyntaxKind, sourceFile: string) {
|
||||
|
||||
function getRootNode(sourceFile: string) {
|
||||
return ts.createSourceFile(sourceFile, fs.readFileSync(sourceFile).toString(),
|
||||
ts.ScriptTarget.ES6, true);
|
||||
ts.ScriptTarget.ES6, true);
|
||||
}
|
271
packages/ast-tools/src/ast-utils.ts
Normal file
271
packages/ast-tools/src/ast-utils.ts
Normal file
@ -0,0 +1,271 @@
|
||||
import * as ts from 'typescript';
|
||||
import * as fs from 'fs';
|
||||
import {Symbols} from '@angular/tsc-wrapped/src/symbols';
|
||||
import {
|
||||
isMetadataImportedSymbolReferenceExpression,
|
||||
isMetadataModuleReferenceExpression
|
||||
} from '@angular/tsc-wrapped';
|
||||
import {Change, InsertChange, NoopChange, MultiChange} from './change';
|
||||
import {findNodes} from './node';
|
||||
import {insertImport} from './route-utils';
|
||||
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
import {ReplaySubject} from 'rxjs/ReplaySubject';
|
||||
import 'rxjs/add/observable/empty';
|
||||
import 'rxjs/add/observable/of';
|
||||
import 'rxjs/add/operator/do';
|
||||
import 'rxjs/add/operator/filter';
|
||||
import 'rxjs/add/operator/last';
|
||||
import 'rxjs/add/operator/map';
|
||||
import 'rxjs/add/operator/mergeMap';
|
||||
import 'rxjs/add/operator/toArray';
|
||||
import 'rxjs/add/operator/toPromise';
|
||||
|
||||
|
||||
/**
|
||||
* Get TS source file based on path.
|
||||
* @param filePath
|
||||
* @return source file of ts.SourceFile kind
|
||||
*/
|
||||
export function getSource(filePath: string): ts.SourceFile {
|
||||
return ts.createSourceFile(filePath, fs.readFileSync(filePath).toString(),
|
||||
ts.ScriptTarget.ES6, true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all the nodes from a source, as an observable.
|
||||
* @param sourceFile The source file object.
|
||||
* @returns {Observable<ts.Node>} An observable of all the nodes in the source.
|
||||
*/
|
||||
export function getSourceNodes(sourceFile: ts.SourceFile): Observable<ts.Node> {
|
||||
const subject = new ReplaySubject<ts.Node>();
|
||||
let nodes: ts.Node[] = [sourceFile];
|
||||
|
||||
while(nodes.length > 0) {
|
||||
const node = nodes.shift();
|
||||
|
||||
if (node) {
|
||||
subject.next(node);
|
||||
if (node.getChildCount(sourceFile) >= 0) {
|
||||
nodes.unshift(...node.getChildren());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subject.complete();
|
||||
return subject.asObservable();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Helper for sorting nodes.
|
||||
* @return function to sort nodes in increasing order of position in sourceFile
|
||||
*/
|
||||
function nodesByPosition(first: ts.Node, second: ts.Node): number {
|
||||
return first.pos - second.pos;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Insert `toInsert` after the last occurence of `ts.SyntaxKind[nodes[i].kind]`
|
||||
* or after the last of occurence of `syntaxKind` if the last occurence is a sub child
|
||||
* of ts.SyntaxKind[nodes[i].kind] and save the changes in file.
|
||||
*
|
||||
* @param nodes insert after the last occurence of nodes
|
||||
* @param toInsert string to insert
|
||||
* @param file file to insert changes into
|
||||
* @param fallbackPos position to insert if toInsert happens to be the first occurence
|
||||
* @param syntaxKind the ts.SyntaxKind of the subchildren to insert after
|
||||
* @return Change instance
|
||||
* @throw Error if toInsert is first occurence but fall back is not set
|
||||
*/
|
||||
export function insertAfterLastOccurrence(nodes: ts.Node[], toInsert: string,
|
||||
file: string, fallbackPos?: number, syntaxKind?: ts.SyntaxKind): Change {
|
||||
var lastItem = nodes.sort(nodesByPosition).pop();
|
||||
if (syntaxKind) {
|
||||
lastItem = findNodes(lastItem, syntaxKind).sort(nodesByPosition).pop();
|
||||
}
|
||||
if (!lastItem && fallbackPos == undefined) {
|
||||
throw new Error(`tried to insert ${toInsert} as first occurence with no fallback position`);
|
||||
}
|
||||
let lastItemPosition: number = lastItem ? lastItem.end : fallbackPos;
|
||||
return new InsertChange(file, lastItemPosition, toInsert);
|
||||
}
|
||||
|
||||
|
||||
export function getContentOfKeyLiteral(source: ts.SourceFile, node: ts.Node): string {
|
||||
if (node.kind == ts.SyntaxKind.Identifier) {
|
||||
return (<ts.Identifier>node).text;
|
||||
} else if (node.kind == ts.SyntaxKind.StringLiteral) {
|
||||
try {
|
||||
return JSON.parse(node.getFullText(source))
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function getDecoratorMetadata(source: ts.SourceFile, identifier: string,
|
||||
module: string): Observable<ts.Node> {
|
||||
const symbols = new Symbols(source as any);
|
||||
|
||||
return getSourceNodes(source)
|
||||
.filter(node => {
|
||||
return node.kind == ts.SyntaxKind.Decorator
|
||||
&& (<ts.Decorator>node).expression.kind == ts.SyntaxKind.CallExpression;
|
||||
})
|
||||
.map(node => <ts.CallExpression>(<ts.Decorator>node).expression)
|
||||
.filter(expr => {
|
||||
if (expr.expression.kind == ts.SyntaxKind.Identifier) {
|
||||
const id = <ts.Identifier>expr.expression;
|
||||
const metaData = symbols.resolve(id.getFullText(source));
|
||||
if (isMetadataImportedSymbolReferenceExpression(metaData)) {
|
||||
return metaData.name == identifier && metaData.module == module;
|
||||
}
|
||||
} else if (expr.expression.kind == ts.SyntaxKind.PropertyAccessExpression) {
|
||||
// This covers foo.NgModule when importing * as foo.
|
||||
const paExpr = <ts.PropertyAccessExpression>expr.expression;
|
||||
// If the left expression is not an identifier, just give up at that point.
|
||||
if (paExpr.expression.kind !== ts.SyntaxKind.Identifier) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const id = paExpr.name;
|
||||
const moduleId = <ts.Identifier>paExpr.expression;
|
||||
const moduleMetaData = symbols.resolve(moduleId.getFullText(source));
|
||||
if (isMetadataModuleReferenceExpression(moduleMetaData)) {
|
||||
return moduleMetaData.module == module && id.getFullText(source) == identifier;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.filter(expr => expr.arguments[0]
|
||||
&& expr.arguments[0].kind == ts.SyntaxKind.ObjectLiteralExpression)
|
||||
.map(expr => <ts.ObjectLiteralExpression>expr.arguments[0]);
|
||||
}
|
||||
|
||||
|
||||
function _addSymbolToNgModuleMetadata(ngModulePath: string, metadataField: string,
|
||||
symbolName: string, importPath: string) {
|
||||
const source: ts.SourceFile = getSource(ngModulePath);
|
||||
let metadata = getDecoratorMetadata(source, 'NgModule', '@angular/core');
|
||||
|
||||
// Find the decorator declaration.
|
||||
return metadata
|
||||
.toPromise()
|
||||
.then((node: ts.ObjectLiteralExpression) => {
|
||||
if (!node) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get all the children property assignment of object literals.
|
||||
return node.properties
|
||||
.filter(prop => prop.kind == ts.SyntaxKind.PropertyAssignment)
|
||||
// Filter out every fields that's not "metadataField". Also handles string literals
|
||||
// (but not expressions).
|
||||
.filter((prop: ts.PropertyAssignment) => {
|
||||
const name = prop.name;
|
||||
switch (name.kind) {
|
||||
case ts.SyntaxKind.Identifier:
|
||||
return (name as ts.Identifier).getText(source) == metadataField;
|
||||
case ts.SyntaxKind.StringLiteral:
|
||||
return (name as ts.StringLiteral).text == metadataField;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
})
|
||||
// Get the last node of the array literal.
|
||||
.then((matchingProperties: ts.ObjectLiteralElement[]): any => {
|
||||
if (!matchingProperties) {
|
||||
return null;
|
||||
}
|
||||
if (matchingProperties.length == 0) {
|
||||
return metadata.toPromise();
|
||||
}
|
||||
|
||||
const assignment = <ts.PropertyAssignment>matchingProperties[0];
|
||||
|
||||
// If it's not an array, nothing we can do really.
|
||||
if (assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const arrLiteral = <ts.ArrayLiteralExpression>assignment.initializer;
|
||||
if (arrLiteral.elements.length == 0) {
|
||||
// Forward the property.
|
||||
return arrLiteral;
|
||||
}
|
||||
return arrLiteral.elements;
|
||||
})
|
||||
.then((node: ts.Node) => {
|
||||
if (!node) {
|
||||
console.log('No app module found. Please add your new class to your component.');
|
||||
return new NoopChange();
|
||||
}
|
||||
if (Array.isArray(node)) {
|
||||
node = node[node.length - 1];
|
||||
}
|
||||
|
||||
let toInsert: string;
|
||||
let position = node.getEnd();
|
||||
if (node.kind == ts.SyntaxKind.ObjectLiteralExpression) {
|
||||
// We haven't found the field in the metadata declaration. Insert a new
|
||||
// field.
|
||||
let expr = <ts.ObjectLiteralExpression>node;
|
||||
if (expr.properties.length == 0) {
|
||||
position = expr.getEnd() - 1;
|
||||
toInsert = ` ${metadataField}: [${symbolName}]\n`;
|
||||
} else {
|
||||
node = expr.properties[expr.properties.length - 1];
|
||||
position = node.getEnd();
|
||||
// Get the indentation of the last element, if any.
|
||||
const text = node.getFullText(source);
|
||||
if (text.startsWith('\n')) {
|
||||
toInsert = `,${text.match(/^\n(\r?)\s+/)[0]}${metadataField}: [${symbolName}]`;
|
||||
} else {
|
||||
toInsert = `, ${metadataField}: [${symbolName}]`;
|
||||
}
|
||||
}
|
||||
} else if (node.kind == ts.SyntaxKind.ArrayLiteralExpression) {
|
||||
// We found the field but it's empty. Insert it just before the `]`.
|
||||
position--;
|
||||
toInsert = `${symbolName}`;
|
||||
} else {
|
||||
// Get the indentation of the last element, if any.
|
||||
const text = node.getFullText(source);
|
||||
if (text.startsWith('\n')) {
|
||||
toInsert = `,${text.match(/^\n(\r?)\s+/)[0]}${symbolName}`;
|
||||
} else {
|
||||
toInsert = `, ${symbolName}`;
|
||||
}
|
||||
}
|
||||
|
||||
const insert = new InsertChange(ngModulePath, position, toInsert);
|
||||
const importInsert: Change = insertImport(ngModulePath, symbolName, importPath);
|
||||
return new MultiChange([insert, importInsert]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom function to insert a declaration (component, pipe, directive)
|
||||
* into NgModule declarations. It also imports the component.
|
||||
*/
|
||||
export function addComponentToModule(modulePath: string, classifiedName: string,
|
||||
importPath: string): Promise<Change> {
|
||||
|
||||
return _addSymbolToNgModuleMetadata(modulePath, 'declarations', classifiedName, importPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom function to insert a provider into NgModule. It also imports it.
|
||||
*/
|
||||
export function addProviderToModule(modulePath: string, classifiedName: string,
|
||||
importPath: string): Promise<Change> {
|
||||
return _addSymbolToNgModuleMetadata(modulePath, 'providers', classifiedName, importPath);
|
||||
}
|
||||
|
@ -3,8 +3,8 @@
|
||||
// This needs to be first so fs module can be mocked correctly.
|
||||
let mockFs = require('mock-fs');
|
||||
|
||||
import {expect} from 'chai';
|
||||
import {InsertChange, RemoveChange, ReplaceChange} from '../../addon/ng2/utilities/change';
|
||||
import {it} from './spec-utils';
|
||||
import {InsertChange, RemoveChange, ReplaceChange} from './change';
|
||||
import fs = require('fs');
|
||||
|
||||
let path = require('path');
|
||||
@ -38,11 +38,11 @@ describe('Change', () => {
|
||||
.apply()
|
||||
.then(() => readFile(sourceFile, 'utf8'))
|
||||
.then(contents => {
|
||||
expect(contents).to.equal('hello world!');
|
||||
expect(contents).toEqual('hello world!');
|
||||
});
|
||||
});
|
||||
it('fails for negative position', () => {
|
||||
expect(() => new InsertChange(sourceFile, -6, ' world!')).to.throw(Error);
|
||||
expect(() => new InsertChange(sourceFile, -6, ' world!')).toThrowError();
|
||||
});
|
||||
it('adds nothing in the source code if empty string is inserted', () => {
|
||||
let changeInstance = new InsertChange(sourceFile, 6, '');
|
||||
@ -50,7 +50,7 @@ describe('Change', () => {
|
||||
.apply()
|
||||
.then(() => readFile(sourceFile, 'utf8'))
|
||||
.then(contents => {
|
||||
expect(contents).to.equal('hello');
|
||||
expect(contents).toEqual('hello');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -64,11 +64,11 @@ describe('Change', () => {
|
||||
.apply()
|
||||
.then(() => readFile(sourceFile, 'utf8'))
|
||||
.then(contents => {
|
||||
expect(contents).to.equal('import * from "./bar"');
|
||||
expect(contents).toEqual('import * from "./bar"');
|
||||
});
|
||||
});
|
||||
it('fails for negative position', () => {
|
||||
expect(() => new RemoveChange(sourceFile, -6, ' world!')).to.throw(Error);
|
||||
expect(() => new RemoveChange(sourceFile, -6, ' world!')).toThrow();
|
||||
});
|
||||
it('does not change the file if told to remove empty string', () => {
|
||||
let changeInstance = new RemoveChange(sourceFile, 9, '');
|
||||
@ -76,7 +76,7 @@ describe('Change', () => {
|
||||
.apply()
|
||||
.then(() => readFile(sourceFile, 'utf8'))
|
||||
.then(contents => {
|
||||
expect(contents).to.equal('import * as foo from "./bar"');
|
||||
expect(contents).toEqual('import * as foo from "./bar"');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -89,12 +89,12 @@ describe('Change', () => {
|
||||
.apply()
|
||||
.then(() => readFile(sourceFile, 'utf8'))
|
||||
.then(contents => {
|
||||
expect(contents).to.equal('import { fooComponent } from "./bar"');
|
||||
expect(contents).toEqual('import { fooComponent } from "./bar"');
|
||||
});
|
||||
});
|
||||
it('fails for negative position', () => {
|
||||
let sourceFile = path.join(sourcePath, 'remove-replace-file.txt');
|
||||
expect(() => new ReplaceChange(sourceFile, -6, 'hello', ' world!')).to.throw(Error);
|
||||
expect(() => new ReplaceChange(sourceFile, -6, 'hello', ' world!')).toThrow();
|
||||
});
|
||||
it('adds string to the position of an empty string', () => {
|
||||
let sourceFile = path.join(sourcePath, 'replace-file.txt');
|
||||
@ -103,7 +103,7 @@ describe('Change', () => {
|
||||
.apply()
|
||||
.then(() => readFile(sourceFile, 'utf8'))
|
||||
.then(contents => {
|
||||
expect(contents).to.equal('import { BarComponent, FooComponent } from "./baz"');
|
||||
expect(contents).toEqual('import { BarComponent, FooComponent } from "./baz"');
|
||||
});
|
||||
});
|
||||
it('removes the given string only if an empty string to add is given', () => {
|
||||
@ -113,7 +113,7 @@ describe('Change', () => {
|
||||
.apply()
|
||||
.then(() => readFile(sourceFile, 'utf8'))
|
||||
.then(contents => {
|
||||
expect(contents).to.equal('import * from "./bar"');
|
||||
expect(contents).toEqual('import * from "./bar"');
|
||||
});
|
||||
});
|
||||
});
|
152
packages/ast-tools/src/change.ts
Normal file
152
packages/ast-tools/src/change.ts
Normal file
@ -0,0 +1,152 @@
|
||||
import fs = require('fs');
|
||||
import denodeify = require('denodeify');
|
||||
|
||||
const readFile = (denodeify(fs.readFile) as (...args: any[]) => Promise<string>);
|
||||
const writeFile = (denodeify(fs.writeFile) as (...args: any[]) => Promise<string>);
|
||||
|
||||
export interface Change {
|
||||
apply(): Promise<void>;
|
||||
|
||||
// The file this change should be applied to. Some changes might not apply to
|
||||
// a file (maybe the config).
|
||||
readonly path: string | null;
|
||||
|
||||
// The order this change should be applied. Normally the position inside the file.
|
||||
// Changes are applied from the bottom of a file to the top.
|
||||
readonly order: number;
|
||||
|
||||
// The description of this change. This will be outputted in a dry or verbose run.
|
||||
readonly description: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* An operation that does nothing.
|
||||
*/
|
||||
export class NoopChange implements Change {
|
||||
description = 'No operation.';
|
||||
order = Infinity;
|
||||
path: string = null;
|
||||
apply() { return Promise.resolve(); }
|
||||
}
|
||||
|
||||
/**
|
||||
* An operation that mixes two or more changes, and merge them (in order).
|
||||
* Can only apply to a single file. Use a ChangeManager to apply changes to multiple
|
||||
* files.
|
||||
*/
|
||||
export class MultiChange implements Change {
|
||||
private _path: string;
|
||||
private _changes: Change[];
|
||||
|
||||
constructor(...changes: (Change[] | Change)[]) {
|
||||
this._changes = [];
|
||||
[].concat(...changes).forEach(change => this.appendChange(change));
|
||||
}
|
||||
|
||||
appendChange(change: Change) {
|
||||
// Validate that the path is the same for everyone of those.
|
||||
if (this._path === undefined) {
|
||||
this._path = change.path;
|
||||
} else if (change.path !== this._path) {
|
||||
throw new Error('Cannot apply a change to a different path.');
|
||||
}
|
||||
this._changes.push(change);
|
||||
}
|
||||
|
||||
get description() {
|
||||
return `Changes:\n ${this._changes.map(x => x.description).join('\n ')}`;
|
||||
}
|
||||
// Always apply as early as the highest change.
|
||||
get order() { return Math.max(...this._changes.map(c => c.order)); }
|
||||
get path() { return this._path; }
|
||||
|
||||
apply() {
|
||||
return this._changes
|
||||
.sort((a: Change, b: Change) => b.order - a.order)
|
||||
.reduce((promise, change) => {
|
||||
return promise.then(() => change.apply())
|
||||
}, Promise.resolve());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Will add text to the source code.
|
||||
*/
|
||||
export class InsertChange implements Change {
|
||||
|
||||
order: number;
|
||||
description: string;
|
||||
|
||||
constructor(public path: string, private pos: number, private toAdd: string) {
|
||||
if (pos < 0) {
|
||||
throw new Error('Negative positions are invalid');
|
||||
}
|
||||
this.description = `Inserted ${toAdd} into position ${pos} of ${path}`;
|
||||
this.order = pos;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method does not insert spaces if there is none in the original string.
|
||||
*/
|
||||
apply(): Promise<any> {
|
||||
return readFile(this.path, 'utf8').then(content => {
|
||||
let prefix = content.substring(0, this.pos);
|
||||
let suffix = content.substring(this.pos);
|
||||
return writeFile(this.path, `${prefix}${this.toAdd}${suffix}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will remove text from the source code.
|
||||
*/
|
||||
export class RemoveChange implements Change {
|
||||
|
||||
order: number;
|
||||
description: string;
|
||||
|
||||
constructor(public path: string, private pos: number, private toRemove: string) {
|
||||
if (pos < 0) {
|
||||
throw new Error('Negative positions are invalid');
|
||||
}
|
||||
this.description = `Removed ${toRemove} into position ${pos} of ${path}`;
|
||||
this.order = pos;
|
||||
}
|
||||
|
||||
apply(): Promise<any> {
|
||||
return readFile(this.path, 'utf8').then(content => {
|
||||
let prefix = content.substring(0, this.pos);
|
||||
let suffix = content.substring(this.pos + this.toRemove.length);
|
||||
// TODO: throw error if toRemove doesn't match removed string.
|
||||
return writeFile(this.path, `${prefix}${suffix}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will replace text from the source code.
|
||||
*/
|
||||
export class ReplaceChange implements Change {
|
||||
order: number;
|
||||
description: string;
|
||||
|
||||
constructor(public path: string, private pos: number, private oldText: string,
|
||||
private newText: string) {
|
||||
if (pos < 0) {
|
||||
throw new Error('Negative positions are invalid');
|
||||
}
|
||||
this.description = `Replaced ${oldText} into position ${pos} of ${path} with ${newText}`;
|
||||
this.order = pos;
|
||||
}
|
||||
|
||||
apply(): Promise<any> {
|
||||
return readFile(this.path, 'utf8').then(content => {
|
||||
let prefix = content.substring(0, this.pos);
|
||||
let suffix = content.substring(this.pos + this.oldText.length);
|
||||
// TODO: throw error if oldText doesn't match removed string.
|
||||
return writeFile(this.path, `${prefix}${this.newText}${suffix}`);
|
||||
});
|
||||
}
|
||||
}
|
3
packages/ast-tools/src/index.ts
Normal file
3
packages/ast-tools/src/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './ast-utils';
|
||||
export * from './change';
|
||||
export * from './node';
|
47
packages/ast-tools/src/node.ts
Normal file
47
packages/ast-tools/src/node.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import ts = require('typescript');
|
||||
import {RemoveChange, Change} from './change';
|
||||
|
||||
|
||||
/**
|
||||
* Find all nodes from the AST in the subtree of node of SyntaxKind kind.
|
||||
* @param node
|
||||
* @param kind
|
||||
* @param max The maximum number of items to return.
|
||||
* @return all nodes of kind, or [] if none is found
|
||||
*/
|
||||
export function findNodes(node: ts.Node, kind: ts.SyntaxKind, max: number = Infinity): ts.Node[] {
|
||||
if (!node || max == 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let arr: ts.Node[] = [];
|
||||
if (node.kind === kind) {
|
||||
arr.push(node);
|
||||
max--;
|
||||
}
|
||||
if (max > 0) {
|
||||
for (const child of node.getChildren()) {
|
||||
findNodes(child, kind, max).forEach(node => {
|
||||
if (max > 0) {
|
||||
arr.push(node);
|
||||
}
|
||||
max--;
|
||||
});
|
||||
|
||||
if (max <= 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
|
||||
export function removeAstNode(node: ts.Node): Change {
|
||||
const source = node.getSourceFile();
|
||||
return new RemoveChange(
|
||||
source.path,
|
||||
node.getStart(source),
|
||||
node.getFullText(source)
|
||||
);
|
||||
}
|
@ -1,14 +1,14 @@
|
||||
import * as mockFs from 'mock-fs';
|
||||
import * as fs from 'fs';
|
||||
import { expect } from 'chai';
|
||||
import * as nru from '../../addon/ng2/utilities/route-utils';
|
||||
import * as ts from 'typescript';
|
||||
import * as nru from './route-utils';
|
||||
import * as path from 'path';
|
||||
import { InsertChange, RemoveChange } from '../../addon/ng2/utilities/change';
|
||||
import * as Promise from 'ember-cli/lib/ext/promise';
|
||||
import { InsertChange, RemoveChange } from './change';
|
||||
import denodeify = require('denodeify');
|
||||
import * as _ from 'lodash';
|
||||
import {it} from './spec-utils';
|
||||
|
||||
const readFile = (denodeify(fs.readFile) as (...args: any[]) => Promise<string>);
|
||||
|
||||
const readFile = Promise.denodeify(fs.readFile);
|
||||
|
||||
describe('route utils', () => {
|
||||
describe('insertImport', () => {
|
||||
@ -30,76 +30,76 @@ describe('route utils', () => {
|
||||
let content = `'use strict'\n import {foo} from 'bar'\n import * as fz from 'fizz';`;
|
||||
let editedFile = new InsertChange(sourceFile, 0, content);
|
||||
return editedFile.apply()
|
||||
.then(() => nru.insertImport(sourceFile, 'Router', '@angular/router').apply())
|
||||
.then(() => readFile(sourceFile, 'utf8'))
|
||||
.then(newContent => {
|
||||
expect(newContent).to.equal(content + `\nimport { Router } from '@angular/router';`);
|
||||
});
|
||||
.then(() => nru.insertImport(sourceFile, 'Router', '@angular/router').apply())
|
||||
.then(() => readFile(sourceFile, 'utf8'))
|
||||
.then(newContent => {
|
||||
expect(newContent).toEqual(content + `\nimport { Router } from '@angular/router';`);
|
||||
});
|
||||
});
|
||||
it('does not insert if present', () => {
|
||||
let content = `'use strict'\n import {Router} from '@angular/router'`;
|
||||
let editedFile = new InsertChange(sourceFile, 0, content);
|
||||
return editedFile.apply()
|
||||
.then(() => nru.insertImport(sourceFile, 'Router', '@angular/router'))
|
||||
.then(() => readFile(sourceFile, 'utf8'))
|
||||
.then(newContent => {
|
||||
expect(newContent).to.equal(content);
|
||||
});
|
||||
.then(() => nru.insertImport(sourceFile, 'Router', '@angular/router'))
|
||||
.then(() => readFile(sourceFile, 'utf8'))
|
||||
.then(newContent => {
|
||||
expect(newContent).toEqual(content);
|
||||
});
|
||||
});
|
||||
it('inserts into existing import clause if import file is already cited', () => {
|
||||
let content = `'use strict'\n import { foo, bar } from 'fizz'`;
|
||||
let editedFile = new InsertChange(sourceFile, 0, content);
|
||||
return editedFile.apply()
|
||||
.then(() => nru.insertImport(sourceFile, 'baz', 'fizz').apply())
|
||||
.then(() => readFile(sourceFile, 'utf8'))
|
||||
.then(newContent => {
|
||||
expect(newContent).to.equal(`'use strict'\n import { foo, bar, baz } from 'fizz'`);
|
||||
});
|
||||
.then(() => nru.insertImport(sourceFile, 'baz', 'fizz').apply())
|
||||
.then(() => readFile(sourceFile, 'utf8'))
|
||||
.then(newContent => {
|
||||
expect(newContent).toEqual(`'use strict'\n import { foo, bar, baz } from 'fizz'`);
|
||||
});
|
||||
});
|
||||
it('understands * imports', () => {
|
||||
let content = `\nimport * as myTest from 'tests' \n`;
|
||||
let editedFile = new InsertChange(sourceFile, 0, content);
|
||||
return editedFile.apply()
|
||||
.then(() => nru.insertImport(sourceFile, 'Test', 'tests'))
|
||||
.then(() => readFile(sourceFile, 'utf8'))
|
||||
.then(newContent => {
|
||||
expect(newContent).to.equal(content);
|
||||
});
|
||||
.then(() => nru.insertImport(sourceFile, 'Test', 'tests'))
|
||||
.then(() => readFile(sourceFile, 'utf8'))
|
||||
.then(newContent => {
|
||||
expect(newContent).toEqual(content);
|
||||
});
|
||||
});
|
||||
it('inserts after use-strict', () => {
|
||||
let content = `'use strict';\n hello`;
|
||||
let editedFile = new InsertChange(sourceFile, 0, content);
|
||||
return editedFile.apply()
|
||||
.then(() => nru.insertImport(sourceFile, 'Router', '@angular/router').apply())
|
||||
.then(() => readFile(sourceFile, 'utf8'))
|
||||
.then(newContent => {
|
||||
expect(newContent).to.equal(
|
||||
`'use strict';\nimport { Router } from '@angular/router';\n hello`);
|
||||
});
|
||||
.then(() => nru.insertImport(sourceFile, 'Router', '@angular/router').apply())
|
||||
.then(() => readFile(sourceFile, 'utf8'))
|
||||
.then(newContent => {
|
||||
expect(newContent).toEqual(
|
||||
`'use strict';\nimport { Router } from '@angular/router';\n hello`);
|
||||
});
|
||||
});
|
||||
it('inserts inserts at beginning of file if no imports exist', () => {
|
||||
return nru.insertImport(sourceFile, 'Router', '@angular/router').apply()
|
||||
.then(() => readFile(sourceFile, 'utf8'))
|
||||
.then(newContent => {
|
||||
expect(newContent).to.equal(`import { Router } from '@angular/router';\n`);
|
||||
});
|
||||
.then(() => readFile(sourceFile, 'utf8'))
|
||||
.then(newContent => {
|
||||
expect(newContent).toEqual(`import { Router } from '@angular/router';\n`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('bootstrapItem', () => {
|
||||
const mainFile = 'tmp/main.ts';
|
||||
const prefix = `import {bootstrap} from '@angular/platform-browser-dynamic'; \n` +
|
||||
`import { AppComponent } from './app/';\n`;
|
||||
`import { AppComponent } from './app/';\n`;
|
||||
const routes = {'provideRouter': ['@angular/router'], 'routes': ['./routes', true]};
|
||||
const toBootstrap = 'provideRouter(routes)';
|
||||
const routerImport = `import routes from './routes';\n` +
|
||||
`import { provideRouter } from '@angular/router'; \n`;
|
||||
`import { provideRouter } from '@angular/router'; \n`;
|
||||
beforeEach(() => {
|
||||
let mockDrive = {
|
||||
'tmp': {
|
||||
'main.ts': `import {bootstrap} from '@angular/platform-browser-dynamic'; \n` +
|
||||
`import { AppComponent } from './app/'; \n` +
|
||||
'bootstrap(AppComponent);'
|
||||
`import { AppComponent } from './app/'; \n` +
|
||||
'bootstrap(AppComponent);'
|
||||
}
|
||||
};
|
||||
mockFs(mockDrive);
|
||||
@ -111,107 +111,107 @@ describe('route utils', () => {
|
||||
|
||||
it('adds a provideRouter import if not there already', () => {
|
||||
return nru.applyChanges(nru.bootstrapItem(mainFile, routes, toBootstrap))
|
||||
.then(() => readFile(mainFile, 'utf8'))
|
||||
.then(content => {
|
||||
expect(content).to.equal(prefix + routerImport +
|
||||
'bootstrap(AppComponent, [ provideRouter(routes) ]);');
|
||||
});
|
||||
.then(() => readFile(mainFile, 'utf8'))
|
||||
.then(content => {
|
||||
expect(content).toEqual(prefix + routerImport +
|
||||
'bootstrap(AppComponent, [ provideRouter(routes) ]);');
|
||||
});
|
||||
});
|
||||
it('does not add a provideRouter import if it exits already', () => {
|
||||
xit('does not add a provideRouter import if it exits already', () => {
|
||||
return nru.insertImport(mainFile, 'provideRouter', '@angular/router').apply()
|
||||
.then(() => nru.applyChanges(nru.bootstrapItem(mainFile, routes, toBootstrap)));
|
||||
.then(() => readFile(mainFile, 'utf8'))
|
||||
.then(content => {
|
||||
expect(content).to.equal(
|
||||
`import routes from './routes';
|
||||
.then(() => nru.applyChanges(nru.bootstrapItem(mainFile, routes, toBootstrap)))
|
||||
.then(() => readFile(mainFile, 'utf8'))
|
||||
.then(content => {
|
||||
expect(content).toEqual(
|
||||
`import routes from './routes';
|
||||
import { provideRouter } from '@angular/router';
|
||||
bootstrap(AppComponent, [ provideRouter(routes) ]);`);
|
||||
});
|
||||
});
|
||||
});
|
||||
it('does not duplicate import to route.ts ', () => {
|
||||
xit('does not duplicate import to route.ts ', () => {
|
||||
let editedFile = new InsertChange(mainFile, 100, `\nimport routes from './routes';`);
|
||||
return editedFile
|
||||
.apply()
|
||||
.then(() => nru.applyChanges(nru.bootstrapItem(mainFile, routes, toBootstrap)))
|
||||
.then(() => readFile(mainFile, 'utf8'))
|
||||
.then(content => {
|
||||
expect(content).to.equal(prefix + routerImport +
|
||||
'bootstrap(AppComponent, [ provideRouter(routes) ]);');
|
||||
});
|
||||
.apply()
|
||||
.then(() => nru.applyChanges(nru.bootstrapItem(mainFile, routes, toBootstrap)))
|
||||
.then(() => readFile(mainFile, 'utf8'))
|
||||
.then(content => {
|
||||
expect(content).toEqual(prefix + routerImport +
|
||||
'bootstrap(AppComponent, [ provideRouter(routes) ]);');
|
||||
});
|
||||
});
|
||||
it('adds provideRouter to bootstrap if absent and no providers array', () => {
|
||||
return nru.applyChanges(nru.bootstrapItem(mainFile, routes, toBootstrap))
|
||||
.then(() => readFile(mainFile, 'utf8'))
|
||||
.then(content => {
|
||||
expect(content).to.equal(prefix + routerImport +
|
||||
'bootstrap(AppComponent, [ provideRouter(routes) ]);');
|
||||
});
|
||||
.then(() => readFile(mainFile, 'utf8'))
|
||||
.then(content => {
|
||||
expect(content).toEqual(prefix + routerImport +
|
||||
'bootstrap(AppComponent, [ provideRouter(routes) ]);');
|
||||
});
|
||||
});
|
||||
it('adds provideRouter to bootstrap if absent and empty providers array', () => {
|
||||
let editFile = new InsertChange(mainFile, 124, ', []');
|
||||
return editFile.apply()
|
||||
.then(() => nru.applyChanges(nru.bootstrapItem(mainFile, routes, toBootstrap)))
|
||||
.then(() => readFile(mainFile, 'utf8'))
|
||||
.then(content => {
|
||||
expect(content).to.equal(prefix + routerImport +
|
||||
'bootstrap(AppComponent, [provideRouter(routes)]);');
|
||||
});
|
||||
.then(() => nru.applyChanges(nru.bootstrapItem(mainFile, routes, toBootstrap)))
|
||||
.then(() => readFile(mainFile, 'utf8'))
|
||||
.then(content => {
|
||||
expect(content).toEqual(prefix + routerImport +
|
||||
'bootstrap(AppComponent, [provideRouter(routes)]);');
|
||||
});
|
||||
});
|
||||
it('adds provideRouter to bootstrap if absent and non-empty providers array', () => {
|
||||
let editedFile = new InsertChange(mainFile, 124, ', [ HTTP_PROVIDERS ]');
|
||||
return editedFile.apply()
|
||||
.then(() => nru.applyChanges(nru.bootstrapItem(mainFile, routes, toBootstrap)))
|
||||
.then(() => readFile(mainFile, 'utf8'))
|
||||
.then(content => {
|
||||
expect(content).to.equal(prefix + routerImport +
|
||||
'bootstrap(AppComponent, [ HTTP_PROVIDERS, provideRouter(routes) ]);');
|
||||
});
|
||||
.then(() => nru.applyChanges(nru.bootstrapItem(mainFile, routes, toBootstrap)))
|
||||
.then(() => readFile(mainFile, 'utf8'))
|
||||
.then(content => {
|
||||
expect(content).toEqual(prefix + routerImport +
|
||||
'bootstrap(AppComponent, [ HTTP_PROVIDERS, provideRouter(routes) ]);');
|
||||
});
|
||||
});
|
||||
it('does not add provideRouter to bootstrap if present', () => {
|
||||
let editedFile = new InsertChange(mainFile, 124, ', [ HTTP_PROVIDERS, provideRouter(routes) ]');
|
||||
return editedFile.apply()
|
||||
.then(() => nru.applyChanges(nru.bootstrapItem(mainFile, routes, toBootstrap)))
|
||||
.then(() => readFile(mainFile, 'utf8'))
|
||||
.then(content => {
|
||||
expect(content).to.equal(prefix + routerImport +
|
||||
'bootstrap(AppComponent, [ HTTP_PROVIDERS, provideRouter(routes) ]);');
|
||||
});
|
||||
.then(() => nru.applyChanges(nru.bootstrapItem(mainFile, routes, toBootstrap)))
|
||||
.then(() => readFile(mainFile, 'utf8'))
|
||||
.then(content => {
|
||||
expect(content).toEqual(prefix + routerImport +
|
||||
'bootstrap(AppComponent, [ HTTP_PROVIDERS, provideRouter(routes) ]);');
|
||||
});
|
||||
});
|
||||
it('inserts into the correct array', () => {
|
||||
let editedFile = new InsertChange(mainFile, 124, ', [ HTTP_PROVIDERS, {provide: [BAR]}]');
|
||||
return editedFile.apply()
|
||||
.then(() => nru.applyChanges(nru.bootstrapItem(mainFile, routes, toBootstrap)))
|
||||
.then(() => readFile(mainFile, 'utf8'))
|
||||
.then(content => {
|
||||
expect(content).to.equal(prefix + routerImport +
|
||||
'bootstrap(AppComponent, [ HTTP_PROVIDERS, {provide: [BAR]}, provideRouter(routes)]);');
|
||||
});
|
||||
.then(() => nru.applyChanges(nru.bootstrapItem(mainFile, routes, toBootstrap)))
|
||||
.then(() => readFile(mainFile, 'utf8'))
|
||||
.then(content => {
|
||||
expect(content).toEqual(prefix + routerImport +
|
||||
'bootstrap(AppComponent, [ HTTP_PROVIDERS, {provide: [BAR]}, provideRouter(routes)]);');
|
||||
});
|
||||
});
|
||||
it('throws an error if there is no or multiple bootstrap expressions', () => {
|
||||
let editedFile = new InsertChange(mainFile, 126, '\n bootstrap(moreStuff);');
|
||||
return editedFile.apply()
|
||||
.then(() => nru.bootstrapItem(mainFile, routes, toBootstrap))
|
||||
.catch(e =>
|
||||
expect(e.message).to.equal('Did not bootstrap provideRouter in' +
|
||||
' tmp/main.ts because of multiple or no bootstrap calls')
|
||||
);
|
||||
.then(() => nru.bootstrapItem(mainFile, routes, toBootstrap))
|
||||
.catch(e =>
|
||||
expect(e.message).toEqual('Did not bootstrap provideRouter in' +
|
||||
' tmp/main.ts because of multiple or no bootstrap calls')
|
||||
);
|
||||
});
|
||||
it('configures correctly if bootstrap or provide router is not at top level', () => {
|
||||
let editedFile = new InsertChange(mainFile, 126, '\n if(e){bootstrap, provideRouter});');
|
||||
return editedFile.apply()
|
||||
.then(() => nru.applyChanges(nru.bootstrapItem(mainFile, routes, toBootstrap)))
|
||||
.then(() => readFile(mainFile, 'utf8'))
|
||||
.then(content => {
|
||||
expect(content).to.equal(prefix + routerImport +
|
||||
'bootstrap(AppComponent, [ provideRouter(routes) ]);\n if(e){bootstrap, provideRouter});');
|
||||
});
|
||||
.then(() => nru.applyChanges(nru.bootstrapItem(mainFile, routes, toBootstrap)))
|
||||
.then(() => readFile(mainFile, 'utf8'))
|
||||
.then(content => {
|
||||
expect(content).toEqual(prefix + routerImport +
|
||||
'bootstrap(AppComponent, [ provideRouter(routes) ]);\n if(e){bootstrap, provideRouter});');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('addPathToRoutes', () => {
|
||||
const routesFile = 'src/routes.ts';
|
||||
var options = {dir: 'src/app', appRoot: 'src/app', routesFile: routesFile,
|
||||
component: 'NewRouteComponent', dasherizedName: 'new-route'};
|
||||
component: 'NewRouteComponent', dasherizedName: 'new-route'};
|
||||
const nestedRoutes = `\n { path: 'home', component: HomeComponent,
|
||||
children: [
|
||||
{ path: 'about', component: AboutComponent,
|
||||
@ -236,20 +236,20 @@ describe('route utils', () => {
|
||||
|
||||
it('adds import to new route component if absent', () => {
|
||||
return nru.applyChanges(nru.addPathToRoutes(routesFile, _.merge({route: 'new-route'}, options)))
|
||||
.then(() => readFile(routesFile, 'utf8'))
|
||||
.then(content => {
|
||||
expect(content).to.equal(
|
||||
`import { NewRouteComponent } from './app/new-route/new-route.component';
|
||||
.then(() => readFile(routesFile, 'utf8'))
|
||||
.then(content => {
|
||||
expect(content).toEqual(
|
||||
`import { NewRouteComponent } from './app/new-route/new-route.component';
|
||||
export default [\n { path: 'new-route', component: NewRouteComponent }\n];`);
|
||||
});
|
||||
});
|
||||
});
|
||||
it('throws error if multiple export defaults exist', () => {
|
||||
let editedFile = new InsertChange(routesFile, 20, 'export default {}');
|
||||
return editedFile.apply().then(() => {
|
||||
return nru.addPathToRoutes(routesFile, _.merge({route: 'new-route'}, options));
|
||||
}).catch(e => {
|
||||
expect(e.message).to.equal('Did not insert path in routes.ts because '
|
||||
+ `there were multiple or no 'export default' statements`);
|
||||
expect(e.message).toEqual('Did not insert path in routes.ts because '
|
||||
+ `there were multiple or no 'export default' statements`);
|
||||
});
|
||||
});
|
||||
it('throws error if no export defaults exists', () => {
|
||||
@ -257,28 +257,28 @@ export default [\n { path: 'new-route', component: NewRouteComponent }\n];`);
|
||||
return editedFile.apply().then(() => {
|
||||
return nru.addPathToRoutes(routesFile, _.merge({route: 'new-route'}, options));
|
||||
}).catch(e => {
|
||||
expect(e.message).to.equal('Did not insert path in routes.ts because '
|
||||
+ `there were multiple or no 'export default' statements`);
|
||||
expect(e.message).toEqual('Did not insert path in routes.ts because '
|
||||
+ `there were multiple or no 'export default' statements`);
|
||||
});
|
||||
});
|
||||
it('treats positional params correctly', () => {
|
||||
let editedFile = new InsertChange(routesFile, 16,
|
||||
`\n { path: 'home', component: HomeComponent }\n`);
|
||||
`\n { path: 'home', component: HomeComponent }\n`);
|
||||
return editedFile.apply().then(() => {
|
||||
options.dasherizedName = 'about';
|
||||
options.component = 'AboutComponent';
|
||||
return nru.applyChanges(
|
||||
nru.addPathToRoutes(routesFile, _.merge({route: 'home/about/:id'}, options))); })
|
||||
.then(() => readFile(routesFile, 'utf8'))
|
||||
.then(content => {
|
||||
expect(content).to.equal(
|
||||
`import { AboutComponent } from './app/home/about/about.component';` +
|
||||
`\nexport default [\n` +
|
||||
` { path: 'home', component: HomeComponent,\n` +
|
||||
` children: [\n` +
|
||||
` { path: 'about/:id', component: AboutComponent } ` +
|
||||
`\n ]\n }\n];`);
|
||||
});
|
||||
.then(() => readFile(routesFile, 'utf8'))
|
||||
.then(content => {
|
||||
expect(content).toEqual(
|
||||
`import { AboutComponent } from './app/home/about/about.component';` +
|
||||
`\nexport default [\n` +
|
||||
` { path: 'home', component: HomeComponent,\n` +
|
||||
` children: [\n` +
|
||||
` { path: 'about/:id', component: AboutComponent } ` +
|
||||
`\n ]\n }\n];`);
|
||||
});
|
||||
});
|
||||
it('inserts under parent, mid', () => {
|
||||
let editedFile = new InsertChange(routesFile, 16, nestedRoutes);
|
||||
@ -287,9 +287,9 @@ export default [\n { path: 'new-route', component: NewRouteComponent }\n];`);
|
||||
options.component = 'DetailsComponent';
|
||||
return nru.applyChanges(
|
||||
nru.addPathToRoutes(routesFile, _.merge({route: 'home/about/details'}, options))); })
|
||||
.then(() => readFile(routesFile, 'utf8'))
|
||||
.then(content => {
|
||||
let expected = `import { DetailsComponent } from './app/home/about/details/details.component';
|
||||
.then(() => readFile(routesFile, 'utf8'))
|
||||
.then(content => {
|
||||
let expected = `import { DetailsComponent } from './app/home/about/details/details.component';
|
||||
export default [
|
||||
{ path: 'home', component: HomeComponent,
|
||||
children: [
|
||||
@ -301,8 +301,8 @@ export default [
|
||||
}
|
||||
]
|
||||
}\n];`;
|
||||
expect(content).to.equal(expected);
|
||||
});
|
||||
expect(content).toEqual(expected);
|
||||
});
|
||||
});
|
||||
it('inserts under parent, deep', () => {
|
||||
let editedFile = new InsertChange(routesFile, 16, nestedRoutes);
|
||||
@ -311,9 +311,9 @@ export default [
|
||||
options.component = 'SectionsComponent';
|
||||
return nru.applyChanges(
|
||||
nru.addPathToRoutes(routesFile, _.merge({route: 'home/about/more/sections'}, options))); })
|
||||
.then(() => readFile(routesFile, 'utf8'))
|
||||
.then(content => {
|
||||
let expected = `import { SectionsComponent } from './app/home/about/more/sections/sections.component';
|
||||
.then(() => readFile(routesFile, 'utf8'))
|
||||
.then(content => {
|
||||
let expected = `import { SectionsComponent } from './app/home/about/more/sections/sections.component';
|
||||
export default [
|
||||
{ path: 'home', component: HomeComponent,
|
||||
children: [
|
||||
@ -329,8 +329,8 @@ export default [
|
||||
]
|
||||
}
|
||||
];`;
|
||||
expect(content).to.equal(expected);
|
||||
});
|
||||
expect(content).toEqual(expected);
|
||||
});
|
||||
});
|
||||
it('works well with multiple routes in a level', () => {
|
||||
let paths = `\n { path: 'main', component: MainComponent }
|
||||
@ -345,9 +345,9 @@ export default [
|
||||
options.component = 'AboutComponent_1';
|
||||
return nru.applyChanges(
|
||||
nru.addPathToRoutes(routesFile, _.merge({route: 'home/about/:id'}, options))); })
|
||||
.then(() => readFile(routesFile, 'utf8'))
|
||||
.then(content => {
|
||||
expect(content).to.equal(`import { AboutComponent_1 } from './app/home/about/about.component';
|
||||
.then(() => readFile(routesFile, 'utf8'))
|
||||
.then(content => {
|
||||
expect(content).toEqual(`import { AboutComponent_1 } from './app/home/about/about.component';
|
||||
export default [
|
||||
{ path: 'main', component: MainComponent }
|
||||
{ path: 'home', component: HomeComponent,
|
||||
@ -357,17 +357,17 @@ export default [
|
||||
]
|
||||
}
|
||||
];`
|
||||
);
|
||||
});
|
||||
);
|
||||
});
|
||||
});
|
||||
it('throws error if repeating child, shallow', () => {
|
||||
let editedFile = new InsertChange(routesFile, 16, nestedRoutes);
|
||||
return editedFile.apply().then(() => {
|
||||
options.dasherizedName = 'home';
|
||||
options.component = 'HomeComponent';
|
||||
return nru.addPathToRoutes(routesFile, _.merge({route: '/home'}, options));
|
||||
return nru.addPathToRoutes(routesFile, _.merge({route: '/home'}, options));
|
||||
}).catch(e => {
|
||||
expect(e.message).to.equal('Route was not added since it is a duplicate');
|
||||
expect(e.message).toEqual('Route was not added since it is a duplicate');
|
||||
});
|
||||
});
|
||||
it('throws error if repeating child, mid', () => {
|
||||
@ -375,9 +375,9 @@ export default [
|
||||
return editedFile.apply().then(() => {
|
||||
options.dasherizedName = 'about';
|
||||
options.component = 'AboutComponent';
|
||||
return nru.addPathToRoutes(routesFile, _.merge({route: 'home/about/'}, options));
|
||||
return nru.addPathToRoutes(routesFile, _.merge({route: 'home/about/'}, options));
|
||||
}).catch(e => {
|
||||
expect(e.message).to.equal('Route was not added since it is a duplicate');
|
||||
expect(e.message).toEqual('Route was not added since it is a duplicate');
|
||||
});
|
||||
});
|
||||
it('throws error if repeating child, deep', () => {
|
||||
@ -385,9 +385,9 @@ export default [
|
||||
return editedFile.apply().then(() => {
|
||||
options.dasherizedName = 'more';
|
||||
options.component = 'MoreComponent';
|
||||
return nru.addPathToRoutes(routesFile, _.merge({route: 'home/about/more'}, options));
|
||||
return nru.addPathToRoutes(routesFile, _.merge({route: 'home/about/more'}, options));
|
||||
}).catch(e => {
|
||||
expect(e.message).to.equal('Route was not added since it is a duplicate');
|
||||
expect(e.message).toEqual('Route was not added since it is a duplicate');
|
||||
});
|
||||
});
|
||||
it('does not report false repeat', () => {
|
||||
@ -397,9 +397,9 @@ export default [
|
||||
options.component = 'MoreComponent';
|
||||
return nru.applyChanges(nru.addPathToRoutes(routesFile, _.merge({route: 'more'}, options)));
|
||||
})
|
||||
.then(() => readFile(routesFile, 'utf8'))
|
||||
.then(content => {
|
||||
let expected = `import { MoreComponent } from './app/more/more.component';
|
||||
.then(() => readFile(routesFile, 'utf8'))
|
||||
.then(content => {
|
||||
let expected = `import { MoreComponent } from './app/more/more.component';
|
||||
export default [
|
||||
{ path: 'more', component: MoreComponent },
|
||||
{ path: 'home', component: HomeComponent,
|
||||
@ -411,8 +411,8 @@ export default [
|
||||
}
|
||||
]
|
||||
}\n];`;
|
||||
expect(content).to.equal(expected);
|
||||
});
|
||||
expect(content).toEqual(expected);
|
||||
});
|
||||
});
|
||||
it('does not report false repeat: multiple paths on a level', () => {
|
||||
|
||||
@ -431,8 +431,9 @@ export default [
|
||||
options.dasherizedName = 'trap-queen';
|
||||
options.component = 'TrapQueenComponent';
|
||||
return nru.applyChanges(
|
||||
nru.addPathToRoutes(routesFile, _.merge({route: 'home/trap-queen'}, options))); })
|
||||
.then(() => readFile(routesFile, 'utf8')
|
||||
nru.addPathToRoutes(routesFile, _.merge({route: 'home/trap-queen'}, options)));
|
||||
})
|
||||
.then(() => readFile(routesFile, 'utf8'))
|
||||
.then(content => {
|
||||
let expected = `import { TrapQueenComponent } from './app/home/trap-queen/trap-queen.component';
|
||||
export default [
|
||||
@ -446,25 +447,25 @@ export default [
|
||||
}
|
||||
]
|
||||
},\n { path: 'trap-queen', component: TrapQueenComponent}\n];`;
|
||||
expect(content).to.equal(expected);
|
||||
expect(content).toEqual(expected);
|
||||
});
|
||||
});
|
||||
it('resolves imports correctly', () => {
|
||||
let editedFile = new InsertChange(routesFile, 16,
|
||||
`\n { path: 'home', component: HomeComponent }\n`);
|
||||
`\n { path: 'home', component: HomeComponent }\n`);
|
||||
return editedFile.apply().then(() => {
|
||||
let editedFile = new InsertChange(routesFile, 0,
|
||||
`import { HomeComponent } from './app/home/home.component';\n`);
|
||||
`import { HomeComponent } from './app/home/home.component';\n`);
|
||||
return editedFile.apply();
|
||||
})
|
||||
.then(() => {
|
||||
options.dasherizedName = 'home';
|
||||
options.component = 'HomeComponent';
|
||||
return nru.applyChanges(
|
||||
nru.addPathToRoutes(routesFile, _.merge({route: 'home/home'}, options))); })
|
||||
.then(() => readFile(routesFile, 'utf8'))
|
||||
.then(content => {
|
||||
let expected = `import { HomeComponent } from './app/home/home.component';
|
||||
.then(() => {
|
||||
options.dasherizedName = 'home';
|
||||
options.component = 'HomeComponent';
|
||||
return nru.applyChanges(
|
||||
nru.addPathToRoutes(routesFile, _.merge({route: 'home/home'}, options))); })
|
||||
.then(() => readFile(routesFile, 'utf8'))
|
||||
.then(content => {
|
||||
let expected = `import { HomeComponent } from './app/home/home.component';
|
||||
import { HomeComponent as HomeComponent_1 } from './app/home/home/home.component';
|
||||
export default [
|
||||
{ path: 'home', component: HomeComponent,
|
||||
@ -473,12 +474,12 @@ export default [
|
||||
]
|
||||
}
|
||||
];`;
|
||||
expect(content).to.equal(expected);
|
||||
});
|
||||
expect(content).toEqual(expected);
|
||||
});
|
||||
});
|
||||
it('throws error if components collide and there is repitition', () => {
|
||||
let editedFile = new InsertChange(routesFile, 16,
|
||||
`\n { path: 'about', component: AboutComponent,
|
||||
`\n { path: 'about', component: AboutComponent,
|
||||
children: [
|
||||
{ path: 'details/:id', component: DetailsComponent_1 },
|
||||
{ path: 'details', component: DetailsComponent }
|
||||
@ -486,7 +487,7 @@ export default [
|
||||
}`);
|
||||
return editedFile.apply().then(() => {
|
||||
let editedFile = new InsertChange(routesFile, 0,
|
||||
`import { AboutComponent } from './app/about/about.component';
|
||||
`import { AboutComponent } from './app/about/about.component';
|
||||
import { DetailsComponent } from './app/about/details/details.component';
|
||||
import { DetailsComponent as DetailsComponent_1 } from './app/about/description/details.component;\n`);
|
||||
return editedFile.apply();
|
||||
@ -494,7 +495,7 @@ import { DetailsComponent as DetailsComponent_1 } from './app/about/description/
|
||||
options.dasherizedName = 'details';
|
||||
options.component = 'DetailsComponent';
|
||||
expect(() => nru.addPathToRoutes(routesFile, _.merge({route: 'about/details'}, options)))
|
||||
.to.throw(Error);
|
||||
.toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
@ -503,11 +504,11 @@ import { DetailsComponent as DetailsComponent_1 } from './app/about/description/
|
||||
let editedFile = new InsertChange(routesFile, 16, path);
|
||||
return editedFile.apply().then(() => {
|
||||
let toInsert = {'home': ['canActivate', '[ MyGuard ]'] };
|
||||
return nru.applyChanges(nru.addItemsToRouteProperties(routesFile, toInsert)); })
|
||||
return nru.applyChanges(nru.addItemsToRouteProperties(routesFile, toInsert));
|
||||
})
|
||||
.then(() => readFile(routesFile, 'utf8'))
|
||||
.then(content => {
|
||||
expect(content).to.equal(
|
||||
`export default [
|
||||
expect(content).toEqual(`export default [
|
||||
{ path: 'home', component: HomeComponent, canActivate: [ MyGuard ] }
|
||||
];`
|
||||
);
|
||||
@ -521,16 +522,16 @@ import { DetailsComponent as DetailsComponent_1 } from './app/about/description/
|
||||
options.component = 'MoreComponent';
|
||||
return nru.applyChanges(
|
||||
nru.addPathToRoutes(routesFile, _.merge({route: 'home/more'}, options))); })
|
||||
.then(() => {
|
||||
return nru.applyChanges(nru.addItemsToRouteProperties(routesFile,
|
||||
{ 'home/more': ['canDeactivate', '[ MyGuard ]'] })); })
|
||||
.then(() => {
|
||||
return nru.applyChanges(nru.addItemsToRouteProperties(
|
||||
routesFile, { 'home/more': ['useAsDefault', 'true'] })); })
|
||||
.then(() => readFile(routesFile, 'utf8'))
|
||||
.then(content => {
|
||||
expect(content).to.equal(
|
||||
`import { MoreComponent } from './app/home/more/more.component';
|
||||
.then(() => {
|
||||
return nru.applyChanges(nru.addItemsToRouteProperties(routesFile,
|
||||
{ 'home/more': ['canDeactivate', '[ MyGuard ]'] })); })
|
||||
.then(() => {
|
||||
return nru.applyChanges(nru.addItemsToRouteProperties(
|
||||
routesFile, { 'home/more': ['useAsDefault', 'true'] })); })
|
||||
.then(() => readFile(routesFile, 'utf8'))
|
||||
.then(content => {
|
||||
expect(content).toEqual(
|
||||
`import { MoreComponent } from './app/home/more/more.component';
|
||||
export default [
|
||||
{ path: 'home', component: HomeComponent,
|
||||
children: [
|
||||
@ -538,8 +539,8 @@ export default [
|
||||
]
|
||||
}
|
||||
];`
|
||||
);
|
||||
});
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -565,37 +566,37 @@ export default [
|
||||
|
||||
it('accepts component name without \'component\' suffix: resolveComponentPath', () => {
|
||||
let fileName = nru.resolveComponentPath(projectRoot, 'src/app', 'about');
|
||||
expect(fileName).to.equal(componentFile);
|
||||
expect(fileName).toEqual(componentFile);
|
||||
});
|
||||
it('accepts component name with \'component\' suffix: resolveComponentPath', () => {
|
||||
let fileName = nru.resolveComponentPath(projectRoot, 'src/app', 'about.component');
|
||||
expect(fileName).to.equal(componentFile);
|
||||
expect(fileName).toEqual(componentFile);
|
||||
});
|
||||
it('accepts path absolute from project root: resolveComponentPath', () => {
|
||||
let fileName = nru.resolveComponentPath(projectRoot, '', `${path.sep}about`);
|
||||
expect(fileName).to.equal(componentFile);
|
||||
expect(fileName).toEqual(componentFile);
|
||||
});
|
||||
it('accept component with directory name: resolveComponentPath', () => {
|
||||
let fileName = nru.resolveComponentPath(projectRoot, 'src/app', 'about/about.component');
|
||||
expect(fileName).to.equal(componentFile);
|
||||
expect(fileName).toEqual(componentFile);
|
||||
});
|
||||
|
||||
it('finds component name: confirmComponentExport', () => {
|
||||
let exportExists = nru.confirmComponentExport(componentFile, 'AboutComponent');
|
||||
expect(exportExists).to.be.truthy;
|
||||
expect(exportExists).toBeTruthy();
|
||||
});
|
||||
it('finds component in the presence of decorators: confirmComponentExport', () => {
|
||||
let editedFile = new InsertChange(componentFile, 0, '@Component{}\n');
|
||||
return editedFile.apply().then(() => {
|
||||
let exportExists = nru.confirmComponentExport(componentFile, 'AboutComponent');
|
||||
expect(exportExists).to.be.truthy;
|
||||
expect(exportExists).toBeTruthy();
|
||||
});
|
||||
});
|
||||
it('report absence of component name: confirmComponentExport', () => {
|
||||
let editedFile = new RemoveChange(componentFile, 21, 'onent');
|
||||
return editedFile.apply().then(() => {
|
||||
let exportExists = nru.confirmComponentExport(componentFile, 'AboutComponent');
|
||||
expect(exportExists).to.not.be.truthy;
|
||||
expect(exportExists).not.toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
529
packages/ast-tools/src/route-utils.ts
Normal file
529
packages/ast-tools/src/route-utils.ts
Normal file
@ -0,0 +1,529 @@
|
||||
import * as ts from 'typescript';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import {Change, InsertChange, NoopChange} from './change';
|
||||
import {findNodes} from './node';
|
||||
import {insertAfterLastOccurrence} from './ast-utils';
|
||||
|
||||
/**
|
||||
* Adds imports to mainFile and adds toBootstrap to the array of providers
|
||||
* in bootstrap, if not present
|
||||
* @param mainFile main.ts
|
||||
* @param imports Object { importedClass: ['path/to/import/from', defaultStyleImport?] }
|
||||
* @param toBootstrap
|
||||
*/
|
||||
export function bootstrapItem(mainFile: string, imports: {[key: string]: (string | boolean)[]}, toBootstrap: string ) {
|
||||
let changes = Object.keys(imports).map(importedClass => {
|
||||
var defaultStyleImport = imports[importedClass].length === 2 && !!imports[importedClass][1];
|
||||
return insertImport(
|
||||
mainFile,
|
||||
importedClass,
|
||||
imports[importedClass][0].toString(),
|
||||
defaultStyleImport
|
||||
);
|
||||
});
|
||||
let rootNode = getRootNode(mainFile);
|
||||
// get ExpressionStatements from the top level syntaxList of the sourceFile
|
||||
let bootstrapNodes = rootNode.getChildAt(0).getChildren().filter(node => {
|
||||
// get bootstrap expressions
|
||||
return node.kind === ts.SyntaxKind.ExpressionStatement &&
|
||||
(node.getChildAt(0).getChildAt(0) as ts.Identifier).text.toLowerCase() === 'bootstrap';
|
||||
});
|
||||
if (bootstrapNodes.length !== 1) {
|
||||
throw new Error(`Did not bootstrap provideRouter in ${mainFile}` +
|
||||
' because of multiple or no bootstrap calls');
|
||||
}
|
||||
let bootstrapNode = bootstrapNodes[0].getChildAt(0);
|
||||
let isBootstraped = findNodes(bootstrapNode, ts.SyntaxKind.SyntaxList) // get bootstrapped items
|
||||
.reduce((a, b) => a.concat(b.getChildren().map(n => n.getText())), [])
|
||||
.filter(n => n !== ',')
|
||||
.indexOf(toBootstrap) !== -1;
|
||||
if (isBootstraped) {
|
||||
return changes;
|
||||
}
|
||||
// if bracket exitst already, add configuration template,
|
||||
// otherwise, insert into bootstrap parens
|
||||
var fallBackPos: number, configurePathsTemplate: string, separator: string;
|
||||
var syntaxListNodes: any;
|
||||
let bootstrapProviders = bootstrapNode.getChildAt(2).getChildAt(2); // array of providers
|
||||
|
||||
if ( bootstrapProviders ) {
|
||||
syntaxListNodes = bootstrapProviders.getChildAt(1).getChildren();
|
||||
fallBackPos = bootstrapProviders.getChildAt(2).pos; // closeBracketLiteral
|
||||
separator = syntaxListNodes.length === 0 ? '' : ', ';
|
||||
configurePathsTemplate = `${separator}${toBootstrap}`;
|
||||
} else {
|
||||
fallBackPos = bootstrapNode.getChildAt(3).pos; // closeParenLiteral
|
||||
syntaxListNodes = bootstrapNode.getChildAt(2).getChildren();
|
||||
configurePathsTemplate = `, [ ${toBootstrap} ]`;
|
||||
}
|
||||
|
||||
changes.push(insertAfterLastOccurrence(syntaxListNodes, configurePathsTemplate,
|
||||
mainFile, fallBackPos));
|
||||
return changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Import `import { symbolName } from fileName` if the import doesn't exit
|
||||
* already. Assumes fileToEdit can be resolved and accessed.
|
||||
* @param fileToEdit (file we want to add import to)
|
||||
* @param symbolName (item to import)
|
||||
* @param fileName (path to the file)
|
||||
* @param isDefault (if true, import follows style for importing default exports)
|
||||
* @return Change
|
||||
*/
|
||||
|
||||
export function insertImport(fileToEdit: string, symbolName: string,
|
||||
fileName: string, isDefault = false): Change {
|
||||
let rootNode = getRootNode(fileToEdit);
|
||||
let allImports = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
|
||||
|
||||
// get nodes that map to import statements from the file fileName
|
||||
let relevantImports = allImports.filter(node => {
|
||||
// StringLiteral of the ImportDeclaration is the import file (fileName in this case).
|
||||
let importFiles = node.getChildren().filter(child => child.kind === ts.SyntaxKind.StringLiteral)
|
||||
.map(n => (<ts.StringLiteralTypeNode>n).text);
|
||||
return importFiles.filter(file => file === fileName).length === 1;
|
||||
});
|
||||
|
||||
if (relevantImports.length > 0) {
|
||||
|
||||
var importsAsterisk = false;
|
||||
// imports from import file
|
||||
let imports: ts.Node[] = [];
|
||||
relevantImports.forEach(n => {
|
||||
Array.prototype.push.apply(imports, findNodes(n, ts.SyntaxKind.Identifier));
|
||||
if (findNodes(n, ts.SyntaxKind.AsteriskToken).length > 0) {
|
||||
importsAsterisk = true;
|
||||
}
|
||||
});
|
||||
|
||||
// if imports * from fileName, don't add symbolName
|
||||
if (importsAsterisk) {
|
||||
return;
|
||||
}
|
||||
|
||||
let importTextNodes = imports.filter(n => (<ts.Identifier>n).text === symbolName);
|
||||
|
||||
// insert import if it's not there
|
||||
if (importTextNodes.length === 0) {
|
||||
let fallbackPos = findNodes(relevantImports[0], ts.SyntaxKind.CloseBraceToken)[0].pos ||
|
||||
findNodes(relevantImports[0], ts.SyntaxKind.FromKeyword)[0].pos;
|
||||
return insertAfterLastOccurrence(imports, `, ${symbolName}`, fileToEdit, fallbackPos);
|
||||
}
|
||||
return new NoopChange();
|
||||
}
|
||||
|
||||
// no such import declaration exists
|
||||
let useStrict = findNodes(rootNode, ts.SyntaxKind.StringLiteral)
|
||||
.filter((n: ts.StringLiteral) => n.text === 'use strict');
|
||||
let fallbackPos = 0;
|
||||
if (useStrict.length > 0) {
|
||||
fallbackPos = useStrict[0].end;
|
||||
}
|
||||
let open = isDefault ? '' : '{ ';
|
||||
let close = isDefault ? '' : ' }';
|
||||
// if there are no imports or 'use strict' statement, insert import at beginning of file
|
||||
let insertAtBeginning = allImports.length === 0 && useStrict.length === 0;
|
||||
let separator = insertAtBeginning ? '' : ';\n';
|
||||
let toInsert = `${separator}import ${open}${symbolName}${close}` +
|
||||
` from '${fileName}'${insertAtBeginning ? ';\n' : ''}`;
|
||||
return insertAfterLastOccurrence(allImports, toInsert, fileToEdit, fallbackPos, ts.SyntaxKind.StringLiteral);
|
||||
};
|
||||
|
||||
/**
|
||||
* Inserts a path to the new route into src/routes.ts if it doesn't exist
|
||||
* @param routesFile
|
||||
* @param pathOptions
|
||||
* @return Change[]
|
||||
* @throws Error if routesFile has multiple export default or none.
|
||||
*/
|
||||
export function addPathToRoutes(routesFile: string, pathOptions: any): Change[] {
|
||||
let route = pathOptions.route.split('/')
|
||||
.filter((n: string) => n !== '').join('/'); // change say `/about/:id/` to `about/:id`
|
||||
let isDefault = pathOptions.isDefault ? ', useAsDefault: true' : '';
|
||||
let outlet = pathOptions.outlet ? `, outlet: '${pathOptions.outlet}'` : '';
|
||||
|
||||
// create route path and resolve component import
|
||||
let positionalRoutes = /\/:[^/]*/g;
|
||||
let routePath = route.replace(positionalRoutes, '');
|
||||
routePath = `./app/${routePath}/${pathOptions.dasherizedName}.component`;
|
||||
let originalComponent = pathOptions.component;
|
||||
pathOptions.component = resolveImportName(pathOptions.component, routePath, pathOptions.routesFile);
|
||||
|
||||
var content = `{ path: '${route}', component: ${pathOptions.component}${isDefault}${outlet} }`;
|
||||
let rootNode = getRootNode(routesFile);
|
||||
let routesNode = rootNode.getChildAt(0).getChildren().filter(n => {
|
||||
// get export statement
|
||||
return n.kind === ts.SyntaxKind.ExportAssignment &&
|
||||
n.getFullText().indexOf('export default') !== -1;
|
||||
});
|
||||
if (routesNode.length !== 1) {
|
||||
throw new Error('Did not insert path in routes.ts because ' +
|
||||
`there were multiple or no 'export default' statements`);
|
||||
}
|
||||
var pos = routesNode[0].getChildAt(2).getChildAt(0).end; // openBracketLiteral
|
||||
// all routes in export route array
|
||||
let routesArray = routesNode[0].getChildAt(2).getChildAt(1)
|
||||
.getChildren()
|
||||
.filter(n => n.kind === ts.SyntaxKind.ObjectLiteralExpression);
|
||||
|
||||
if (pathExists(routesArray, route, pathOptions.component)) {
|
||||
// don't duplicate routes
|
||||
throw new Error('Route was not added since it is a duplicate');
|
||||
}
|
||||
var isChild = false;
|
||||
// get parent to insert under
|
||||
let parent: ts.Node;
|
||||
if (pathOptions.parent) {
|
||||
// append '_' to route to find the actual parent (not parent of the parent)
|
||||
parent = getParent(routesArray, `${pathOptions.parent}/_`);
|
||||
if (!parent) {
|
||||
throw new Error(`You specified parent '${pathOptions.parent}'' which was not found in routes.ts`);
|
||||
}
|
||||
if (route.indexOf(pathOptions.parent) === 0) {
|
||||
route = route.substring(pathOptions.parent.length);
|
||||
}
|
||||
} else {
|
||||
parent = getParent(routesArray, route);
|
||||
}
|
||||
|
||||
if (parent) {
|
||||
let childrenInfo = addChildPath(parent, pathOptions, route);
|
||||
if (!childrenInfo) {
|
||||
// path exists already
|
||||
throw new Error('Route was not added since it is a duplicate');
|
||||
}
|
||||
content = childrenInfo.newContent;
|
||||
pos = childrenInfo.pos;
|
||||
isChild = true;
|
||||
}
|
||||
|
||||
let isFirstElement = routesArray.length === 0;
|
||||
if (!isChild) {
|
||||
let separator = isFirstElement ? '\n' : ',';
|
||||
content = `\n ${content}${separator}`;
|
||||
}
|
||||
let changes: Change[] = [new InsertChange(routesFile, pos, content)];
|
||||
let component = originalComponent === pathOptions.component ? originalComponent :
|
||||
`${originalComponent} as ${pathOptions.component}`;
|
||||
routePath = routePath.replace(/\\/, '/'); // correction in windows
|
||||
changes.push(insertImport(routesFile, component, routePath));
|
||||
return changes;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add more properties to the route object in routes.ts
|
||||
* @param routesFile routes.ts
|
||||
* @param routes Object {route: [key, value]}
|
||||
*/
|
||||
export function addItemsToRouteProperties(routesFile: string, routes: {[key: string]: string[]}) {
|
||||
let rootNode = getRootNode(routesFile);
|
||||
let routesNode = rootNode.getChildAt(0).getChildren().filter(n => {
|
||||
// get export statement
|
||||
return n.kind === ts.SyntaxKind.ExportAssignment &&
|
||||
n.getFullText().indexOf('export default') !== -1;
|
||||
});
|
||||
if (routesNode.length !== 1) {
|
||||
throw new Error('Did not insert path in routes.ts because ' +
|
||||
`there were multiple or no 'export default' statements`);
|
||||
}
|
||||
let routesArray = routesNode[0].getChildAt(2).getChildAt(1)
|
||||
.getChildren()
|
||||
.filter(n => n.kind === ts.SyntaxKind.ObjectLiteralExpression);
|
||||
let changes: Change[] = Object.keys(routes).reduce((result, route) => {
|
||||
// let route = routes[guardName][0];
|
||||
let itemKey = routes[route][0];
|
||||
let itemValue = routes[route][1];
|
||||
let currRouteNode = getParent(routesArray, `${route}/_`);
|
||||
if (!currRouteNode) {
|
||||
throw new Error(`Could not find '${route}' in routes.ts`);
|
||||
}
|
||||
let fallBackPos = findNodes(currRouteNode, ts.SyntaxKind.CloseBraceToken).pop().pos;
|
||||
let pathPropertiesNodes = currRouteNode.getChildAt(1).getChildren()
|
||||
.filter(n => n.kind === ts.SyntaxKind.PropertyAssignment);
|
||||
return result.concat([insertAfterLastOccurrence(pathPropertiesNodes,
|
||||
`, ${itemKey}: ${itemValue}`, routesFile, fallBackPos)]);
|
||||
}, []);
|
||||
return changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that a component file exports a class of the component
|
||||
* @param file
|
||||
* @param componentName
|
||||
* @return whether file exports componentName
|
||||
*/
|
||||
export function confirmComponentExport (file: string, componentName: string): boolean {
|
||||
const rootNode = getRootNode(file);
|
||||
let exportNodes = rootNode.getChildAt(0).getChildren().filter(n => {
|
||||
return n.kind === ts.SyntaxKind.ClassDeclaration &&
|
||||
(n.getChildren().filter((p: ts.Identifier) => p.text === componentName).length !== 0);
|
||||
});
|
||||
return exportNodes.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures there is no collision between import names. If a collision occurs, resolve by adding
|
||||
* underscore number to the name
|
||||
* @param importName
|
||||
* @param importPath path to import component from
|
||||
* @param fileName (file to add import to)
|
||||
* @return resolved importName
|
||||
*/
|
||||
function resolveImportName (importName: string, importPath: string, fileName: string): string {
|
||||
const rootNode = getRootNode(fileName);
|
||||
// get all the import names
|
||||
let importNodes = rootNode.getChildAt(0).getChildren()
|
||||
.filter(n => n.kind === ts.SyntaxKind.ImportDeclaration);
|
||||
// check if imported file is same as current one before updating component name
|
||||
let importNames = importNodes
|
||||
.reduce((a, b) => {
|
||||
let importFrom = findNodes(b, ts.SyntaxKind.StringLiteral); // there's only one
|
||||
if ((importFrom.pop() as ts.StringLiteral).text !== importPath) {
|
||||
// importing from different file, add to imported components to inspect
|
||||
// if only one identifier { FooComponent }, if two { FooComponent as FooComponent_1 }
|
||||
// choose last element of identifier array in both cases
|
||||
return a.concat([findNodes(b, ts.SyntaxKind.Identifier).pop()]);
|
||||
}
|
||||
return a;
|
||||
}, [])
|
||||
.map(n => n.text);
|
||||
|
||||
const index = importNames.indexOf(importName);
|
||||
if (index === -1) {
|
||||
return importName;
|
||||
}
|
||||
const baseName = importNames[index].split('_')[0];
|
||||
var newName = baseName;
|
||||
var resolutionNumber = 1;
|
||||
while (importNames.indexOf(newName) !== -1) {
|
||||
newName = `${baseName}_${resolutionNumber}`;
|
||||
resolutionNumber++;
|
||||
}
|
||||
return newName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a path to a component file. If the path begins with path.sep, it is treated to be
|
||||
* absolute from the app/ directory. Otherwise, it is relative to currDir
|
||||
* @param projectRoot
|
||||
* @param currentDir
|
||||
* @param filePath componentName or path to componentName
|
||||
* @return component file name
|
||||
* @throw Error if component file referenced by path is not found
|
||||
*/
|
||||
export function resolveComponentPath(projectRoot: string, currentDir: string, filePath: string) {
|
||||
|
||||
let parsedPath = path.parse(filePath);
|
||||
let componentName = parsedPath.base.split('.')[0];
|
||||
let componentDir = path.parse(parsedPath.dir).base;
|
||||
|
||||
// correction for a case where path is /**/componentName/componentName(.component.ts)
|
||||
if ( componentName === componentDir) {
|
||||
filePath = parsedPath.dir;
|
||||
}
|
||||
if (parsedPath.dir === '') {
|
||||
// only component file name is given
|
||||
filePath = componentName;
|
||||
}
|
||||
var directory = filePath[0] === path.sep ?
|
||||
path.resolve(path.join(projectRoot, 'src', 'app', filePath)) : path.resolve(currentDir, filePath);
|
||||
|
||||
if (!fs.existsSync(directory)) {
|
||||
throw new Error(`path '${filePath}' must be relative to current directory` +
|
||||
` or absolute from project root`);
|
||||
}
|
||||
if (directory.indexOf('src' + path.sep + 'app') === -1) {
|
||||
throw new Error('Route must be within app');
|
||||
}
|
||||
let componentFile = path.join(directory, `${componentName}.component.ts`);
|
||||
if (!fs.existsSync(componentFile)) {
|
||||
throw new Error(`could not find component file referenced by ${filePath}`);
|
||||
}
|
||||
return componentFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort changes in decreasing order and apply them.
|
||||
* @param changes
|
||||
* @return Promise
|
||||
*/
|
||||
export function applyChanges(changes: Change[]): Promise<void> {
|
||||
return changes
|
||||
.filter(change => !!change)
|
||||
.sort((curr, next) => next.order - curr.order)
|
||||
.reduce((newChange, change) => newChange.then(() => change.apply()), Promise.resolve());
|
||||
}
|
||||
/**
|
||||
* Helper for addPathToRoutes. Adds child array to the appropriate position in the routes.ts file
|
||||
* @return Object (pos, newContent)
|
||||
*/
|
||||
function addChildPath (parentObject: ts.Node, pathOptions: any, route: string) {
|
||||
if (!parentObject) {
|
||||
return;
|
||||
}
|
||||
var pos: number;
|
||||
var newContent: string;
|
||||
|
||||
// get object with 'children' property
|
||||
let childrenNode = parentObject.getChildAt(1).getChildren()
|
||||
.filter(n => n.kind === ts.SyntaxKind.PropertyAssignment
|
||||
&& ((n as ts.PropertyAssignment).name as ts.Identifier).text === 'children');
|
||||
// find number of spaces to pad nested paths
|
||||
let nestingLevel = 1; // for indenting route object in the `children` array
|
||||
let n = parentObject;
|
||||
while (n.parent) {
|
||||
if (n.kind === ts.SyntaxKind.ObjectLiteralExpression
|
||||
|| n.kind === ts.SyntaxKind.ArrayLiteralExpression) {
|
||||
nestingLevel ++;
|
||||
}
|
||||
n = n.parent;
|
||||
}
|
||||
|
||||
// strip parent route
|
||||
let parentRoute = (parentObject.getChildAt(1).getChildAt(0).getChildAt(2) as ts.Identifier).text;
|
||||
let childRoute = route.substring(route.indexOf(parentRoute) + parentRoute.length + 1);
|
||||
|
||||
let isDefault = pathOptions.isDefault ? ', useAsDefault: true' : '';
|
||||
let outlet = pathOptions.outlet ? `, outlet: '${pathOptions.outlet}'` : '';
|
||||
let content = `{ path: '${childRoute}', component: ${pathOptions.component}` +
|
||||
`${isDefault}${outlet} }`;
|
||||
let spaces = Array(2 * nestingLevel + 1).join(' ');
|
||||
|
||||
if (childrenNode.length !== 0) {
|
||||
// add to beginning of children array
|
||||
pos = childrenNode[0].getChildAt(2).getChildAt(1).pos; // open bracket
|
||||
newContent = `\n${spaces}${content}, `;
|
||||
} else {
|
||||
// no children array, add one
|
||||
pos = parentObject.getChildAt(2).pos; // close brace
|
||||
newContent = `,\n${spaces.substring(2)}children: [\n${spaces}${content} ` +
|
||||
`\n${spaces.substring(2)}]\n${spaces.substring(5)}`;
|
||||
}
|
||||
return {newContent: newContent, pos: pos};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for addPathToRoutes.
|
||||
* @return parentNode which contains the children array to add a new path to or
|
||||
* undefined if none or the entire route was matched.
|
||||
*/
|
||||
function getParent(routesArray: ts.Node[], route: string, parent?: ts.Node): ts.Node {
|
||||
if (routesArray.length === 0 && !parent) {
|
||||
return; // no children array and no parent found
|
||||
}
|
||||
if (route.length === 0) {
|
||||
return; // route has been completely matched
|
||||
}
|
||||
var splitRoute = route.split('/');
|
||||
// don't treat positional parameters separately
|
||||
if (splitRoute.length > 1 && splitRoute[1].indexOf(':') !== -1) {
|
||||
let actualRoute = splitRoute.shift();
|
||||
splitRoute[0] = `${actualRoute}/${splitRoute[0]}`;
|
||||
}
|
||||
let potentialParents: ts.Node[] = routesArray // route nodes with same path as current route
|
||||
.filter(n => getValueForKey(n, 'path') === splitRoute[0]);
|
||||
if (potentialParents.length !== 0) {
|
||||
splitRoute.shift(); // matched current parent, move on
|
||||
route = splitRoute.join('/');
|
||||
}
|
||||
// get all children paths
|
||||
let newRouteArray = getChildrenArray(routesArray);
|
||||
if (route && parent && potentialParents.length === 0) {
|
||||
return parent; // final route is not matched. assign parent from here
|
||||
}
|
||||
parent = potentialParents.sort((a, b) => a.pos - b.pos).shift();
|
||||
return getParent(newRouteArray, route, parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for addPathToRoutes.
|
||||
* @return whether path with same route and component exists
|
||||
*/
|
||||
function pathExists(routesArray: ts.Node[], route: string, component: string, fullRoute?: string): boolean {
|
||||
if (routesArray.length === 0) {
|
||||
return false;
|
||||
}
|
||||
fullRoute = fullRoute ? fullRoute : route;
|
||||
var sameRoute = false;
|
||||
var splitRoute = route.split('/');
|
||||
// don't treat positional parameters separately
|
||||
if (splitRoute.length > 1 && splitRoute[1].indexOf(':') !== -1) {
|
||||
let actualRoute = splitRoute.shift();
|
||||
splitRoute[0] = `${actualRoute}/${splitRoute[0]}`;
|
||||
}
|
||||
let repeatedRoutes: ts.Node[] = routesArray.filter(n => {
|
||||
let currentRoute = getValueForKey(n, 'path');
|
||||
let sameComponent = getValueForKey(n, 'component') === component;
|
||||
|
||||
sameRoute = currentRoute === splitRoute[0];
|
||||
// Confirm that it's parents are the same
|
||||
if (sameRoute && sameComponent) {
|
||||
var path = currentRoute;
|
||||
let objExp = n.parent;
|
||||
while (objExp) {
|
||||
if (objExp.kind === ts.SyntaxKind.ObjectLiteralExpression) {
|
||||
let currentParentPath = getValueForKey(objExp, 'path');
|
||||
path = currentParentPath ? `${currentParentPath}/${path}` : path;
|
||||
}
|
||||
objExp = objExp.parent;
|
||||
}
|
||||
return path === fullRoute;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (sameRoute) {
|
||||
splitRoute.shift(); // matched current parent, move on
|
||||
route = splitRoute.join('/');
|
||||
}
|
||||
if (repeatedRoutes.length !== 0) {
|
||||
return true; // new path will be repeating if inserted. report that path already exists
|
||||
}
|
||||
|
||||
// all children paths
|
||||
let newRouteArray = getChildrenArray(routesArray);
|
||||
return pathExists(newRouteArray, route, component, fullRoute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for getParent and pathExists
|
||||
* @return array with all nodes holding children array under routes
|
||||
* in routesArray
|
||||
*/
|
||||
function getChildrenArray(routesArray: ts.Node[]): ts.Node[] {
|
||||
return routesArray.reduce((allRoutes, currRoute) => allRoutes.concat(
|
||||
currRoute.getChildAt(1).getChildren()
|
||||
.filter(n => n.kind === ts.SyntaxKind.PropertyAssignment
|
||||
&& ((n as ts.PropertyAssignment).name as ts.Identifier).text === 'children')
|
||||
.map(n => n.getChildAt(2).getChildAt(1)) // syntaxList containing chilren paths
|
||||
.reduce((childrenArray, currChild) => childrenArray.concat(currChild.getChildren()
|
||||
.filter(p => p.kind === ts.SyntaxKind.ObjectLiteralExpression)
|
||||
), [])
|
||||
), []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get the path text or component
|
||||
* @param objectLiteralNode
|
||||
* @param key 'path' or 'component'
|
||||
*/
|
||||
function getValueForKey(objectLiteralNode: ts.Node, key: string) {
|
||||
let currentNode = key === 'component' ? objectLiteralNode.getChildAt(1).getChildAt(2) :
|
||||
objectLiteralNode.getChildAt(1).getChildAt(0);
|
||||
return currentNode
|
||||
&& currentNode.getChildAt(0)
|
||||
&& (currentNode.getChildAt(0) as ts.Identifier).text === key
|
||||
&& currentNode.getChildAt(2)
|
||||
&& (currentNode.getChildAt(2) as ts.Identifier).text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get AST from file
|
||||
* @param file
|
||||
*/
|
||||
function getRootNode(file: string) {
|
||||
return ts.createSourceFile(file, fs.readFileSync(file).toString(), ts.ScriptTarget.ES6, true);
|
||||
}
|
26
packages/ast-tools/src/spec-utils.ts
Normal file
26
packages/ast-tools/src/spec-utils.ts
Normal file
@ -0,0 +1,26 @@
|
||||
// This file exports a version of the Jasmine `it` that understands promises.
|
||||
// To use this, simply `import {it} from './spec-utils`.
|
||||
// TODO(hansl): move this to its own Jasmine-TypeScript package.
|
||||
|
||||
function async(fn: () => PromiseLike<any> | void) {
|
||||
return (done: DoneFn) => {
|
||||
let result: PromiseLike<any> | void = null;
|
||||
|
||||
try {
|
||||
result = fn();
|
||||
|
||||
if (result && 'then' in result) {
|
||||
(result as Promise<any>).then(done, done.fail);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
} catch (err) {
|
||||
done.fail(err);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function it(description: string, fn: () => PromiseLike<any> | void) {
|
||||
return (global as any)['it'](description, async(fn));
|
||||
}
|
24
packages/ast-tools/tsconfig.json
Normal file
24
packages/ast-tools/tsconfig.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"experimentalDecorators": true,
|
||||
"mapRoot": "",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"noEmitOnError": true,
|
||||
"noImplicitAny": true,
|
||||
"outDir": "../../dist/ast-tools",
|
||||
"rootDir": ".",
|
||||
"sourceMap": true,
|
||||
"sourceRoot": "/",
|
||||
"target": "es5",
|
||||
"lib": ["es6"],
|
||||
"typeRoots": [
|
||||
"../../node_modules/@types"
|
||||
],
|
||||
"types": [
|
||||
"jasmine",
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
20
scripts/run-packages-spec.js
Normal file
20
scripts/run-packages-spec.js
Normal file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
require('../lib/bootstrap-local');
|
||||
|
||||
const path = require('path');
|
||||
const Jasmine = require('jasmine');
|
||||
const JasmineSpecReporter = require('jasmine-spec-reporter');
|
||||
|
||||
const projectBaseDir = path.join(__dirname, '../packages');
|
||||
|
||||
// Create a Jasmine runner and configure it.
|
||||
const jasmine = new Jasmine({ projectBaseDir: projectBaseDir });
|
||||
jasmine.loadConfig({
|
||||
spec_dir: projectBaseDir
|
||||
});
|
||||
jasmine.addReporter(new JasmineSpecReporter());
|
||||
|
||||
// Run the tests.
|
||||
jasmine.execute(['**/*.spec.ts']);
|
@ -1,34 +1,7 @@
|
||||
/* eslint-disable no-console */
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const ts = require('typescript');
|
||||
const old = require.extensions['.ts'];
|
||||
|
||||
require.extensions['.ts'] = function(m, filename) {
|
||||
if (!filename.match(/angular-cli/) && filename.match(/node_modules/)) {
|
||||
if (old) {
|
||||
return old(m, filename);
|
||||
}
|
||||
return m._compile(fs.readFileSync(filename), filename);
|
||||
}
|
||||
|
||||
const source = fs.readFileSync(filename).toString();
|
||||
|
||||
try {
|
||||
const result = ts.transpile(source, {
|
||||
target: ts.ScriptTarget.ES5,
|
||||
module: ts.ModuleKind.CommonJs
|
||||
});
|
||||
|
||||
// Send it to node to execute.
|
||||
return m._compile(result, filename);
|
||||
} catch (err) {
|
||||
console.error('Error while running script "' + filename + '":');
|
||||
console.error(err.stack);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
require('../lib/bootstrap-local');
|
||||
|
||||
var Mocha = require('mocha');
|
||||
var glob = require('glob');
|
||||
|
Loading…
x
Reference in New Issue
Block a user