From 4e16ef57c3bcd0771363bde28d88b9d9681b132d Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Tue, 9 Aug 2016 15:23:51 -0700 Subject: [PATCH] feat: ngmodules and insert components based on the AST --- addon/ng2/blueprints/component/index.js | 25 +-- addon/ng2/blueprints/directive/index.js | 24 +-- .../ng2/files/__path__/app/app.module.ts | 8 +- addon/ng2/blueprints/ng2/files/package.json | 16 +- addon/ng2/commands/version.ts | 1 + addon/ng2/utilities/ast-utils.ts | 189 +++++++++++++----- addon/ng2/utilities/change.ts | 52 +++++ addon/ng2/utilities/dynamic-path-parser.js | 6 +- package.json | 2 + 9 files changed, 229 insertions(+), 94 deletions(-) diff --git a/addon/ng2/blueprints/component/index.js b/addon/ng2/blueprints/component/index.js index 1de14e4afdac..b2ba3be270e4 100644 --- a/addon/ng2/blueprints/component/index.js +++ b/addon/ng2/blueprints/component/index.js @@ -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); diff --git a/addon/ng2/blueprints/directive/index.js b/addon/ng2/blueprints/directive/index.js index bd22a272689f..c3cfc070aac7 100644 --- a/addon/ng2/blueprints/directive/index.js +++ b/addon/ng2/blueprints/directive/index.js @@ -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); diff --git a/addon/ng2/blueprints/ng2/files/__path__/app/app.module.ts b/addon/ng2/blueprints/ng2/files/__path__/app/app.module.ts index 93334a5d006e..e6327c0dbbd7 100644 --- a/addon/ng2/blueprints/ng2/files/__path__/app/app.module.ts +++ b/addon/ng2/blueprints/ng2/files/__path__/app/app.module.ts @@ -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: [ @@ -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 { -} \ No newline at end of file +} diff --git a/addon/ng2/blueprints/ng2/files/package.json b/addon/ng2/blueprints/ng2/files/package.json index 5572dda6e93e..a2cdbd04b80e 100644 --- a/addon/ng2/blueprints/ng2/files/package.json +++ b/addon/ng2/blueprints/ng2/files/package.json @@ -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", diff --git a/addon/ng2/commands/version.ts b/addon/ng2/commands/version.ts index 5c34b91af81c..3564426c2076 100644 --- a/addon/ng2/commands/version.ts +++ b/addon/ng2/commands/version.ts @@ -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')); diff --git a/addon/ng2/utilities/ast-utils.ts b/addon/ng2/utilities/ast-utils.ts index f631eefb9653..c38d1ebbf817 100644 --- a/addon/ng2/utilities/ast-utils.ts +++ b/addon/ng2/utilities/ast-utils.ts @@ -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. @@ -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} An observable of all the nodes in the source. + */ +export function getSourceNodes(sourceFile: ts.SourceFile): Observable { + const subject = new ReplaySubject(); + 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 @@ -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. @@ -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 @@ -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 { + const symbols = new Symbols(source); + return getSourceNodes(source) + .filter(node => { + return node.kind == ts.SyntaxKind.Decorator + && (node).expression.kind == ts.SyntaxKind.CallExpression; + }) + .map(node => (node).expression) + .filter(expr => { + if (expr.expression.kind == ts.SyntaxKind.Identifier) { + const id = 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 = 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 = 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 => 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 { - 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 { + 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 = 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(...(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]); + }); + } } diff --git a/addon/ng2/utilities/change.ts b/addon/ng2/utilities/change.ts index 4e6191952875..50ae75b40f6b 100644 --- a/addon/ng2/utilities/change.ts +++ b/addon/ng2/utilities/change.ts @@ -22,6 +22,58 @@ export interface Change { 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) { + 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. */ diff --git a/addon/ng2/utilities/dynamic-path-parser.js b/addon/ng2/utilities/dynamic-path-parser.js index 0313813cafad..a74002125693 100644 --- a/addon/ng2/utilities/dynamic-path-parser.js +++ b/addon/ng2/utilities/dynamic-path-parser.js @@ -4,7 +4,8 @@ var fs = require('fs'); module.exports = function dynamicPathParser(project, entityName) { var projectRoot = project.root; - var appRoot = path.join(project.ngConfig.defaults.sourceDir, 'app'); + var sourceDir = project.ngConfig.defaults.sourceDir; + var appRoot = path.join(sourceDir, 'app'); var cwd = process.env.PWD; var rootPath = path.join(projectRoot, appRoot); @@ -52,7 +53,8 @@ module.exports = function dynamicPathParser(project, entityName) { } parsedPath.dir = parsedPath.dir === path.sep ? '' : parsedPath.dir; - parsedPath.appRoot = appRoot + parsedPath.appRoot = appRoot; + parsedPath.sourceDir = sourceDir; return parsedPath; }; diff --git a/package.json b/package.json index 0e8cc9d9ce9f..f720d364a7c3 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ }, "homepage": "https://github.com/angular/angular-cli", "dependencies": { + "@angular/tsc-wrapped": "^0.2.2", "@types/lodash": "^4.0.25-alpha", "@types/rimraf": "0.0.25-alpha", "@types/webpack": "^1.12.22-alpha", @@ -74,6 +75,7 @@ "remap-istanbul": "^0.6.4", "resolve": "^1.1.7", "rimraf": "^2.5.3", + "rxjs": "^5.0.0-beta.11", "sass-loader": "^3.2.0", "shelljs": "^0.7.0", "silent-error": "^1.0.0",