Skip to content

Commit

Permalink
feat: ngmodules and insert components based on the AST
Browse files Browse the repository at this point in the history
  • Loading branch information
hansl committed Aug 9, 2016
1 parent 4fd8e9c commit 4e16ef5
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 94 deletions.
25 changes: 10 additions & 15 deletions addon/ng2/blueprints/component/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,27 +121,22 @@ module.exports = {
}

var returns = [];
var modulePath = path.resolve(process.env.PWD, this.dynamicPath.appRoot, 'app.module.ts');
var classifiedName =
stringUtils.classify(`${options.entity.name}-${options.originBlueprintName}`);
var importPath = `'./${options.entity.name}/` +
stringUtils.dasherize(`${options.entity.name}.component';`);
var modulePath = path.join(process.cwd(), this.dynamicPath.appRoot, 'app.module.ts');
const className = stringUtils.classify(`${options.entity.name}Component`);
const fileName = stringUtils.dasherize(`${options.entity.name}.component`);
const componentDir = path.relative(this.dynamicPath.appRoot, this.generatePath);
var importPath = `./${componentDir}/${fileName}`;

if (!options.flat) {
returns.push(function() {
return addBarrelRegistration(this, this.generatePath)
});
returns.push(addBarrelRegistration(this, componentDir));
} else {
returns.push(function() {
return addBarrelRegistration(
this,
this.generatePath,
options.entity.name + '.component')
});
returns.push(addBarrelRegistration(this, componentDir, fileName));
}

if (!options['skip-import']) {
returns.push(astUtils.importComponent(modulePath, classifiedName, importPath));
returns.push(
astUtils.addComponentToModule(modulePath, className, importPath)
.then(change => change.apply()));
}

return Promise.all(returns);
Expand Down
24 changes: 10 additions & 14 deletions addon/ng2/blueprints/directive/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,26 +55,22 @@ module.exports = {

afterInstall: function(options) {
var returns = [];
var modulePath = path.resolve(process.env.PWD, this.dynamicPath.appRoot, 'app.module.ts');
var classifiedName =
stringUtils.classify(options.entity.name);
var importPath = '\'./' + stringUtils.dasherize(`${options.entity.name}.directive';`);
var modulePath = path.resolve(process.env.PWD, this.dynamicPath.sourceDir, 'app.module.ts');
var classifiedName = stringUtils.classify(options.entity.name);
const fileName = stringUtils.dasherize(`${options.entity.name}.directive`);

if (!options.flat) {
returns.push(function() {
return addBarrelRegistration(this, this.generatePath)
});
returns.push(addBarrelRegistration(this, this.generatePath));
} else {
returns.push(function() {
return addBarrelRegistration(
this,
this.generatePath,
options.entity.name + '.directive')
});
returns.push(addBarrelRegistration(this, this.generatePath, fileName)
);
}

if (!options['skip-import']) {
returns.push(astUtils.importComponent(modulePath, classifiedName, importPath));
returns.push(
astUtils.addComponentToModule(modulePath, classifiedName, fileName)
.then(change => change.apply())
);
}

return Promise.all(returns);
Expand Down
8 changes: 3 additions & 5 deletions addon/ng2/blueprints/ng2/files/__path__/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import { BrowserModule } from '@angular/platform-browser';
import { NgModule, ApplicationRef } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';<% if (isMobile) { %>
import { AppShellModule } from '../app-shell-module';<% } %>
import { AppComponent } from './app.component';

@NgModule({
declarations: [
Expand All @@ -12,12 +11,11 @@ import { AppShellModule } from '../app-shell-module';<% } %>
imports: [
BrowserModule,
CommonModule,
FormsModule<% if (isMobile) { %>,
AppShellModule<% } %>
FormsModule
],
entryComponents: [AppComponent],
bootstrap: [AppComponent]
})
export class AppModule {

}
}
16 changes: 8 additions & 8 deletions addon/ng2/blueprints/ng2/files/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
},
"private": true,
"dependencies": {
"@angular/common": "github:angular/common-builds",
"@angular/compiler": "github:angular/compiler-builds",
"@angular/core": "github:angular/core-builds",
"@angular/forms": "github:angular/forms-builds",
"@angular/http": "github:angular/http-builds",
"@angular/platform-browser": "github:angular/platform-browser-builds",
"@angular/platform-browser-dynamic": "github:angular/platform-browser-dynamic-builds",
"@angular/router": "github:angular/router-builds",
"@angular/common": "2.0.0-rc.5",
"@angular/compiler": "2.0.0-rc.5",
"@angular/core": "2.0.0-rc.5",
"@angular/forms": "0.3.0",
"@angular/http": "2.0.0-rc.5",
"@angular/platform-browser": "2.0.0-rc.5",
"@angular/platform-browser-dynamic": "2.0.0-rc.5",
"@angular/router": "3.0.0-rc.1",
"core-js": "^2.4.0",
"reflect-metadata": "0.1.3",
"rxjs": "5.0.0-beta.6",
Expand Down
1 change: 1 addition & 0 deletions addon/ng2/commands/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const VersionCommand = Command.extend({
}],

run: function (options) {
console.log(123);
var versions = process.versions;
var pkg = require(path.resolve(__dirname, '..', '..', '..', 'package.json'));

Expand Down
189 changes: 139 additions & 50 deletions addon/ng2/utilities/ast-utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
import * as ts from 'typescript';
import * as fs from 'fs';
import { InsertChange } from './change';
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/toPromise';


/**
* Get TS source file based on path.
Expand All @@ -12,6 +29,32 @@ export function getSource(filePath: string): ts.SourceFile {
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
Expand All @@ -30,25 +73,6 @@ export function findNodes(node: ts.Node, kind: ts.SyntaxKind): ts.Node[] {
foundNodes.concat(findNodes(child, kind)), arr);
}

/**
* Find all nodes from the AST in the subtree based on text.
* @param node
* @param text
* @return all nodes of text, or [] if none is found
*/
export function findNodesByText(node: ts.Node, text: string): ts.Node[] {
if (!node) {
return [];
}
let arr: ts.Node[] = [];
if (node.getText() === text) {
arr.push(node);
}

return node.getChildren().reduce((foundNodes, child) => {
return foundNodes.concat(findNodesByText(child, text));
}, arr);
}

/**
* Helper for sorting nodes.
Expand All @@ -58,6 +82,7 @@ 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
Expand All @@ -84,39 +109,103 @@ export function insertAfterLastOccurrence(nodes: ts.Node[], toInsert: string,
return new InsertChange(file, lastItemPosition, toInsert);
}


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]);
}


/**
* Custom function to insert component (component, pipe, directive)
* into NgModule declarations. It also imports the component.
* @param modulePath
* @param classifiedName
* @param importPath
* @return Promise
*/
export function importComponent(modulePath: string, classifiedName: string,
importPath: string): Promise<void> {
let source: ts.SourceFile = this.getSource(modulePath);

let importNode: ts.Node =
this.findNodesByText(source, 'import').pop();
let iPos: ts.LineAndCharacter =
source.getLineAndCharacterOfPosition(importNode.getEnd());
let iLine: number = iPos.line + 1;
let iStart: number = source.getPositionOfLineAndCharacter(iLine, 0);
let iStr: string = `import { ${classifiedName} } from ${importPath}\n`;
let changeImport: InsertChange = new InsertChange(modulePath, iStart, iStr);

return changeImport.apply().then(() => {
source = this.getSource(modulePath);
let declarationsNode: ts.Node =
this.findNodesByText(source, 'declarations').shift();
let dPos: ts.LineAndCharacter =
source.getLineAndCharacterOfPosition(declarationsNode.getEnd());
let dStart: number =
source.getPositionOfLineAndCharacter(dPos.line + 1, -1);
let dStr: string = `\n ${classifiedName},`;
let changeDeclarations: InsertChange = new InsertChange(modulePath, dStart, dStr);

return changeDeclarations.apply();
});
export function addComponentToModule(modulePath: string, classifiedName: string,
importPath: string): Promise<Change> {
const source: ts.SourceFile = getSource(modulePath);

// Find the declaration.
return getDecoratorMetadata(source, 'NgModule', '@angular/core')
// Get all the children property assignment of object literals.
.mergeMap((node: ts.ObjectLiteralExpression) => Observable.of(
...node.properties
.filter(prop => prop.kind == ts.SyntaxKind.PropertyAssignment)
))
// Filter out every fields that's not "declarations". Also handles string literals
// (but not expression).
.filter(prop => {
switch (prop.name.kind) {
case ts.SyntaxKind.Identifier:
return prop.name.getText(source) == 'declarations';
case ts.SyntaxKind.StringLiteral:
return prop.name.text == 'declarations';
}
return false;
})
// Get the last node of the array literal.
.mergeMap(prop => {
const assignment = <ts.PropertyAssignment>prop;

// If it's not an array, nothing we can do really.
if (assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) {
return Observable.empty();
}
return Observable.of(...(<ts.ArrayLiteralExpression>assignment.initializer).elements);
})
.toPromise()
.then((node: ts.Expression) => {
if (!node) {
console.log('No app module found. Please add your new class to your component.');
return;
}

// Get the indentation of the last element, if any.
const text = node.getFullText(source);

let toInsert;
if (text.startsWith('\n')) {
toInsert = `,${text.match(/^\n(\r?)\s+/)[0]}${classifiedName}`;
} else {
toInsert = `,${classifiedName}`;
}

const insert = new InsertChange(modulePath, node.getEnd(), toInsert);
const importInsert: Change = insertImport(modulePath, classifiedName, importPath);
return new MultiChange([insert, importInsert]);
});
}
}

Loading

0 comments on commit 4e16ef5

Please sign in to comment.