From b11bc94dfb6e3c8e802647f4baaae470c6e33671 Mon Sep 17 00:00:00 2001 From: Jan Kuri Date: Tue, 2 Aug 2016 23:47:30 +0200 Subject: [PATCH 01/12] fix(webpack-copy): copies files from public/ directory to dist/ and preserves references --- addon/ng2/models/webpack-build-common.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/addon/ng2/models/webpack-build-common.ts b/addon/ng2/models/webpack-build-common.ts index 4871410f9be7..a3fcdd5eac12 100644 --- a/addon/ng2/models/webpack-build-common.ts +++ b/addon/ng2/models/webpack-build-common.ts @@ -73,7 +73,11 @@ export function getWebpackCommonConfig(projectRoot: string, sourceDir: string) { filename: 'inline.js', sourceMapFilename: 'inline.map' }), - new CopyWebpackPlugin([{from: path.resolve(projectRoot, './public'), to: path.resolve(projectRoot, './dist')}]) + new CopyWebpackPlugin([{ + context: path.resolve(projectRoot, './public'), + from: '**/*', + to: path.resolve(projectRoot, './dist') + }]) ], node: { global: 'window', From f1808c3b8010687dd62e464350ac5373dc964f91 Mon Sep 17 00:00:00 2001 From: Mike Brocchi Date: Tue, 2 Aug 2016 23:51:51 -0400 Subject: [PATCH 02/12] bug(version): fix the version of angular-cli in package.json (#1531) Fixes #1528 --- addon/ng2/blueprints/ng2/files/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addon/ng2/blueprints/ng2/files/package.json b/addon/ng2/blueprints/ng2/files/package.json index e7dfbbcc5608..161087266c4c 100644 --- a/addon/ng2/blueprints/ng2/files/package.json +++ b/addon/ng2/blueprints/ng2/files/package.json @@ -36,7 +36,7 @@ "parse5": "1.5.1",<% } %> "@types/jasmine": "^2.2.30", "@types/protractor": "^1.5.16", - "angular-cli": "^<%= version %>", + "angular-cli": "<%= version %>", "codelyzer": "0.0.26", "jasmine-core": "2.4.1", "jasmine-spec-reporter": "2.5.0", From 5ea4b03b6e47d8eef1ef0e64d6a37368b64f1672 Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Wed, 3 Aug 2016 18:33:11 +0100 Subject: [PATCH 03/12] chore(docs): add webpack update instructions (#1456) --- README.md | 6 +++++ WEBPACK_UPDATE.md | 60 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 WEBPACK_UPDATE.md diff --git a/README.md b/README.md index ef4b9014c3c8..c38970179dd8 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,12 @@ This project is very much still a work in progress. The CLI is now in beta. If you wish to collaborate while the project is still young, check out [our issue list](https://github.com/angular/angular-cli/issues). +## Webpack preview release update + +We're updating the build system in Angular-CLI to use webpack instead of Broccoli. + +You can install and update your projects using [these instructions](https://github.com/angular/angular-cli/blob/master/WEBPACK_UPDATE.md). + ## Prerequisites The generated project has dependencies that require **Node 4 or greater**. diff --git a/WEBPACK_UPDATE.md b/WEBPACK_UPDATE.md new file mode 100644 index 000000000000..239bac58dfd3 --- /dev/null +++ b/WEBPACK_UPDATE.md @@ -0,0 +1,60 @@ +# Upgrading from angular-cli@1.0.0-beta.10 to the angular-cli@webpack + +To update `angular-cli` to the webpack preview, you must update both the global package, project's local package and your project files. + +## Global package: +``` +npm uninstall -g angular-cli +npm cache clean +npm install -g angular-cli@webpack +``` + +## Local project package: +``` +# commit any changes you have to git first +rm -rf node_modules dist tmp typings +npm install --save-dev angular-cli@webpack +``` + +IMPORTANT NOTE: +Currently project generated with `ng new` will use a wrong local CLI version (see https://github.com/angular/angular-cli/issues/1528). After initializing your project, run `npm install --save-dev angular-cli@webpack` to set the correct version. + +## Project files + +You will need to run `ng init` to check for changes in all the auto-generated files created by `ng new` and allow you to update yours. You are offered four choices for each changed file: `y` (overwrite), `n` (don't overwrite), `d` (show diff between your file and the updated file) and `h` (help). + +Carefully read the diffs for each code file, and either accept the changes or incorporate them manually after `ng init` finishes. + +You can find a sample github diff of the changes introduced between a beta.10 and webpack preview project at https://github.com/filipesilva/angular-cli-webpack-upgrade/commit/HEAD. + +Here is a summary of the file changes: + +1. Updated files: + * `./config/karma.conf.js` - `frameworks`/`plugins`/`files`/`exclude`/`preprocessors` entries changed, added `angularCliConfig` entry. + * `./e2e/tsconfig.json` - property cleanup, changed `outdir`, added `typeRoots` + * `./angular-cli.json` - updated version entry. + * `./README.md` - updated version entry, removed route from generator list. + * `./package.json` - removed typings postinstall script, removed `ember-cli-inject-live-reload`/`es6-shim`/`systemjs` dependencies, added `core-js`/`ts-helpers` dependencies, updated `angular-cli`/`codelyzer`/`ts-node`/`ts-lint`/`typescript` devDependencies, added `@types/jasmine`/`@types/protractor`/`karma-coverage` devDependencies. + * `./src/app/app.component.ts` (and all other components) - removed `module.id`, sass/less/stylus preprocessing now uses the real extension in `styleUrls` instead of `.css`. + * `./src/app/index.ts` - import adjusted due to environment files moving (see below). + * `./src/index.html` - removed templating tags and `SystemJS` import script. + * `./src/app/tsconfig.json` - property cleanup, changed `outdir`/`module`, added `libs`/`typeRoots`/`types` + * `./src/typings.d.ts` - added `System`/`require` typings, removed `typings` ref +1. Mobile app updated files: + * `./package.json` - local versions of `angular2-universal` now used by the build system, updated package versions. + * `./src/index.html` - moved service worker code to build system, hardcoded icons. + * `./src/main-app-shell.ts` - reworked to interface with the build system, see comments in file. +1. New files: + * `./src/polyfills.ts` - loads needed polyfills before main app + * `./src/test.ts` - unit test spec loader +1. These files have moved to `./src/app/environments/`: + * `./config/environment.dev.ts` + * `./config/environment.prod.ts` + * `./src/app/environment.ts` + +Lastly, you can delete these files as they are not needed anymore. + * `./config/karma-test-shim.js` + * `./src/system-config.ts` + * `./angular-cli-build.js` + * `./typings.json` + * `./.clang-format` (if present) \ No newline at end of file From 36c0ee5ee309ce6e1e4ff39a84ef73466897e5d5 Mon Sep 17 00:00:00 2001 From: Danny Blue Date: Thu, 4 Aug 2016 18:06:17 -0400 Subject: [PATCH 04/12] feat (CodeCoverage): use karma-remap-istanbul (#1468) --- addon/ng2/blueprints/ng2/files/config/karma.conf.js | 11 ++++++++--- addon/ng2/blueprints/ng2/files/package.json | 2 +- addon/ng2/models/webpack-build-test.js | 6 +++--- tests/e2e/e2e_workflow.spec.js | 6 ++++++ 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/addon/ng2/blueprints/ng2/files/config/karma.conf.js b/addon/ng2/blueprints/ng2/files/config/karma.conf.js index 118cdc2e7721..b8aa26e084b2 100644 --- a/addon/ng2/blueprints/ng2/files/config/karma.conf.js +++ b/addon/ng2/blueprints/ng2/files/config/karma.conf.js @@ -1,4 +1,4 @@ -// Karma configuration file, see link for more information +// Karma configuration file, see link for more information // https://karma-runner.github.io/0.13/config/configuration-file.html module.exports = function (config) { @@ -8,7 +8,7 @@ module.exports = function (config) { plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), - require('karma-coverage'), + require('karma-remap-istanbul'), require('angular-cli/plugins/karma') ], customLaunchers: { @@ -24,8 +24,13 @@ module.exports = function (config) { preprocessors: { './src/test.ts': ['angular-cli'] }, + remapIstanbulReporter: { + reports: { + html: 'coverage' + } + }, angularCliConfig: './angular-cli.json', - reporters: ['coverage', 'progress'], + reporters: ['progress', 'karma-remap-istanbul'], port: 9876, colors: true, logLevel: config.LOG_INFO, diff --git a/addon/ng2/blueprints/ng2/files/package.json b/addon/ng2/blueprints/ng2/files/package.json index 161087266c4c..ca5bead3b842 100644 --- a/addon/ng2/blueprints/ng2/files/package.json +++ b/addon/ng2/blueprints/ng2/files/package.json @@ -42,8 +42,8 @@ "jasmine-spec-reporter": "2.5.0", "karma": "0.13.22", "karma-chrome-launcher": "0.2.3", - "karma-coverage": "^1.0.0", "karma-jasmine": "0.3.8", + "karma-remap-istanbul": "^0.2.1", "protractor": "3.3.0", "ts-node": "1.2.1", "tslint": "3.13.0", diff --git a/addon/ng2/models/webpack-build-test.js b/addon/ng2/models/webpack-build-test.js index 5ed3d5c55ff8..a57313156b1f 100644 --- a/addon/ng2/models/webpack-build-test.js +++ b/addon/ng2/models/webpack-build-test.js @@ -46,8 +46,7 @@ const getWebpackTestConfig = function(projectRoot, sourceDir) { tsconfig: path.resolve(projectRoot, `./${sourceDir}/tsconfig.json`), module: 'commonjs', target: 'es5', - useForkChecker: true, - removeComments: true + useForkChecker: true } }, { @@ -70,7 +69,8 @@ const getWebpackTestConfig = function(projectRoot, sourceDir) { exclude: [ /\.(e2e|spec)\.ts$/, /node_modules/ - ] + ], + query: { 'force-sourcemap': true } } ] }, diff --git a/tests/e2e/e2e_workflow.spec.js b/tests/e2e/e2e_workflow.spec.js index 1aaa5159f3b7..deb515ffd943 100644 --- a/tests/e2e/e2e_workflow.spec.js +++ b/tests/e2e/e2e_workflow.spec.js @@ -283,6 +283,12 @@ describe('Basic end-to-end Workflow', function () { }); }); + it('Make sure the correct coverage folder is created', function () { + const coverageReport = path.join(process.cwd(), 'coverage', 'src', 'app'); + + expect(existsSync(coverageReport)).to.be.equal(true); + }); + it('moves all files that live inside `public` into `dist`', function () { this.timeout(420000); From 5a9941e4f32e3b02c3ca24a2833b94ec1bbe7d73 Mon Sep 17 00:00:00 2001 From: Hans Date: Sun, 7 Aug 2016 19:02:16 -0700 Subject: [PATCH 05/12] feature: disable --mobile (#1592) --- .travis.yml | 6 +++--- README.md | 4 +++- addon/ng2/commands/new.ts | 7 +++++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index dc16f03e5a5b..ef499ff31908 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ env: matrix: - SCRIPT=lint - SCRIPT=test - - TARGET=mobile SCRIPT=mobile_test +# - TARGET=mobile SCRIPT=mobile_test matrix: exclude: - node_js: "6" @@ -21,8 +21,8 @@ matrix: - os: osx node_js: "5" env: SCRIPT=lint - - os: osx - env: TARGET=mobile SCRIPT=mobile_test +# - os: osx +# env: TARGET=mobile SCRIPT=mobile_test before_install: - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi diff --git a/README.md b/README.md index c38970179dd8..d2687e641d56 100644 --- a/README.md +++ b/README.md @@ -212,7 +212,9 @@ You can modify the these scripts in `package.json` to run whatever tool you pref ### Support for offline applications -Angular-CLI includes support for offline applications via the `--mobile` flag on `ng new`. Support is experimental, please see the angular/mobile-toolkit project and https://mobile.angular.io/ for documentation on how to make use of this functionality. +**The `--mobile` flag has been disabled temporarily. Sorry for the inconvenience.** + +~~Angular-CLI includes support for offline applications via the `--` flag on `ng new`. Support is experimental, please see the angular/mobile-toolkit project and https://mobile.angular.io/ for documentation on how to make use of this functionality.~~ ### Commands autocompletion diff --git a/addon/ng2/commands/new.ts b/addon/ng2/commands/new.ts index 685e4e3e1124..d0f0d520908e 100644 --- a/addon/ng2/commands/new.ts +++ b/addon/ng2/commands/new.ts @@ -52,6 +52,13 @@ const NewCommand = Command.extend({ new SilentError(`We currently do not support a name of "${packageName}".`)); } + if (commandOptions.mobile) { + return Promise.reject(new SilentError( + 'The --mobile flag has been disabled temporarily while we await an update of ' + + 'angular-universal for supporting NgModule. Sorry for the inconvenience.' + )); + } + commandOptions.blueprint = normalizeBlueprint(commandOptions.blueprint); if (!commandOptions.directory) { From ace720beab6af1b82a0d212e8519270cc5696793 Mon Sep 17 00:00:00 2001 From: Sean Larkin Date: Mon, 8 Aug 2016 10:14:29 -0500 Subject: [PATCH 06/12] fix: Updated webpack-karma which has proper peer deps settings (#1597) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d6c4d54b3990..0e8cc9d9ce9f 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "istanbul-instrumenter-loader": "^0.2.0", "json-loader": "^0.5.4", "karma-sourcemap-loader": "^0.3.7", - "karma-webpack": "^1.7.0", + "karma-webpack": "^1.8.0", "leek": "0.0.21", "less": "^2.7.1", "less-loader": "^2.2.3", From 560ae8f9c294673805b1c99173309259c120fbb7 Mon Sep 17 00:00:00 2001 From: Sean Larkin Date: Mon, 8 Aug 2016 12:07:36 -0500 Subject: [PATCH 07/12] fix: Set fs building/polyfill empty for better package support (#1599) --- addon/ng2/models/webpack-build-common.ts | 3 ++- addon/ng2/models/webpack-build-development.ts | 1 + addon/ng2/models/webpack-build-production.ts | 1 + addon/ng2/models/webpack-build-test.js | 1 + 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/addon/ng2/models/webpack-build-common.ts b/addon/ng2/models/webpack-build-common.ts index a3fcdd5eac12..dfcbb9f65def 100644 --- a/addon/ng2/models/webpack-build-common.ts +++ b/addon/ng2/models/webpack-build-common.ts @@ -75,11 +75,12 @@ export function getWebpackCommonConfig(projectRoot: string, sourceDir: string) { }), new CopyWebpackPlugin([{ context: path.resolve(projectRoot, './public'), - from: '**/*', + from: '**/*', to: path.resolve(projectRoot, './dist') }]) ], node: { + fs: 'empty', global: 'window', crypto: 'empty', module: false, diff --git a/addon/ng2/models/webpack-build-development.ts b/addon/ng2/models/webpack-build-development.ts index 61932014d254..8c0ad7d146e4 100644 --- a/addon/ng2/models/webpack-build-development.ts +++ b/addon/ng2/models/webpack-build-development.ts @@ -17,6 +17,7 @@ export const getWebpackDevConfigPartial = function(projectRoot: string, sourceDi resourcePath: path.resolve(projectRoot, `./${sourceDir}`) }, node: { + fs: 'empty', global: 'window', crypto: 'empty', process: true, diff --git a/addon/ng2/models/webpack-build-production.ts b/addon/ng2/models/webpack-build-production.ts index af0bfe8bb055..fb4dcc083840 100644 --- a/addon/ng2/models/webpack-build-production.ts +++ b/addon/ng2/models/webpack-build-production.ts @@ -50,6 +50,7 @@ export const getWebpackProdConfigPartial = function(projectRoot: string, sourceD customAttrAssign: [/\)?\]?=/] }, node: { + fs: 'empty', global: 'window', crypto: 'empty', process: true, diff --git a/addon/ng2/models/webpack-build-test.js b/addon/ng2/models/webpack-build-test.js index a57313156b1f..d4fdbe37d854 100644 --- a/addon/ng2/models/webpack-build-test.js +++ b/addon/ng2/models/webpack-build-test.js @@ -86,6 +86,7 @@ const getWebpackTestConfig = function(projectRoot, sourceDir) { resourcePath: `./${sourceDir}` }, node: { + fs: 'empty', global: 'window', process: false, crypto: 'empty', From b4f8b7eadcbbd5fbff188f25f06374d2fed1446d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=C3=A1ngel=20Cabrera?= Date: Tue, 9 Aug 2016 19:55:20 +0200 Subject: [PATCH 08/12] Remove `import 'rxjs/Rx'` from `test.ts` #1602 (#1612) --- addon/ng2/blueprints/ng2/files/__path__/test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/addon/ng2/blueprints/ng2/files/__path__/test.ts b/addon/ng2/blueprints/ng2/files/__path__/test.ts index 21a83d1b0ad2..cef85f112c20 100644 --- a/addon/ng2/blueprints/ng2/files/__path__/test.ts +++ b/addon/ng2/blueprints/ng2/files/__path__/test.ts @@ -11,9 +11,6 @@ import 'zone.js/dist/async-test'; import 'zone.js/dist/fake-async-test'; import 'zone.js/dist/sync-test'; -// RxJS -import 'rxjs/Rx'; - // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. declare var __karma__: any; From e89d6f410215aa9d3c119d2148c88059fa0cc27a Mon Sep 17 00:00:00 2001 From: Jan Kuri Date: Tue, 9 Aug 2016 20:19:35 +0200 Subject: [PATCH 09/12] refactor(NgModule): update to RC5 (#1579) Merging this and will clean up manually myself. --- addon/ng2/blueprints/component/index.js | 28 ++++++-- addon/ng2/blueprints/directive/index.js | 30 ++++++-- .../ng2/files/__path__/app/app.module.ts | 23 ++++++ .../ng2/files/__path__/app/index.ts | 1 + .../ng2/blueprints/ng2/files/__path__/main.ts | 7 +- addon/ng2/blueprints/ng2/files/package.json | 16 ++--- addon/ng2/blueprints/pipe/index.js | 30 ++++++-- addon/ng2/utilities/ast-utils.ts | 70 ++++++++++++++++++- 8 files changed, 173 insertions(+), 32 deletions(-) create mode 100644 addon/ng2/blueprints/ng2/files/__path__/app/app.module.ts diff --git a/addon/ng2/blueprints/component/index.js b/addon/ng2/blueprints/component/index.js index 51c1b030fc1e..1de14e4afdac 100644 --- a/addon/ng2/blueprints/component/index.js +++ b/addon/ng2/blueprints/component/index.js @@ -5,6 +5,7 @@ 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'); +const astUtils = require('../../utilities/ast-utils'); module.exports = { description: '', @@ -119,13 +120,30 @@ 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';`); + if (!options.flat) { - return addBarrelRegistration(this, this.generatePath); + returns.push(function() { + return addBarrelRegistration(this, this.generatePath) + }); } else { - return addBarrelRegistration( - this, - this.generatePath, - options.entity.name + '.component'); + returns.push(function() { + return addBarrelRegistration( + this, + this.generatePath, + options.entity.name + '.component') + }); + } + + if (!options['skip-import']) { + returns.push(astUtils.importComponent(modulePath, classifiedName, importPath)); } + + return Promise.all(returns); } }; diff --git a/addon/ng2/blueprints/directive/index.js b/addon/ng2/blueprints/directive/index.js index d88faa6a0693..bd22a272689f 100644 --- a/addon/ng2/blueprints/directive/index.js +++ b/addon/ng2/blueprints/directive/index.js @@ -3,6 +3,8 @@ 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'); +const astUtils = require('../../utilities/ast-utils'); module.exports = { description: '', @@ -52,15 +54,29 @@ 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.flat) { - return addBarrelRegistration( - this, - this.generatePath); + returns.push(function() { + return addBarrelRegistration(this, this.generatePath) + }); } else { - return addBarrelRegistration( - this, - this.generatePath, - options.entity.name + '.directive'); + returns.push(function() { + return addBarrelRegistration( + this, + this.generatePath, + options.entity.name + '.directive') + }); + } + + if (!options['skip-import']) { + returns.push(astUtils.importComponent(modulePath, classifiedName, importPath)); } + + 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 new file mode 100644 index 000000000000..93334a5d006e --- /dev/null +++ b/addon/ng2/blueprints/ng2/files/__path__/app/app.module.ts @@ -0,0 +1,23 @@ +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';<% } %> + +@NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule, + CommonModule, + FormsModule<% if (isMobile) { %>, + AppShellModule<% } %> + ], + entryComponents: [AppComponent], + bootstrap: [AppComponent] +}) +export class AppModule { + +} \ No newline at end of file diff --git a/addon/ng2/blueprints/ng2/files/__path__/app/index.ts b/addon/ng2/blueprints/ng2/files/__path__/app/index.ts index 7aa84b623989..87743c9d56ee 100644 --- a/addon/ng2/blueprints/ng2/files/__path__/app/index.ts +++ b/addon/ng2/blueprints/ng2/files/__path__/app/index.ts @@ -1,2 +1,3 @@ export * from './environments/environment'; export * from './app.component'; +export * from './app.module'; diff --git a/addon/ng2/blueprints/ng2/files/__path__/main.ts b/addon/ng2/blueprints/ng2/files/__path__/main.ts index 68e79759c3b4..4bdf15c04a38 100644 --- a/addon/ng2/blueprints/ng2/files/__path__/main.ts +++ b/addon/ng2/blueprints/ng2/files/__path__/main.ts @@ -1,10 +1,9 @@ -import { bootstrap } from '@angular/platform-browser-dynamic'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { enableProdMode } from '@angular/core'; -import { AppComponent, environment } from './app/';<% if(isMobile) { %> -import { APP_SHELL_RUNTIME_PROVIDERS } from '@angular/app-shell';<% } %> +import { AppModule, environment } from './app/'; if (environment.production) { enableProdMode(); } -bootstrap(AppComponent<% if(isMobile) { %>, [ APP_SHELL_RUNTIME_PROVIDERS ]<% } %>); +platformBrowserDynamic().bootstrapModule(AppModule); diff --git a/addon/ng2/blueprints/ng2/files/package.json b/addon/ng2/blueprints/ng2/files/package.json index ca5bead3b842..5572dda6e93e 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": "2.0.0-rc.4", - "@angular/compiler": "2.0.0-rc.4", - "@angular/core": "2.0.0-rc.4", - "@angular/forms": "0.2.0", - "@angular/http": "2.0.0-rc.4", - "@angular/platform-browser": "2.0.0-rc.4", - "@angular/platform-browser-dynamic": "2.0.0-rc.4", - "@angular/router": "3.0.0-beta.2", + "@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", "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 6ea6ceff46ae..202c025beec4 100644 --- a/addon/ng2/blueprints/pipe/index.js +++ b/addon/ng2/blueprints/pipe/index.js @@ -3,6 +3,8 @@ 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'); +const astUtils = require('../../utilities/ast-utils'); module.exports = { description: '', @@ -50,15 +52,29 @@ 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.flat) { - return addBarrelRegistration( - this, - this.generatePath); + returns.push(function() { + return addBarrelRegistration(this, this.generatePath) + }); } else { - return addBarrelRegistration( - this, - this.generatePath, - options.entity.name + '.pipe'); + returns.push(function() { + return addBarrelRegistration( + this, + this.generatePath, + options.entity.name + '.pipe') + }); + } + + if (!options['skip-import']) { + returns.push(astUtils.importComponent(modulePath, classifiedName, importPath)); } + + return Promise.all(returns); } }; diff --git a/addon/ng2/utilities/ast-utils.ts b/addon/ng2/utilities/ast-utils.ts index 7b9f1104f33f..f631eefb9653 100644 --- a/addon/ng2/utilities/ast-utils.ts +++ b/addon/ng2/utilities/ast-utils.ts @@ -1,11 +1,22 @@ import * as ts from 'typescript'; +import * as fs from 'fs'; import { InsertChange } from './change'; +/** +* 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); +} + /** * Find all nodes from the AST in the subtree of node of SyntaxKind kind. * @param node * @param kind -* @return all nodes of kind kind, or [] if none is found +* @return all nodes of kind, or [] if none is found */ export function findNodes(node: ts.Node, kind: ts.SyntaxKind): ts.Node[] { if (!node) { @@ -19,6 +30,26 @@ 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. * @return function to sort nodes in increasing order of position in sourceFile @@ -52,3 +83,40 @@ export function insertAfterLastOccurrence(nodes: ts.Node[], toInsert: string, let lastItemPosition: number = lastItem ? lastItem.end : fallbackPos; 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); + + 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(); + }); +} + From 4fd8e9c511ef10722a86975567971b622fd19236 Mon Sep 17 00:00:00 2001 From: emma-mens Date: Tue, 9 Aug 2016 15:22:58 -0700 Subject: [PATCH 10/12] feat: add utility functions for route generation (#1330) 'route-utils.ts' provides utility functions to be used in generating routes --- addon/ng2/utilities/dynamic-path-parser.js | 1 - addon/ng2/utilities/route-utils.ts | 522 ++++++++++++++++++ tests/acceptance/route-utils.spec.ts | 602 +++++++++++++++++++++ 3 files changed, 1124 insertions(+), 1 deletion(-) create mode 100644 addon/ng2/utilities/route-utils.ts create mode 100644 tests/acceptance/route-utils.spec.ts diff --git a/addon/ng2/utilities/dynamic-path-parser.js b/addon/ng2/utilities/dynamic-path-parser.js index a00300596b70..0313813cafad 100644 --- a/addon/ng2/utilities/dynamic-path-parser.js +++ b/addon/ng2/utilities/dynamic-path-parser.js @@ -56,4 +56,3 @@ module.exports = function dynamicPathParser(project, entityName) { return parsedPath; }; - diff --git a/addon/ng2/utilities/route-utils.ts b/addon/ng2/utilities/route-utils.ts new file mode 100644 index 000000000000..34716bfefa97 --- /dev/null +++ b/addon/ng2/utilities/route-utils.ts @@ -0,0 +1,522 @@ +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 => (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 => (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 { + 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); +} diff --git a/tests/acceptance/route-utils.spec.ts b/tests/acceptance/route-utils.spec.ts new file mode 100644 index 000000000000..6d1a7c48f8dd --- /dev/null +++ b/tests/acceptance/route-utils.spec.ts @@ -0,0 +1,602 @@ +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 path from 'path'; +import { InsertChange, RemoveChange } from '../../addon/ng2/utilities/change'; +import * as Promise from 'ember-cli/lib/ext/promise'; +import * as _ from 'lodash'; + +const readFile = Promise.denodeify(fs.readFile); + +describe('route utils', () => { + describe('insertImport', () => { + const sourceFile = 'tmp/tmp.ts'; + beforeEach(() => { + let mockDrive = { + 'tmp': { + 'tmp.ts': '' + } + }; + mockFs(mockDrive); + }); + + afterEach(() => { + mockFs.restore(); + }); + + it('inserts as last import if not present', () => { + 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';`); + }); + }); + 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); + }); + }); + 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'`); + }); + }); + 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); + }); + }); + 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`); + }); + }); + 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`); + }); + }); + }); + + describe('bootstrapItem', () => { + const mainFile = 'tmp/main.ts'; + const prefix = `import {bootstrap} from '@angular/platform-browser-dynamic'; \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`; + beforeEach(() => { + let mockDrive = { + 'tmp': { + 'main.ts': `import {bootstrap} from '@angular/platform-browser-dynamic'; \n` + + `import { AppComponent } from './app/'; \n` + + 'bootstrap(AppComponent);' + } + }; + mockFs(mockDrive); + }); + + afterEach(() => { + mockFs.restore(); + }); + + 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) ]);'); + }); + }); + it('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'; + import { provideRouter } from '@angular/router'; + bootstrap(AppComponent, [ provideRouter(routes) ]);`); + }); + }); + it('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) ]);'); + }); + }); + 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) ]);'); + }); + }); + 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)]);'); + }); + }); + 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) ]);'); + }); + }); + 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) ]);'); + }); + }); + 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)]);'); + }); + }); + 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') + ); + }); + 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});'); + }); + }); + }); + + describe('addPathToRoutes', () => { + const routesFile = 'src/routes.ts'; + var options = {dir: 'src/app', appRoot: 'src/app', routesFile: routesFile, + component: 'NewRouteComponent', dasherizedName: 'new-route'}; + const nestedRoutes = `\n { path: 'home', component: HomeComponent, + children: [ + { path: 'about', component: AboutComponent, + children: [ + { path: 'more', component: MoreComponent } + ] + } + ] + }\n`; + beforeEach(() => { + let mockDrive = { + 'src': { + 'routes.ts' : 'export default [];' + } + }; + mockFs(mockDrive); + }); + + afterEach(() => { + mockFs.restore(); + }); + + 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'; +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`); + }); + }); + it('throws error if no export defaults exists', () => { + let editedFile = new RemoveChange(routesFile, 0, '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`); + }); + }); + it('treats positional params correctly', () => { + let editedFile = new InsertChange(routesFile, 16, + `\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];`); + }); + }); + it('inserts under parent, mid', () => { + let editedFile = new InsertChange(routesFile, 16, nestedRoutes); + return editedFile.apply().then(() => { + options.dasherizedName = 'details'; + 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'; +export default [ + { path: 'home', component: HomeComponent, + children: [ + { path: 'about', component: AboutComponent, + children: [ + { path: 'details', component: DetailsComponent }, + { path: 'more', component: MoreComponent } + ] + } + ] + }\n];`; + expect(content).to.equal(expected); + }); + }); + it('inserts under parent, deep', () => { + let editedFile = new InsertChange(routesFile, 16, nestedRoutes); + return editedFile.apply().then(() => { + options.dasherizedName = 'sections'; + 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'; +export default [ + { path: 'home', component: HomeComponent, + children: [ + { path: 'about', component: AboutComponent, + children: [ + { path: 'more', component: MoreComponent, + children: [ + { path: 'sections', component: SectionsComponent } + ] + } + ] + } + ] + } +];`; + expect(content).to.equal(expected); + }); + }); + it('works well with multiple routes in a level', () => { + let paths = `\n { path: 'main', component: MainComponent } + { path: 'home', component: HomeComponent, + children: [ + { path: 'about', component: AboutComponent } + ] + }\n`; + let editedFile = new InsertChange(routesFile, 16, paths); + return editedFile.apply().then(() => { + options.dasherizedName = 'about'; + 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'; +export default [ + { path: 'main', component: MainComponent } + { path: 'home', component: HomeComponent, + children: [ + { path: 'about/:id', component: AboutComponent_1 }, + { path: 'about', component: AboutComponent } + ] + } +];` + ); + }); + }); + 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)); + }).catch(e => { + expect(e.message).to.equal('Route was not added since it is a duplicate'); + }); + }); + it('throws error if repeating child, mid', () => { + let editedFile = new InsertChange(routesFile, 16, nestedRoutes); + return editedFile.apply().then(() => { + options.dasherizedName = 'about'; + options.component = 'AboutComponent'; + 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'); + }); + }); + it('throws error if repeating child, deep', () => { + let editedFile = new InsertChange(routesFile, 16, nestedRoutes); + return editedFile.apply().then(() => { + options.dasherizedName = 'more'; + options.component = 'MoreComponent'; + 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'); + }); + }); + it('does not report false repeat', () => { + let editedFile = new InsertChange(routesFile, 16, nestedRoutes); + return editedFile.apply().then(() => { + options.dasherizedName = 'more'; + 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'; +export default [ + { path: 'more', component: MoreComponent }, + { path: 'home', component: HomeComponent, + children: [ + { path: 'about', component: AboutComponent, + children: [ + { path: 'more', component: MoreComponent } + ] + } + ] + }\n];`; + expect(content).to.equal(expected); + }); + }); + it('does not report false repeat: multiple paths on a level', () => { + + let routes = `\n { path: 'home', component: HomeComponent, + children: [ + { path: 'about', component: AboutComponent, + children: [ + { path: 'more', component: MoreComponent } + ] + } + ] + },\n { path: 'trap-queen', component: TrapQueenComponent}\n`; + + let editedFile = new InsertChange(routesFile, 16, routes); + return editedFile.apply().then(() => { + options.dasherizedName = 'trap-queen'; + options.component = 'TrapQueenComponent'; + return nru.applyChanges( + 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 [ + { path: 'home', component: HomeComponent, + children: [ + { path: 'trap-queen', component: TrapQueenComponent }, + { path: 'about', component: AboutComponent, + children: [ + { path: 'more', component: MoreComponent } + ] + } + ] + },\n { path: 'trap-queen', component: TrapQueenComponent}\n];`; + expect(content).to.equal(expected); + }); + }); + it('resolves imports correctly', () => { + let editedFile = new InsertChange(routesFile, 16, + `\n { path: 'home', component: HomeComponent }\n`); + return editedFile.apply().then(() => { + let editedFile = new InsertChange(routesFile, 0, + `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'; +import { HomeComponent as HomeComponent_1 } from './app/home/home/home.component'; +export default [ + { path: 'home', component: HomeComponent, + children: [ + { path: 'home', component: HomeComponent_1 } + ] + } +];`; + expect(content).to.equal(expected); + }); + }); + it('throws error if components collide and there is repitition', () => { + let editedFile = new InsertChange(routesFile, 16, +`\n { path: 'about', component: AboutComponent, + children: [ + { path: 'details/:id', component: DetailsComponent_1 }, + { path: 'details', component: DetailsComponent } + ] + }`); + return editedFile.apply().then(() => { + let editedFile = new InsertChange(routesFile, 0, +`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(); + }).then(() => { + options.dasherizedName = 'details'; + options.component = 'DetailsComponent'; + expect(() => nru.addPathToRoutes(routesFile, _.merge({route: 'about/details'}, options))) + .to.throw(Error); + }); + }); + + it('adds guard to parent route: addItemsToRouteProperties', () => { + let path = `\n { path: 'home', component: HomeComponent }\n`; + let editedFile = new InsertChange(routesFile, 16, path); + return editedFile.apply().then(() => { + let toInsert = {'home': ['canActivate', '[ MyGuard ]'] }; + return nru.applyChanges(nru.addItemsToRouteProperties(routesFile, toInsert)); }) + .then(() => readFile(routesFile, 'utf8')) + .then(content => { + expect(content).to.equal( +`export default [ + { path: 'home', component: HomeComponent, canActivate: [ MyGuard ] } +];` + ); + }); + }); + it('adds guard to child route: addItemsToRouteProperties', () => { + let path = `\n { path: 'home', component: HomeComponent }\n`; + let editedFile = new InsertChange(routesFile, 16, path); + return editedFile.apply().then(() => { + options.dasherizedName = 'more'; + 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'; +export default [ + { path: 'home', component: HomeComponent, + children: [ + { path: 'more', component: MoreComponent, canDeactivate: [ MyGuard ], useAsDefault: true } + ] + } +];` + ); + }); + }); + }); + + describe('validators', () => { + const projectRoot = process.cwd(); + const componentFile = path.join(projectRoot, 'src/app/about/about.component.ts'); + beforeEach(() => { + let mockDrive = { + 'src': { + 'app': { + 'about': { + 'about.component.ts' : 'export class AboutComponent { }' + } + } + } + }; + mockFs(mockDrive); + }); + + afterEach(() => { + mockFs.restore(); + }); + + it('accepts component name without \'component\' suffix: resolveComponentPath', () => { + let fileName = nru.resolveComponentPath(projectRoot, 'src/app', 'about'); + expect(fileName).to.equal(componentFile); + }); + it('accepts component name with \'component\' suffix: resolveComponentPath', () => { + let fileName = nru.resolveComponentPath(projectRoot, 'src/app', 'about.component'); + expect(fileName).to.equal(componentFile); + }); + it('accepts path absolute from project root: resolveComponentPath', () => { + let fileName = nru.resolveComponentPath(projectRoot, '', `${path.sep}about`); + expect(fileName).to.equal(componentFile); + }); + it('accept component with directory name: resolveComponentPath', () => { + let fileName = nru.resolveComponentPath(projectRoot, 'src/app', 'about/about.component'); + expect(fileName).to.equal(componentFile); + }); + + it('finds component name: confirmComponentExport', () => { + let exportExists = nru.confirmComponentExport(componentFile, 'AboutComponent'); + expect(exportExists).to.be.truthy; + }); + 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; + }); + }); + 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; + }); + }); + }); +}); From 5bcb7be9172b9b3eb6e5290a828f02ae4f113056 Mon Sep 17 00:00:00 2001 From: Hans Date: Tue, 9 Aug 2016 21:49:57 -0700 Subject: [PATCH 11/12] feat: ngmodules and insert components based on the AST (#1616) --- addon/ng2/blueprints/component/index.js | 27 +- addon/ng2/blueprints/directive/index.js | 30 +-- .../ng2/files/__path__/app/app.module.ts | 9 +- addon/ng2/blueprints/ng2/files/package.json | 16 +- addon/ng2/blueprints/pipe/index.js | 30 +-- addon/ng2/blueprints/service/index.js | 23 +- addon/ng2/utilities/ast-utils.ts | 252 ++++++++++++++---- addon/ng2/utilities/change.ts | 52 ++++ addon/ng2/utilities/dynamic-path-parser.js | 6 +- package.json | 2 + tests/acceptance/ast-utils.spec.ts | 119 ++++++++- tests/acceptance/generate-component.spec.js | 111 ++++---- tests/acceptance/generate-directive.spec.js | 48 ++-- tests/acceptance/generate-pipe.spec.js | 18 +- tests/acceptance/generate-service.spec.js | 28 +- 15 files changed, 558 insertions(+), 213 deletions(-) diff --git a/addon/ng2/blueprints/component/index.js b/addon/ng2/blueprints/component/index.js index 1de14e4afdac..7f992c6ebea8 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 bd22a272689f..fbba059a453d 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 93334a5d006e..f28035614b37 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 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/blueprints/pipe/index.js b/addon/ng2/blueprints/pipe/index.js index 202c025beec4..b747844910d0 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 38c87ecf8799..6fbe84ad5e5f 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 f631eefb9653..c3c52b4d4476 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); } + +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]); +} + + +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 component (component, pipe, directive) +* Custom function to insert a declaration (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 { + + 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 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", diff --git a/tests/acceptance/ast-utils.spec.ts b/tests/acceptance/ast-utils.spec.ts index cfc2cf42f346..e98d929b3dfb 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 3c8ccc87eac1..16916d8ddbe8 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 44ae806c895f..70d75aca9e41 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 19af86494b65..c6463697e2b0 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 71e31ac1cf2e..233d95720e06 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', () => { From 539c57ddd80c0a54de808d9f63cc9584ded7b357 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Wed, 10 Aug 2016 09:15:06 -0700 Subject: [PATCH 12/12] 1.0.0-beta.11-webpack.2 --- CHANGELOG.md | 18 ++++++++++++++++++ package.json | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2179e37fbe54..0b0c31122a70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ + +# [1.0.0-beta.11-webpack.2](https://github.com/angular/angular-cli/compare/v1.0.0-beta.10-webpack...v1.0.0-beta.11-webpack.2) (2016-08-10) + + +### Bug Fixes + +* **webpack-copy:** copies files from public/ directory to dist/ and preserves references ([b11bc94](https://github.com/angular/angular-cli/commit/b11bc94)) +* Set fs building/polyfill empty for better package support ([#1599](https://github.com/angular/angular-cli/issues/1599)) ([560ae8f](https://github.com/angular/angular-cli/commit/560ae8f)) +* Updated webpack-karma which has proper peer deps settings ([#1597](https://github.com/angular/angular-cli/issues/1597)) ([ace720b](https://github.com/angular/angular-cli/commit/ace720b)) + + +### Features + +* add utility functions for route generation ([#1330](https://github.com/angular/angular-cli/issues/1330)) ([4fd8e9c](https://github.com/angular/angular-cli/commit/4fd8e9c)) +* ngmodules and insert components based on the AST ([#1616](https://github.com/angular/angular-cli/issues/1616)) ([5bcb7be](https://github.com/angular/angular-cli/commit/5bcb7be)) + + + # [1.0.0-beta.11-webpack](https://github.com/angular/angular-cli/compare/v1.0.0-beta.10...v1.0.0-beta.11-webpack) (2016-08-02) diff --git a/package.json b/package.json index f720d364a7c3..e592a32ada2f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "angular-cli", - "version": "1.0.0-beta.11-webpack", + "version": "1.0.0-beta.11-webpack.2", "description": "CLI tool for Angular", "main": "lib/cli/index.js", "trackingCode": "UA-8594346-19",