diff --git a/addon/ng2/blueprints/component/index.js b/addon/ng2/blueprints/component/index.js index 1de14e4afd..7f992c6ebe 100644 --- a/addon/ng2/blueprints/component/index.js +++ b/addon/ng2/blueprints/component/index.js @@ -120,28 +120,23 @@ module.exports = { return; } - 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';`); + const returns = []; + const modulePath = path.join(this.project.root, 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); + const importPath = componentDir ? `./${componentDir}/${fileName}` : `./${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 bd22a27268..fbba059a45 100644 --- a/addon/ng2/blueprints/directive/index.js +++ b/addon/ng2/blueprints/directive/index.js @@ -54,27 +54,27 @@ 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';`); + if (options.dryRun) { + return; + } + + const returns = []; + const modulePath = path.join(this.project.root, this.dynamicPath.appRoot, 'app.module.ts'); + const className = stringUtils.classify(`${options.entity.name}`); + const fileName = stringUtils.dasherize(`${options.entity.name}.directive`); + const componentDir = path.relative(this.dynamicPath.appRoot, this.generatePath); + const importPath = componentDir ? `./${componentDir}/${fileName}` : `./${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 + '.directive') - }); + 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/ng2/files/__path__/app/app.module.ts b/addon/ng2/blueprints/ng2/files/__path__/app/app.module.ts index 93334a5d00..f28035614b 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,12 @@ import { AppShellModule } from '../app-shell-module';<% } %> imports: [ BrowserModule, CommonModule, - FormsModule<% if (isMobile) { %>, - AppShellModule<% } %> + FormsModule ], + providers: [], 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 5572dda6e9..a2cdbd04b8 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/blueprints/pipe/index.js b/addon/ng2/blueprints/pipe/index.js index 202c025bee..b747844910 100644 --- a/addon/ng2/blueprints/pipe/index.js +++ b/addon/ng2/blueprints/pipe/index.js @@ -52,27 +52,27 @@ 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}-${options.originBlueprintName}`); - var importPath = '\'./' + stringUtils.dasherize(`${options.entity.name}.pipe';`); + if (options.dryRun) { + return; + } + + const returns = []; + const modulePath = path.join(this.project.root, this.dynamicPath.appRoot, 'app.module.ts'); + const className = stringUtils.classify(`${options.entity.name}Pipe`); + const fileName = stringUtils.dasherize(`${options.entity.name}.pipe`); + const componentDir = path.relative(this.dynamicPath.appRoot, this.generatePath); + const importPath = componentDir ? `./${componentDir}/${fileName}` : `./${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 + '.pipe') - }); + 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/service/index.js b/addon/ng2/blueprints/service/index.js index 38c87ecf87..6fbe84ad5e 100644 --- a/addon/ng2/blueprints/service/index.js +++ b/addon/ng2/blueprints/service/index.js @@ -3,10 +3,11 @@ var Blueprint = require('ember-cli/lib/models/blueprint'); var dynamicPathParser = require('../../utilities/dynamic-path-parser'); var addBarrelRegistration = require('../../utilities/barrel-management'); var getFiles = Blueprint.prototype.files; +const stringUtils = require('ember-cli-string-utils'); module.exports = { description: '', - + availableOptions: [ { name: 'flat', type: Boolean, default: true } ], @@ -24,10 +25,10 @@ module.exports = { flat: options.flat }; }, - + files: function() { var fileList = getFiles.call(this); - + if (this.options && this.options.flat) { fileList = fileList.filter(p => p.indexOf('index.ts') <= 0); } @@ -48,17 +49,17 @@ module.exports = { } }; }, - + afterInstall: function(options) { + const returns = []; + const fileName = stringUtils.dasherize(`${options.entity.name}.service`); + if (!options.flat) { - return addBarrelRegistration( - this, - this.generatePath); + returns.push(addBarrelRegistration(this, this.generatePath)); } else { - return addBarrelRegistration( - this, - this.generatePath, - options.entity.name + '.service'); + returns.push(addBarrelRegistration(this, this.generatePath, fileName)); } + + return Promise.all(returns); } }; diff --git a/addon/ng2/utilities/ast-utils.ts b/addon/ng2/utilities/ast-utils.ts index f631eefb96..c3c52b4d44 100644 --- a/addon/ng2/utilities/ast-utils.ts +++ b/addon/ng2/utilities/ast-utils.ts @@ -1,6 +1,24 @@ 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/toArray'; +import 'rxjs/add/operator/toPromise'; + /** * Get TS source file based on path. @@ -12,6 +30,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 +74,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 +83,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 +110,163 @@ export function insertAfterLastOccurrence(nodes: ts.Node[], toInsert: string, return new InsertChange(file, lastItemPosition, toInsert); } -/** -* 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); +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; + } - 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(); - }); + 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]); +} + + +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 = 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 = 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 = 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 { + + 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 { + return _addSymbolToNgModuleMetadata(modulePath, 'providers', classifiedName, importPath); } diff --git a/addon/ng2/utilities/change.ts b/addon/ng2/utilities/change.ts index 4e61919528..50ae75b40f 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 0313813caf..a740021256 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 0e8cc9d9ce..f720d364a7 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", diff --git a/tests/acceptance/ast-utils.spec.ts b/tests/acceptance/ast-utils.spec.ts index cfc2cf42f3..e98d929b3d 100644 --- a/tests/acceptance/ast-utils.spec.ts +++ b/tests/acceptance/ast-utils.spec.ts @@ -6,7 +6,8 @@ import { InsertChange, RemoveChange } from '../../addon/ng2/utilities/change'; import * as Promise from 'ember-cli/lib/ext/promise'; import { findNodes, - insertAfterLastOccurrence + insertAfterLastOccurrence, + addComponentToModule } from '../../addon/ng2/utilities/ast-utils'; const readFile = Promise.denodeify(fs.readFile); @@ -164,6 +165,122 @@ describe('ast-utils: insertAfterLastOccurrence', () => { }); }); + +describe('addComponentToModule', () => { + beforeEach(() => { + mockFs( { + '1.ts': ` +import {NgModule} from '@angular/core'; + +@NgModule({ + declarations: [] +}) +class Module {}`, + '2.ts': ` +import {NgModule} from '@angular/core'; + +@NgModule({ + declarations: [ + Other + ] +}) +class Module {}`, + '3.ts': ` +import {NgModule} from '@angular/core'; + +@NgModule({ +}) +class Module {}`, + '4.ts': ` +import {NgModule} from '@angular/core'; + +@NgModule({ + field1: [], + field2: {} +}) +class Module {}` + }); + }); + afterEach(() => mockFs.restore()); + + it('works with empty array', () => { + return addComponentToModule('1.ts', 'MyClass', 'MyImportPath') + .then(change => change.apply()) + .then(() => readFile('1.ts', 'utf-8')) + .then(content => { + expect(content).to.equal( + '\n' + + 'import {NgModule} from \'@angular/core\';\n' + + 'import { MyClass } from \'MyImportPath\';\n' + + '\n' + + '@NgModule({\n' + + ' declarations: [MyClass]\n' + + '})\n' + + 'class Module {}' + ); + }) + }); + + it('works with array with declarations', () => { + return addComponentToModule('2.ts', 'MyClass', 'MyImportPath') + .then(change => change.apply()) + .then(() => readFile('2.ts', 'utf-8')) + .then(content => { + expect(content).to.equal( + '\n' + + 'import {NgModule} from \'@angular/core\';\n' + + 'import { MyClass } from \'MyImportPath\';\n' + + '\n' + + '@NgModule({\n' + + ' declarations: [\n' + + ' Other,\n' + + ' MyClass\n' + + ' ]\n' + + '})\n' + + 'class Module {}' + ); + }) + }); + + it('works without any declarations', () => { + return addComponentToModule('3.ts', 'MyClass', 'MyImportPath') + .then(change => change.apply()) + .then(() => readFile('3.ts', 'utf-8')) + .then(content => { + expect(content).to.equal( + '\n' + + 'import {NgModule} from \'@angular/core\';\n' + + 'import { MyClass } from \'MyImportPath\';\n' + + '\n' + + '@NgModule({\n' + + ' declarations: [MyClass]\n' + + '})\n' + + 'class Module {}' + ); + }) + }); + + it('works without a declaration field', () => { + return addComponentToModule('4.ts', 'MyClass', 'MyImportPath') + .then(change => change.apply()) + .then(() => readFile('4.ts', 'utf-8')) + .then(content => { + expect(content).to.equal( + '\n' + + 'import {NgModule} from \'@angular/core\';\n' + + 'import { MyClass } from \'MyImportPath\';\n' + + '\n' + + '@NgModule({\n' + + ' field1: [],\n' + + ' field2: {},\n' + + ' declarations: [MyClass]\n' + + '})\n' + + 'class Module {}' + ); + }) + }); +}); + /** * Gets node of kind kind from sourceFile */ diff --git a/tests/acceptance/generate-component.spec.js b/tests/acceptance/generate-component.spec.js index 3c8ccc87ea..16916d8ddb 100644 --- a/tests/acceptance/generate-component.spec.js +++ b/tests/acceptance/generate-component.spec.js @@ -1,4 +1,3 @@ -/*eslint-disable no-console */ 'use strict'; var fs = require('fs-extra'); @@ -11,6 +10,10 @@ var root = process.cwd(); var conf = require('ember-cli/tests/helpers/conf'); var Promise = require('ember-cli/lib/ext/promise'); var SilentError = require('silent-error'); +const denodeify = require('denodeify'); + +const readFile = denodeify(fs.readFile); + describe('Acceptance: ng generate component', function () { before(conf.setup); @@ -27,18 +30,23 @@ describe('Acceptance: ng generate component', function () { afterEach(function () { this.timeout(10000); - return tmp.teardown('./tmp'); }); - it('ng generate component my-comp', function () { - return ng(['generate', 'component', 'my-comp']).then(() => { - var testPath = path.join(root, 'tmp', 'foo', 'src', 'app', 'my-comp', 'my-comp.component.ts'); - expect(existsSync(testPath)).to.equal(true); - }); + it('my-comp', function () { + const testPath = path.join(root, 'tmp/foo/src/app/my-comp/my-comp.component.ts'); + const appModule = path.join(root, 'tmp/foo/src/app/app.module.ts'); + return ng(['generate', 'component', 'my-comp']) + .then(() => expect(existsSync(testPath)).to.equal(true)) + .then(() => readFile(appModule, 'utf-8')) + .then(content => { + // Expect that the app.module contains a reference to my-comp and its import. + expect(content).matches(/import.*MyCompComponent.*from '.\/my-comp\/my-comp.component';/); + expect(content).matches(/declarations:\s*\[[^\]]+?,\n\s+MyCompComponent\n/m); + }); }); - it('ng generate component test' + path.sep + 'my-comp', function () { + it('test' + path.sep + 'my-comp', function () { fs.mkdirsSync(path.join(root, 'tmp', 'foo', 'src', 'app', 'test')); return ng(['generate', 'component', 'test' + path.sep + 'my-comp']).then(() => { var testPath = path.join(root, 'tmp', 'foo', 'src', 'app', 'test', 'my-comp', 'my-comp.component.ts'); @@ -46,7 +54,7 @@ describe('Acceptance: ng generate component', function () { }); }); - it('ng generate component test' + path.sep + '..' + path.sep + 'my-comp', function () { + it('test' + path.sep + '..' + path.sep + 'my-comp', function () { return ng(['generate', 'component', 'test' + path.sep + '..' + path.sep + 'my-comp']) .then(() => { var testPath = path.join(root, 'tmp', 'foo', 'src', 'app', 'my-comp', 'my-comp.component.ts'); @@ -54,7 +62,7 @@ describe('Acceptance: ng generate component', function () { }); }); - it('ng generate component my-comp from a child dir', () => { + it('my-comp from a child dir', () => { fs.mkdirsSync(path.join(root, 'tmp', 'foo', 'src', 'app', '1')); return new Promise(function (resolve) { process.chdir('./src'); @@ -68,10 +76,10 @@ describe('Acceptance: ng generate component', function () { .then(() => { var testPath = path.join(root, 'tmp', 'foo', 'src', 'app', '1', 'my-comp', 'my-comp.component.ts'); expect(existsSync(testPath)).to.equal(true); - }, err => console.log('ERR: ', err)); + }); }); - it('ng generate component child-dir' + path.sep + 'my-comp from a child dir', () => { + it('child-dir' + path.sep + 'my-comp from a child dir', () => { fs.mkdirsSync(path.join(root, 'tmp', 'foo', 'src', 'app', '1', 'child-dir')); return new Promise(function (resolve) { process.chdir('./src'); @@ -86,50 +94,39 @@ describe('Acceptance: ng generate component', function () { var testPath = path.join( root, 'tmp', 'foo', 'src', 'app', '1', 'child-dir', 'my-comp', 'my-comp.component.ts'); expect(existsSync(testPath)).to.equal(true); - }, err => console.log('ERR: ', err)); + }); }); - it('ng generate component child-dir' + path.sep + '..' + path.sep + 'my-comp from a child dir', - () => { - fs.mkdirsSync(path.join(root, 'tmp', 'foo', 'src', 'app', '1')); - return new Promise(function (resolve) { - process.chdir('./src'); - resolve(); + it('child-dir' + path.sep + '..' + path.sep + 'my-comp from a child dir', () => { + fs.mkdirsSync(path.join(root, 'tmp', 'foo', 'src', 'app', '1')); + return Promise.resolve() + .then(() => process.chdir(path.normalize('./src/app/1'))) + .then(() => { + return ng([ + 'generate', 'component', 'child-dir' + path.sep + '..' + path.sep + 'my-comp' + ]) }) - .then(() => process.chdir('./app')) - .then(() => process.chdir('./1')) - .then(() => { - return ng([ - 'generate', 'component', 'child-dir' + path.sep + '..' + path.sep + 'my-comp' - ]) - }) - .then(() => { - var testPath = - path.join(root, 'tmp', 'foo', 'src', 'app', '1', 'my-comp', 'my-comp.component.ts'); - expect(existsSync(testPath)).to.equal(true); - }, err => console.log('ERR: ', err)); - }); + .then(() => { + var testPath = + path.join(root, 'tmp', 'foo', 'src', 'app', '1', 'my-comp', 'my-comp.component.ts'); + expect(existsSync(testPath)).to.equal(true); + }); + }); - it('ng generate component ' + path.sep + 'my-comp from a child dir, gens under ' + - path.join('src', 'app'), - () => { - fs.mkdirsSync(path.join(root, 'tmp', 'foo', 'src', 'app', '1')); - return new Promise(function (resolve) { - process.chdir('./src'); - resolve(); + it(path.sep + 'my-comp from a child dir, gens under ' + path.join('src', 'app'), () => { + fs.mkdirsSync(path.join(root, 'tmp', 'foo', 'src', 'app', '1')); + return Promise.resolve() + .then(() => process.chdir(path.normalize('./src/app/1'))) + .then(() => { + return ng(['generate', 'component', path.sep + 'my-comp']) }) - .then(() => process.chdir('./app')) - .then(() => process.chdir('./1')) - .then(() => { - return ng(['generate', 'component', path.sep + 'my-comp']) - }) - .then(() => { - var testPath = path.join(root, 'tmp', 'foo', 'src', 'app', 'my-comp', 'my-comp.component.ts'); - expect(existsSync(testPath)).to.equal(true); - }, err => console.log('ERR: ', err)); - }); + .then(() => { + var testPath = path.join(root, 'tmp', 'foo', 'src', 'app', 'my-comp', 'my-comp.component.ts'); + expect(existsSync(testPath)).to.equal(true); + }); + }); - it('ng generate component ..' + path.sep + 'my-comp from root dir will fail', () => { + it('..' + path.sep + 'my-comp from root dir will fail', () => { return ng(['generate', 'component', '..' + path.sep + 'my-comp']).then(() => { throw new SilentError(`ng generate component ..${path.sep}my-comp from root dir should fail.`); }, (err) => { @@ -137,7 +134,7 @@ describe('Acceptance: ng generate component', function () { }); }); - it('ng generate component mycomp will prefix selector', () => { + it('mycomp will prefix selector', () => { return ng(['generate', 'component', 'mycomp']) .then(() => { var testPath = path.join(root, 'tmp', 'foo', 'src', 'app', 'mycomp', 'mycomp.component.ts'); @@ -147,7 +144,7 @@ describe('Acceptance: ng generate component', function () { }); }); - it('ng generate component mycomp --no-prefix will not prefix selector', () => { + it('mycomp --no-prefix will not prefix selector', () => { return ng(['generate', 'component', 'mycomp', '--no-prefix']) .then(() => { var testPath = path.join(root, 'tmp', 'foo', 'src', 'app', 'mycomp', 'mycomp.component.ts'); @@ -157,7 +154,7 @@ describe('Acceptance: ng generate component', function () { }); }); - it('ng generate component myComp will succeed', () => { + it('myComp will succeed', () => { return ng(['generate', 'component', 'myComp']) .then(() => { var testPath = path.join(root, 'tmp', 'foo', 'src', 'app', 'my-comp', 'my-comp.component.ts'); @@ -165,24 +162,24 @@ describe('Acceptance: ng generate component', function () { }); }); - it('ng generate component my-comp --inline-template', function () { + it('my-comp --inline-template', function () { return ng(['generate', 'component', 'my-comp', '--inline-template']).then(() => { var testPath = path.join(root, 'tmp', 'foo', 'src', 'app', 'my-comp', 'my-comp.component.html'); expect(existsSync(testPath)).to.equal(false); }); }); - it('ng generate component my-comp --inline-style', function () { + it('my-comp --inline-style', function () { return ng(['generate', 'component', 'my-comp', '--inline-style']).then(() => { var testPath = path.join(root, 'tmp', 'foo', 'src', 'app', 'my-comp', 'my-comp.component.css'); expect(existsSync(testPath)).to.equal(false); }); }); - it('ng generate component my-comp --nospec', function() { + it('my-comp --nospec', function() { return ng(['generate', 'component', 'my-comp', '--nospec']).then(() => { var testPath = path.join(root, 'tmp', 'foo', 'src', 'app', 'my-comp', 'my-comp.component.spec.ts'); expect(existsSync(testPath)).to.equal(false); }); - }) + }); }); diff --git a/tests/acceptance/generate-directive.spec.js b/tests/acceptance/generate-directive.spec.js index 44ae806c89..70d75aca9e 100644 --- a/tests/acceptance/generate-directive.spec.js +++ b/tests/acceptance/generate-directive.spec.js @@ -1,4 +1,3 @@ -/*eslint-disable no-console */ 'use strict'; var fs = require('fs-extra'); @@ -11,6 +10,10 @@ var root = process.cwd(); var conf = require('ember-cli/tests/helpers/conf'); var Promise = require('ember-cli/lib/ext/promise'); var SilentError = require('silent-error'); +const denodeify = require('denodeify'); + +const readFile = denodeify(fs.readFile); + describe('Acceptance: ng generate directive', function () { before(conf.setup); @@ -31,21 +34,28 @@ describe('Acceptance: ng generate directive', function () { return tmp.teardown('./tmp'); }); - it('ng generate flat directive', function () { + it('flat', function () { return ng(['generate', 'directive', 'flat']).then(() => { - var testPath = path.join(root, 'tmp', 'foo', 'src', 'app', 'flat.directive.ts'); + var testPath = path.join(root, 'tmp/foo/src/app/flat.directive.ts'); expect(existsSync(testPath)).to.equal(true); }); }); - it('ng generate directive my-dir', function () { - return ng(['generate', 'directive', 'my-dir', '--flat', 'false']).then(() => { - var testPath = path.join(root, 'tmp', 'foo', 'src', 'app', 'my-dir', 'my-dir.directive.ts'); - expect(existsSync(testPath)).to.equal(true); - }); + it('my-dir --flat false', function () { + const appRoot = path.join(root, 'tmp/foo'); + const testPath = path.join(appRoot, 'src/app/my-dir/my-dir.directive.ts'); + const appModulePath = path.join(appRoot, 'src/app/app.module.ts'); + + return ng(['generate', 'directive', 'my-dir', '--flat', 'false']) + .then(() => expect(existsSync(testPath)).to.equal(true)) + .then(() => readFile(appModulePath, 'utf-8')) + .then(content => { + expect(content).matches(/import.*\bMyDir\b.*from '.\/my-dir\/my-dir.directive';/); + expect(content).matches(/declarations:\s*\[[^\]]+?,\n\s+MyDir\n/m); + }); }); - it('ng generate directive test' + path.sep + 'my-dir', function () { + it('test' + path.sep + 'my-dir', function () { fs.mkdirsSync(path.join(root, 'tmp', 'foo', 'src', 'app', 'test')); return ng(['generate', 'directive', 'test' + path.sep + 'my-dir', '--flat', 'false']).then(() => { var testPath = path.join(root, 'tmp', 'foo', 'src', 'app', 'test', 'my-dir', 'my-dir.directive.ts'); @@ -53,7 +63,7 @@ describe('Acceptance: ng generate directive', function () { }); }); - it('ng generate directive test' + path.sep + '..' + path.sep + 'my-dir', function () { + it('test' + path.sep + '..' + path.sep + 'my-dir', function () { return ng(['generate', 'directive', 'test' + path.sep + '..' + path.sep + 'my-dir', '--flat', 'false']) .then(() => { var testPath = path.join(root, 'tmp', 'foo', 'src', 'app', 'my-dir', 'my-dir.directive.ts'); @@ -61,7 +71,7 @@ describe('Acceptance: ng generate directive', function () { }); }); - it('ng generate directive my-dir from a child dir', () => { + it('my-dir from a child dir', () => { fs.mkdirsSync(path.join(root, 'tmp', 'foo', 'src', 'app', '1')); return new Promise(function (resolve) { process.chdir('./src'); @@ -76,10 +86,10 @@ describe('Acceptance: ng generate directive', function () { .then(() => { var testPath = path.join(root, 'tmp', 'foo', 'src', 'app', '1', 'my-dir', 'my-dir.directive.ts'); expect(existsSync(testPath)).to.equal(true); - }, err => console.log('ERR: ', err)); + }); }); - it('ng generate directive child-dir' + path.sep + 'my-dir from a child dir', () => { + it('child-dir' + path.sep + 'my-dir from a child dir', () => { fs.mkdirsSync(path.join(root, 'tmp', 'foo', 'src', 'app', '1', 'child-dir')); return new Promise(function (resolve) { process.chdir('./src'); @@ -95,10 +105,10 @@ describe('Acceptance: ng generate directive', function () { var testPath = path.join( root, 'tmp', 'foo', 'src', 'app', '1', 'child-dir', 'my-dir', 'my-dir.directive.ts'); expect(existsSync(testPath)).to.equal(true); - }, err => console.log('ERR: ', err)); + }); }); - it('ng generate directive child-dir' + path.sep + '..' + path.sep + 'my-dir from a child dir', + it('child-dir' + path.sep + '..' + path.sep + 'my-dir from a child dir', () => { fs.mkdirsSync(path.join(root, 'tmp', 'foo', 'src', 'app', '1')); return new Promise(function (resolve) { @@ -116,10 +126,10 @@ describe('Acceptance: ng generate directive', function () { var testPath = path.join(root, 'tmp', 'foo', 'src', 'app', '1', 'my-dir', 'my-dir.directive.ts'); expect(existsSync(testPath)).to.equal(true); - }, err => console.log('ERR: ', err)); + }); }); - it('ng generate directive ' + path.sep + 'my-dir from a child dir, gens under ' + + it(path.sep + 'my-dir from a child dir, gens under ' + path.join('src', 'app'), () => { fs.mkdirsSync(path.join(root, 'tmp', 'foo', 'src', 'app', '1')); @@ -136,10 +146,10 @@ describe('Acceptance: ng generate directive', function () { .then(() => { var testPath = path.join(root, 'tmp', 'foo', 'src', 'app', 'my-dir', 'my-dir.directive.ts'); expect(existsSync(testPath)).to.equal(true); - }, err => console.log('ERR: ', err)); + }); }); - it('ng generate directive ..' + path.sep + 'my-dir from root dir will fail', () => { + it('..' + path.sep + 'my-dir from root dir will fail', () => { return ng(['generate', 'directive', '..' + path.sep + 'my-dir']).then(() => { throw new SilentError(`ng generate directive ..${path.sep}my-dir from root dir should fail.`); }, (err) => { diff --git a/tests/acceptance/generate-pipe.spec.js b/tests/acceptance/generate-pipe.spec.js index 19af86494b..c6463697e2 100644 --- a/tests/acceptance/generate-pipe.spec.js +++ b/tests/acceptance/generate-pipe.spec.js @@ -11,6 +11,10 @@ var root = process.cwd(); var conf = require('ember-cli/tests/helpers/conf'); var Promise = require('ember-cli/lib/ext/promise'); var SilentError = require('silent-error'); +const denodeify = require('denodeify'); + +const readFile = denodeify(fs.readFile); + describe('Acceptance: ng generate pipe', function () { before(conf.setup); @@ -32,10 +36,16 @@ describe('Acceptance: ng generate pipe', function () { }); it('ng generate pipe my-pipe', function () { - return ng(['generate', 'pipe', 'my-pipe']).then(() => { - var testPath = path.join(root, 'tmp', 'foo', 'src', 'app', 'my-pipe.pipe.ts'); - expect(existsSync(testPath)).to.equal(true); - }); + const appRoot = path.join(root, 'tmp/foo'); + const testPath = path.join(appRoot, 'src/app/my-pipe.pipe.ts'); + const appModulePath = path.join(appRoot, 'src/app/app.module.ts'); + return ng(['generate', 'pipe', 'my-pipe']) + .then(() => expect(existsSync(testPath)).to.equal(true)) + .then(() => readFile(appModulePath, 'utf-8')) + .then(content => { + expect(content).matches(/import.*\bMyPipePipe\b.*from '.\/my-pipe.pipe';/); + expect(content).matches(/declarations:\s*\[[^\]]+?,\n\s+MyPipePipe\n/m); + }); }); it('ng generate pipe test' + path.sep + 'my-pipe', function () { diff --git a/tests/acceptance/generate-service.spec.js b/tests/acceptance/generate-service.spec.js index 71e31ac1cf..233d95720e 100644 --- a/tests/acceptance/generate-service.spec.js +++ b/tests/acceptance/generate-service.spec.js @@ -1,4 +1,3 @@ -/*eslint-disable no-console */ 'use strict'; var fs = require('fs-extra'); @@ -11,6 +10,10 @@ var root = process.cwd(); var conf = require('ember-cli/tests/helpers/conf'); var Promise = require('ember-cli/lib/ext/promise'); var SilentError = require('silent-error'); +const denodeify = require('denodeify'); + +const readFile = denodeify(fs.readFile); + describe('Acceptance: ng generate service', function () { before(conf.setup); @@ -32,10 +35,17 @@ describe('Acceptance: ng generate service', function () { }); it('ng generate service my-svc', function () { - return ng(['generate', 'service', 'my-svc']).then(() => { - var testPath = path.join(root, 'tmp', 'foo', 'src', 'app', 'my-svc.service.ts'); - expect(existsSync(testPath)).to.equal(true); - }); + const appRoot = path.join(root, 'tmp/foo'); + const testPath = path.join(appRoot, 'src/app/my-svc.service.ts'); + const appModulePath = path.join(appRoot, 'src/app/app.module.ts'); + + return ng(['generate', 'service', 'my-svc']) + .then(() => expect(existsSync(testPath)).to.equal(true)) + .then(() => readFile(appModulePath, 'utf-8')) + .then(content => { + expect(content).not.to.matches(/import.*\MySvcService\b.*from '.\/my-svc.service';/); + expect(content).not.to.matches(/providers:\s*\[MySvcService\]/m); + }); }); it('ng generate service test' + path.sep + 'my-svc', function () { @@ -68,7 +78,7 @@ describe('Acceptance: ng generate service', function () { .then(() => { var testPath = path.join(root, 'tmp', 'foo', 'src', 'app', '1', 'my-svc.service.ts'); expect(existsSync(testPath)).to.equal(true); - }, err => console.log('ERR: ', err)); + }); }); it('ng generate service child-dir' + path.sep + 'my-svc from a child dir', () => { @@ -87,7 +97,7 @@ describe('Acceptance: ng generate service', function () { var testPath = path.join( root, 'tmp', 'foo', 'src', 'app', '1', 'child-dir', 'my-svc.service.ts'); expect(existsSync(testPath)).to.equal(true); - }, err => console.log('ERR: ', err)); + }); }); it('ng generate service child-dir' + path.sep + '..' + path.sep + 'my-svc from a child dir', @@ -108,7 +118,7 @@ describe('Acceptance: ng generate service', function () { var testPath = path.join(root, 'tmp', 'foo', 'src', 'app', '1', 'my-svc.service.ts'); expect(existsSync(testPath)).to.equal(true); - }, err => console.log('ERR: ', err)); + }); }); it('ng generate service ' + path.sep + 'my-svc from a child dir, gens under ' + @@ -128,7 +138,7 @@ describe('Acceptance: ng generate service', function () { .then(() => { var testPath = path.join(root, 'tmp', 'foo', 'src', 'app', 'my-svc.service.ts'); expect(existsSync(testPath)).to.equal(true); - }, err => console.log('ERR: ', err)); + }); }); it('ng generate service ..' + path.sep + 'my-svc from root dir will fail', () => {