diff --git a/.appveyor.yml b/.appveyor.yml index d9cd073ebc96..6d3c82f53721 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,6 +1,6 @@ environment: matrix: - - nodejs_version: "5.0" + - nodejs_version: "4.0" - nodejs_version: "6.0" matrix: @@ -8,6 +8,7 @@ matrix: install: - ps: Install-Product node $env:nodejs_version + - npm install -g npm - npm install test_script: diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 92f0cfde8292..fa7c45bbb104 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,22 +1,23 @@ > Please provide us with the following information: > --------------------------------------------------------------- -1. OS? Windows 7, 8 or 10. Linux (which distribution). Mac OSX (Yosemite? El Capitan?) +### OS? +> Windows 7, 8 or 10. Linux (which distribution). Mac OSX (Yosemite? El Capitan?) -2. Versions. Please run `ng --version`. If there's nothing outputted, please run -in a Terminal: `node --version` and paste the result here: +### Versions. +> Please run `ng --version`. If there's nothing outputted, please run in a Terminal: `node --version` and paste the result here: -3. Repro steps. Was this an app that wasn't created using the CLI? What change did you - do on your code? etc. +### Repro steps. +> Was this an app that wasn't created using the CLI? What change did you do on your code? etc. -4. The log given by the failure. Normally this include a stack trace and some - more information. +### The log given by the failure. +> Normally this include a stack trace and some more information. -5. Mention any other details that might be useful. +### Mention any other details that might be useful. > --------------------------------------------------------------- > Thanks! We'll be in touch soon. diff --git a/.travis.yml b/.travis.yml index cd2b7f8c4631..f821a853b7c7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,11 +2,9 @@ dist: trusty sudo: required language: node_js node_js: - - "5" - - "6" + - "4" os: - linux - - osx env: global: - DBUS_SESSION_BUS_ADDRESS=/dev/null @@ -20,25 +18,34 @@ env: matrix: fast_finish: true allow_failures: + - node_js: "5" - os: osx - env: NODE_SCRIPT="tests/e2e_runner.js --nightly" - exclude: + include: + - node_js: "5" + os: linux + env: SCRIPT=test + - node_js: "5" + os: linux + env: NODE_SCRIPT=tests/e2e_runner.js - node_js: "6" - env: SCRIPT=lint - - os: osx - env: NODE_SCRIPT="tests/e2e_runner.js --nightly" + os: linux + env: SCRIPT=test - node_js: "6" - env: NODE_SCRIPT="tests/e2e_runner.js --nightly" - - os: osx - node_js: "5" - env: SCRIPT=lint + os: linux + env: NODE_SCRIPT=tests/e2e_runner.js + - node_js: "4" + os: osx + env: SCRIPT=test + - node_js: "4" + os: osx + env: NODE_SCRIPT=tests/e2e_runner.js - node_js: "6" - env: SCRIPT=build - - os: osx - node_js: "5" - env: SCRIPT=build - - os: osx - env: TARGET=mobile SCRIPT=mobile_test + os: osx + env: SCRIPT=test + - node_js: "6" + os: osx + env: NODE_SCRIPT=tests/e2e_runner.js before_install: - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi @@ -50,6 +57,7 @@ before_install: - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export CHROME_BIN=chromium-browser; fi - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then echo "--no-sandbox" > ~/.config/chromium-flags.conf; fi - if [[ "$TARGET" == "mobile" ]]; then export MOBILE_TEST=true; fi + - npm install -g npm - npm config set spin false - npm config set progress false diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a9e221aca32..0025ae6129cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ + +# [1.0.0-beta.16](https://github.com/angular/angular-cli/compare/v1.0.0-beta.15...v1.0.0-beta.16) (2016-09-28) + + +### Bug Fixes + +* **build:** fail ng build on error ([#2360](https://github.com/angular/angular-cli/issues/2360)) ([aa48c30](https://github.com/angular/angular-cli/commit/aa48c30)), closes [#2014](https://github.com/angular/angular-cli/issues/2014) +* **build:** use config output path as default ([#2158](https://github.com/angular/angular-cli/issues/2158)) ([49a120b](https://github.com/angular/angular-cli/commit/49a120b)) +* **generate:** Update directive.spec.ts blueprint to fix incorret import ([#1940](https://github.com/angular/angular-cli/issues/1940)) ([93da512](https://github.com/angular/angular-cli/commit/93da512)) +* **karma:** set defaults for karma.conf.js ([#1837](https://github.com/angular/angular-cli/issues/1837)) ([e2e94a5](https://github.com/angular/angular-cli/commit/e2e94a5)) +* **test:** correctly report packages spec failures ([#2238](https://github.com/angular/angular-cli/issues/2238)) ([3102453](https://github.com/angular/angular-cli/commit/3102453)) + + +### Features + +* **webpackDevServer:** Add watchOptions for webpackDevServer ([#1814](https://github.com/angular/angular-cli/issues/1814)) ([ce03088](https://github.com/angular/angular-cli/commit/ce03088)) + + + # [1.0.0-beta.15](https://github.com/angular/angular-cli/compare/v1.0.0-beta.14...v1.0.0-beta.15) (2016-09-20) diff --git a/README.md b/README.md index 41284f42db97..29eb76208584 100644 --- a/README.md +++ b/README.md @@ -13,16 +13,16 @@ Prototype of a CLI for Angular 2 applications based on the [ember-cli](http://ww This project is very much still a work in progress. -The CLI is now in beta. +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 update -We changed the build system between beta.10 and beta.12, from SystemJS to Webpack. -And with it comes a lot of benefits. +We changed the build system between beta.10 and beta.14, from SystemJS to Webpack. +And with it comes a lot of benefits. To take advantage of these, your app built with the old beta will need to migrate. -You can update your `beta.10` projects to `beta.12` by following [these instructions](https://github.com/angular/angular-cli/wiki/Upgrading-from-Beta.10-to-Beta.14). +You can update your `beta.10` projects to `beta.14` by following [these instructions](https://github.com/angular/angular-cli/wiki/Upgrading-from-Beta.10-to-Beta.14). ## Prerequisites @@ -41,6 +41,7 @@ The generated project has dependencies that require **Node 4.x.x and NPM 3.x.x** * [Adding extra files to the build](#adding-extra-files-to-the-build) * [Running Unit Tests](#running-unit-tests) * [Running End-to-End Tests](#running-end-to-end-tests) +* [Proxy To Backend](#proxy-to-backend) * [Deploying the App via GitHub Pages](#deploying-the-app-via-github-pages) * [Linting and formatting code](#linting-and-formatting-code) * [Support for offline applications](#support-for-offline-applications) @@ -108,6 +109,7 @@ Service | `ng g service my-new-service` Class | `ng g class my-new-class` Interface | `ng g interface my-new-interface` Enum | `ng g enum my-new-enum` +Module | `ng g module my-module` ### Generating a route @@ -125,8 +127,8 @@ The build artifacts will be stored in the `dist/` directory. ### Build Targets and Environment Files -`ng build` can specify both a build target (`--target=production` or `--target=development`) and an -environment file to be used with that build (`--environment=dev` or `--environment=prod`). +`ng build` can specify both a build target (`--target=production` or `--target=development`) and an +environment file to be used with that build (`--environment=dev` or `--environment=prod`). By default, the development build target and environment are used. The mapping used to determine which environment file is used can be found in `angular-cli.json`: @@ -156,7 +158,7 @@ ng build You can also add your own env files other than `dev` and `prod` by doing the following: - create a `src/environments/environment.NAME.ts` -- add `{ "NAME": 'src/environments/environment.NAME.ts' }` to the the `apps[0].environments` object in `angular-cli.json` +- add `{ "NAME": 'src/environments/environment.NAME.ts' }` to the the `apps[0].environments` object in `angular-cli.json` - use them via the `--env=NAME` flag on the build/serve commands. ### Base tag handling in index.html @@ -171,7 +173,7 @@ ng build --bh /myUrl/ ### Bundling -All builds make use of bundling, and using the `--prod` flag in `ng build --prod` +All builds make use of bundling, and using the `--prod` flag in `ng build --prod` or `ng serve --prod` will also make use of uglifying and tree-shaking functionality. ### Running unit tests @@ -192,6 +194,33 @@ Before running the tests make sure you are serving the app via `ng serve`. End-to-end tests are run via [Protractor](https://angular.github.io/protractor/). +### Proxy To Backend +Using the proxying support in webpack's dev server we can highjack certain urls and send them to a backend server. +We do this by passing a file to `--proxy-config` + +Say we have a server running on `http://localhost:3000/api` and we want all calls th `http://localhost:4200/api` to go to that server. + +We create a file next to projects `package.json` called `proxy.conf.json` +with the content + +``` +{ + "/api": { + "target": "http://localhost:3000", + "secure": false + } +} +``` + +You can read more about what options are available here [webpack-dev-server proxy settings](https://webpack.github.io/docs/webpack-dev-server.html#proxy) + +and then we edit the `package.json` file's start script to be + +``` +"start": "ng serve --proxy-config proxy.conf.json", +``` + +now run it with `npm start` ### Deploying the app via GitHub Pages @@ -262,11 +291,11 @@ source ~/.bash_profile ### Global styles -The `styles.css` file allows users to add global styles and supports -[CSS imports](https://developer.mozilla.org/en/docs/Web/CSS/@import). +The `styles.css` file allows users to add global styles and supports +[CSS imports](https://developer.mozilla.org/en/docs/Web/CSS/@import). -If the project is created with the `--style=sass` option, this will be a `.sass` -file instead, and the same applies to `scss/less/styl`. +If the project is created with the `--style=sass` option, this will be a `.sass` +file instead, and the same applies to `scss/less/styl`. You can add more global styles via the `apps[0].styles` property in `angular-cli.json`. @@ -290,7 +319,7 @@ export class AppComponent { } ``` -When generating a new project you can also define which extention you want for +When generating a new project you can also define which extension you want for style files: ```bash @@ -316,11 +345,11 @@ npm install @types/d3 --save-dev ### Global Library Installation -Some javascript libraries need to be added to the global scope, and loaded as if -they were in a script tag. We can do this using the `apps[0].scripts` and +Some javascript libraries need to be added to the global scope, and loaded as if +they were in a script tag. We can do this using the `apps[0].scripts` and `apps[0].styles` properties of `angular-cli.json`. -As an example, to use [Boostrap 4](http://v4-alpha.getbootstrap.com/) this is +As an example, to use [Boostrap 4](http://v4-alpha.getbootstrap.com/) this is what you need to do: First install Bootstrap from `npm`: @@ -329,7 +358,7 @@ First install Bootstrap from `npm`: npm install bootstrap@next ``` -Then add the needed script files to to `apps[0].scripts`. +Then add the needed script files to `apps[0].scripts`: ``` "scripts": [ @@ -342,12 +371,12 @@ Then add the needed script files to to `apps[0].scripts`. Finally add the Bootstrap CSS to the `apps[0].styles` array: ``` "styles": [ - "styles.css", - "../node_modules/bootstrap/dist/css/bootstrap.css" + "../node_modules/bootstrap/dist/css/bootstrap.css", + "styles.css" ], ``` -Restart `ng serve` if you're running it, and Bootstrap 4 should be working on +Restart `ng serve` if you're running it, and Bootstrap 4 should be working on your app. ### Updating angular-cli @@ -372,7 +401,7 @@ Running `ng init` will check for changes in all the auto-generated files created Carefully read the diffs for each code file, and either accept the changes or incorporate them manually after `ng init` finishes. -**The main cause of errors after an update is failing to incorporate these updates into your code**. +**The main cause of errors after an update is failing to incorporate these updates into your code**. You can find more details about changes between versions in [CHANGELOG.md](https://github.com/angular/angular-cli/blob/master/CHANGELOG.md). diff --git a/lib/bootstrap-local.js b/lib/bootstrap-local.js index 1712a756116c..34b7a28c7a47 100644 --- a/lib/bootstrap-local.js +++ b/lib/bootstrap-local.js @@ -7,6 +7,7 @@ const ts = require('typescript'); global.angularCliIsLocal = true; +global.angularCliPackages = require('./packages'); const compilerOptions = JSON.parse(fs.readFileSync(path.join(__dirname, '../tsconfig.json'))); diff --git a/lib/packages.js b/lib/packages.js index ccb0e67215d8..204f71dab448 100644 --- a/lib/packages.js +++ b/lib/packages.js @@ -11,8 +11,11 @@ const packages = fs.readdirSync(packageRoot) .map(pkgName => ({ name: pkgName, root: path.join(packageRoot, pkgName) })) .filter(pkg => fs.statSync(pkg.root).isDirectory()) .reduce((packages, pkg) => { - let name = pkg == 'angular-cli' ? 'angular-cli' : `@angular-cli/${pkg.name}`; + let pkgJson = JSON.parse(fs.readFileSync(path.join(pkg.root, 'package.json'), 'utf8')); + let name = pkgJson['name']; packages[name] = { + dist: path.join(__dirname, '../dist', pkg.name), + packageJson: path.join(pkg.root, 'package.json'), root: pkg.root, main: path.resolve(pkg.root, 'src/index.ts') }; diff --git a/package.json b/package.json index 0f061d2b7463..8a11064fddad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "angular-cli", - "version": "1.0.0-beta.15", + "version": "1.0.0-beta.16", "description": "CLI tool for Angular", "main": "packages/angular-cli/lib/cli/index.js", "trackingCode": "UA-8594346-19", @@ -12,7 +12,7 @@ "build": "node ./scripts/publish/build.js", "build:patch": "node ./scripts/patch.js", "build:packages": "for PKG in packages/*; do echo Building $PKG...; tsc -p $PKG; done", - "test": "npm run test:packages && npm run test:cli", + "test": "npm-run-all -c test:packages test:cli", "e2e": "npm run test:e2e", "e2e:nightly": "node tests/e2e_runner.js --nightly", "mobile_test": "mocha tests/e2e/e2e_workflow.spec.js", @@ -20,7 +20,7 @@ "test:cli": "node tests/runner", "test:inspect": "node --inspect --debug-brk tests/runner", "test:packages": "node scripts/run-packages-spec.js", - "build-config-interface": "dtsgen lib/config/schema.json --out lib/config/schema.d.ts", + "build-config-interface": "dtsgen packages/angular-cli/lib/config/schema.json --out packages/angular-cli/lib/config/schema.d.ts", "eslint": "eslint .", "tslint": "tslint \"**/*.ts\" -c tslint.json -e \"**/blueprints/*/files/**/*.ts\" -e \"node_modules/**\" -e \"tmp/**\" -e \"dist/**\"", "lint": "npm-run-all -c eslint tslint" @@ -55,7 +55,7 @@ "awesome-typescript-loader": "^2.2.3", "chalk": "^1.1.3", "common-tags": "^1.3.1", - "compression-webpack-plugin": "^0.3.1", + "compression-webpack-plugin": "github:webpack/compression-webpack-plugin#7e55907cd54a2e91b96d25a660acc6a2a6453f54", "copy-webpack-plugin": "^3.0.1", "core-js": "^2.4.0", "css-loader": "^0.23.1", @@ -71,6 +71,7 @@ "fs.realpath": "^1.0.0", "glob": "^7.0.3", "handlebars": "^4.0.5", + "html-loader": "^0.4.4", "html-webpack-plugin": "^2.19.0", "istanbul-instrumenter-loader": "^0.2.0", "json-loader": "^0.5.4", @@ -109,7 +110,7 @@ "typedoc": "^0.4.2", "typescript": "2.0.2", "url-loader": "^0.5.7", - "webpack": "2.1.0-beta.22", + "webpack": "2.1.0-beta.25", "webpack-dev-server": "2.1.0-beta.3", "webpack-md5-hash": "0.0.5", "webpack-merge": "^0.14.0", diff --git a/packages/angular-cli/addon/index.js b/packages/angular-cli/addon/index.js index 13056bc09538..3a2d7abb1850 100644 --- a/packages/angular-cli/addon/index.js +++ b/packages/angular-cli/addon/index.js @@ -8,7 +8,8 @@ module.exports = { name: 'ng2', config: function () { - this.project.ngConfig = this.project.ngConfig || config.CliConfig.fromProject().config; + this.project.ngConfigObj = this.project.ngConfigObj || config.CliConfig.fromProject(); + this.project.ngConfig = this.project.ngConfig || this.project.ngConfigObj.config; }, blueprintsPath: function () { diff --git a/packages/angular-cli/blueprints/component/index.js b/packages/angular-cli/blueprints/component/index.js index 3f671370397e..39243e1eb508 100644 --- a/packages/angular-cli/blueprints/component/index.js +++ b/packages/angular-cli/blueprints/component/index.js @@ -1,19 +1,20 @@ -var path = require('path'); -var chalk = require('chalk'); -var Blueprint = require('ember-cli/lib/models/blueprint'); -var dynamicPathParser = require('../../utilities/dynamic-path-parser'); +const path = require('path'); +const chalk = require('chalk'); +const Blueprint = require('ember-cli/lib/models/blueprint'); +const dynamicPathParser = require('../../utilities/dynamic-path-parser'); const findParentModule = require('../../utilities/find-parent-module').default; -var getFiles = Blueprint.prototype.files; +const getFiles = Blueprint.prototype.files; const stringUtils = require('ember-cli-string-utils'); const astUtils = require('../../utilities/ast-utils'); +const NodeHost = require('@angular-cli/ast-tools').NodeHost; module.exports = { description: '', availableOptions: [ { name: 'flat', type: Boolean, default: false }, - { name: 'inline-template', type: Boolean, default: false, aliases: ['it'] }, - { name: 'inline-style', type: Boolean, default: false, aliases: ['is'] }, + { name: 'inline-template', type: Boolean, aliases: ['it'] }, + { name: 'inline-style', type: Boolean, aliases: ['is'] }, { name: 'prefix', type: Boolean, default: true }, { name: 'spec', type: Boolean, default: true } ], @@ -55,6 +56,14 @@ module.exports = { this.styleExt = this.project.ngConfig.defaults.styleExt; } + options.inlineStyle = options.inlineStyle !== undefined ? + options.inlineStyle : + this.project.ngConfigObj.get('defaults.inline.style'); + + options.inlineTemplate = options.inlineTemplate !== undefined ? + options.inlineTemplate : + this.project.ngConfigObj.get('defaults.inline.template'); + return { dynamicPath: this.dynamicPath.dir.replace(this.dynamicPath.appRoot, ''), flat: options.flat, @@ -117,7 +126,7 @@ module.exports = { if (!options['skip-import']) { returns.push( astUtils.addDeclarationToModule(this.pathToModule, className, importPath) - .then(change => change.apply())); + .then(change => change.apply(NodeHost))); } return Promise.all(returns); diff --git a/packages/angular-cli/blueprints/directive/files/__path__/__name__.directive.spec.ts b/packages/angular-cli/blueprints/directive/files/__path__/__name__.directive.spec.ts index 481baeaa967c..ffbcffab88c6 100644 --- a/packages/angular-cli/blueprints/directive/files/__path__/__name__.directive.spec.ts +++ b/packages/angular-cli/blueprints/directive/files/__path__/__name__.directive.spec.ts @@ -1,7 +1,7 @@ /* tslint:disable:no-unused-variable */ import { TestBed, async } from '@angular/core/testing'; -import { <%= classifiedModuleName %> } from './<%= dasherizedModuleName %>.directive'; +import { <%= classifiedModuleName %>Directive } from './<%= dasherizedModuleName %>.directive'; describe('Directive: <%= classifiedModuleName %>', () => { it('should create an instance', () => { diff --git a/packages/angular-cli/blueprints/directive/files/__path__/__name__.directive.ts b/packages/angular-cli/blueprints/directive/files/__path__/__name__.directive.ts index b7d242e80b5a..ee8bd3254f68 100644 --- a/packages/angular-cli/blueprints/directive/files/__path__/__name__.directive.ts +++ b/packages/angular-cli/blueprints/directive/files/__path__/__name__.directive.ts @@ -1,7 +1,7 @@ import { Directive } from '@angular/core'; @Directive({ - selector: '[<%= rawEntityName %>]' + selector: '[<%= selector %>]' }) export class <%= classifiedModuleName %>Directive { diff --git a/packages/angular-cli/blueprints/directive/index.js b/packages/angular-cli/blueprints/directive/index.js index db0858d2fcc9..60d029aabbab 100644 --- a/packages/angular-cli/blueprints/directive/index.js +++ b/packages/angular-cli/blueprints/directive/index.js @@ -3,6 +3,7 @@ var dynamicPathParser = require('../../utilities/dynamic-path-parser'); const stringUtils = require('ember-cli-string-utils'); const astUtils = require('../../utilities/ast-utils'); const findParentModule = require('../../utilities/find-parent-module').default; +const NodeHost = require('@angular-cli/ast-tools').NodeHost; module.exports = { description: '', @@ -31,9 +32,9 @@ module.exports = { this.project.ngConfig.apps[0].prefix) { defaultPrefix = this.project.ngConfig.apps[0].prefix; } - var prefix = this.options.prefix ? defaultPrefix : ''; + var prefix = this.options.prefix ? `${defaultPrefix}-` : ''; - this.rawEntityName = prefix + parsedPath.name; + this.selector = stringUtils.camelize(prefix + parsedPath.name); return parsedPath.name; }, @@ -41,7 +42,7 @@ module.exports = { return { dynamicPath: this.dynamicPath.dir, flat: options.flat, - rawEntityName: this.rawEntityName + selector: this.selector }; }, @@ -73,7 +74,7 @@ module.exports = { if (!options['skip-import']) { returns.push( astUtils.addDeclarationToModule(this.pathToModule, className, importPath) - .then(change => change.apply())); + .then(change => change.apply(NodeHost))); } return Promise.all(returns); diff --git a/packages/angular-cli/blueprints/module/files/__path__/__name__-routing.module.ts b/packages/angular-cli/blueprints/module/files/__path__/__name__-routing.module.ts new file mode 100644 index 000000000000..2d4459211035 --- /dev/null +++ b/packages/angular-cli/blueprints/module/files/__path__/__name__-routing.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; + +const routes: Routes = []; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [] +}) +export class <%= classifiedModuleName %>RoutingModule { } diff --git a/packages/angular-cli/blueprints/module/files/__path__/__name__.module.ts b/packages/angular-cli/blueprints/module/files/__path__/__name__.module.ts index 67f4f4a7a854..2505c216531f 100644 --- a/packages/angular-cli/blueprints/module/files/__path__/__name__.module.ts +++ b/packages/angular-cli/blueprints/module/files/__path__/__name__.module.ts @@ -1,11 +1,11 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common';<% if (routing) { %> -import { <%= camelizedModuleName %>Routing } from './<%= dasherizedModuleName %>.routing';<% } %> +import { <%= classifiedModuleName %>RoutingModule } from './<%= dasherizedModuleName %>-routing.module';<% } %> @NgModule({ imports: [ CommonModule<% if (routing) { %>, - <%= camelizedModuleName %>Routing<% } %> + <%= classifiedModuleName %>RoutingModule<% } %> ], declarations: [] }) diff --git a/packages/angular-cli/blueprints/module/files/__path__/__name__.routing.ts b/packages/angular-cli/blueprints/module/files/__path__/__name__.routing.ts deleted file mode 100644 index f59bd42e3fdc..000000000000 --- a/packages/angular-cli/blueprints/module/files/__path__/__name__.routing.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Routes, RouterModule } from '@angular/router'; - -export const <%= camelizedModuleName %>Routes: Routes = []; - -export const <%= camelizedModuleName %>Routing = RouterModule.forChild(<%= camelizedModuleName %>Routes); - diff --git a/packages/angular-cli/blueprints/module/index.js b/packages/angular-cli/blueprints/module/index.js index 7b2f01ff0e04..7153c6650034 100644 --- a/packages/angular-cli/blueprints/module/index.js +++ b/packages/angular-cli/blueprints/module/index.js @@ -34,7 +34,7 @@ module.exports = { fileList = fileList.filter(p => p.indexOf('__name__.module.spec.ts') < 0); } if (this.options && !this.options.routing) { - fileList = fileList.filter(p => p.indexOf('__name__.routing.ts') < 0); + fileList = fileList.filter(p => p.indexOf('__name__-routing.module.ts') < 0); } return fileList; diff --git a/packages/angular-cli/blueprints/ng2/files/__path__/app/app-routing.module.ts b/packages/angular-cli/blueprints/ng2/files/__path__/app/app-routing.module.ts new file mode 100644 index 000000000000..9e01aadffa7e --- /dev/null +++ b/packages/angular-cli/blueprints/ng2/files/__path__/app/app-routing.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; + +const routes: Routes = []; + +@NgModule({ + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule], + providers: [] +}) +export class AppRoutingModule { } diff --git a/packages/angular-cli/blueprints/ng2/files/__path__/app/app.component.spec.ts b/packages/angular-cli/blueprints/ng2/files/__path__/app/app.component.spec.ts index dfdebd7b9d19..7c6fe5eb99e3 100644 --- a/packages/angular-cli/blueprints/ng2/files/__path__/app/app.component.spec.ts +++ b/packages/angular-cli/blueprints/ng2/files/__path__/app/app.component.spec.ts @@ -21,13 +21,13 @@ describe('App: <%= jsComponentName %>', () => { it(`should have as title 'app works!'`, async(() => { let fixture = TestBed.createComponent(AppComponent); let app = fixture.debugElement.componentInstance; - expect(app.title).toEqual('app works!'); + expect(app.title).toEqual('<%= prefix %> works!'); })); it('should render title in a h1 tag', async(() => { let fixture = TestBed.createComponent(AppComponent); fixture.detectChanges(); let compiled = fixture.debugElement.nativeElement; - expect(compiled.querySelector('h1').textContent).toContain('app works!'); + expect(compiled.querySelector('h1').textContent).toContain('<%= prefix %> works!'); })); }); diff --git a/packages/angular-cli/blueprints/ng2/files/__path__/app/app.component.ts b/packages/angular-cli/blueprints/ng2/files/__path__/app/app.component.ts index 0d430aed8ce3..a7d81f09063c 100644 --- a/packages/angular-cli/blueprints/ng2/files/__path__/app/app.component.ts +++ b/packages/angular-cli/blueprints/ng2/files/__path__/app/app.component.ts @@ -2,16 +2,16 @@ import { Component } from '@angular/core';<% if (isMobile) { %> import { APP_SHELL_DIRECTIVES } from '@angular/app-shell';<% } %> @Component({ - selector: '<%= prefix %>-root', - <% if (isMobile) { %>template: ` + selector: '<%= prefix %>-root',<% if (isMobile || inlineTemplate) { %> + template: `

{{title}}

- `, - styles: [], - directives: [APP_SHELL_DIRECTIVES]<% } else { %>templateUrl: './app.component.html', + `,<% } else { %> + templateUrl: './app.component.html',<% } %><% if (isMobile || inlineStyle) { %> + styles: []<% } else { %> styleUrls: ['./app.component.<%= styleExt %>']<% } %> }) export class AppComponent { - title = 'app works!'; + title = '<%= prefix %> works!'; } diff --git a/packages/angular-cli/blueprints/ng2/files/__path__/app/app.module.ts b/packages/angular-cli/blueprints/ng2/files/__path__/app/app.module.ts index 67ae49119baa..51485d6538e3 100644 --- a/packages/angular-cli/blueprints/ng2/files/__path__/app/app.module.ts +++ b/packages/angular-cli/blueprints/ng2/files/__path__/app/app.module.ts @@ -1,7 +1,8 @@ import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { HttpModule } from '@angular/http'; +import { HttpModule } from '@angular/http';<% if (routing) { %> +import { AppRoutingModule } from './app-routing.module';<% } %> import { AppComponent } from './app.component'; @@ -12,7 +13,8 @@ import { AppComponent } from './app.component'; imports: [ BrowserModule, FormsModule, - HttpModule + HttpModule<% if (routing) { %>, + AppRoutingModule<% } %> ], providers: [], bootstrap: [AppComponent] diff --git a/packages/angular-cli/blueprints/ng2/files/__path__/index.html b/packages/angular-cli/blueprints/ng2/files/__path__/index.html index 250c1d86e223..1f8c4ad70d2d 100644 --- a/packages/angular-cli/blueprints/ng2/files/__path__/index.html +++ b/packages/angular-cli/blueprints/ng2/files/__path__/index.html @@ -22,7 +22,6 @@ <% } %> - <<%= prefix %>-root>Loading...-root> diff --git a/packages/angular-cli/blueprints/ng2/files/e2e/app.po.ts b/packages/angular-cli/blueprints/ng2/files/e2e/app.po.ts index 07330f209b56..9008b7bec9b0 100644 --- a/packages/angular-cli/blueprints/ng2/files/e2e/app.po.ts +++ b/packages/angular-cli/blueprints/ng2/files/e2e/app.po.ts @@ -1,4 +1,4 @@ -import { browser, element, by } from 'protractor/globals'; +import { browser, element, by } from 'protractor'; export class <%= jsComponentName %>Page { navigateTo() { diff --git a/packages/angular-cli/blueprints/ng2/files/package.json b/packages/angular-cli/blueprints/ng2/files/package.json index 776c8819baa3..ff5f4fae3c83 100644 --- a/packages/angular-cli/blueprints/ng2/files/package.json +++ b/packages/angular-cli/blueprints/ng2/files/package.json @@ -12,21 +12,21 @@ }, "private": true, "dependencies": { - "@angular/common": "2.0.0", - "@angular/compiler": "2.0.0", - "@angular/core": "2.0.0", - "@angular/forms": "2.0.0", - "@angular/http": "2.0.0", - "@angular/platform-browser": "2.0.0", - "@angular/platform-browser-dynamic": "2.0.0", - "@angular/router": "3.0.0", + "@angular/common": "~2.0.0", + "@angular/compiler": "~2.0.0", + "@angular/core": "~2.0.0", + "@angular/forms": "~2.0.0", + "@angular/http": "~2.0.0", + "@angular/platform-browser": "~2.0.0", + "@angular/platform-browser-dynamic": "~2.0.0", + "@angular/router": "~3.0.0", "core-js": "^2.4.1", "rxjs": "5.0.0-beta.12", "ts-helpers": "^1.1.1", "zone.js": "^0.6.23" }, "devDependencies": {<% if(isMobile) { %> - "@angular/platform-server": "2.0.0", + "@angular/platform-server": "~2.0.0", "@angular/service-worker": "0.2.0", "@angular/app-shell": "0.0.0", "angular2-universal":"0.104.5", @@ -43,7 +43,7 @@ "karma-cli": "^1.0.1", "karma-jasmine": "^1.0.2", "karma-remap-istanbul": "^0.2.1", - "protractor": "4.0.5", + "protractor": "4.0.9", "ts-node": "1.2.1", "tslint": "3.13.0", "typescript": "2.0.2" diff --git a/packages/angular-cli/blueprints/ng2/index.js b/packages/angular-cli/blueprints/ng2/index.js index 81d14a41d762..70a9f35d31ab 100644 --- a/packages/angular-cli/blueprints/ng2/index.js +++ b/packages/angular-cli/blueprints/ng2/index.js @@ -10,7 +10,10 @@ module.exports = { { name: 'source-dir', type: String, default: 'src', aliases: ['sd'] }, { name: 'prefix', type: String, default: 'app', aliases: ['p'] }, { name: 'style', type: String, default: 'css' }, - { name: 'mobile', type: Boolean, default: false } + { name: 'mobile', type: Boolean, default: false }, + { name: 'routing', type: Boolean, default: false }, + { name: 'inline-style', type: Boolean, default: false, aliases: ['is'] }, + { name: 'inline-template', type: Boolean, default: false, aliases: ['it'] } ], afterInstall: function (options) { @@ -38,15 +41,27 @@ module.exports = { prefix: options.prefix, styleExt: this.styleExt, relativeRootPath: relativeRootPath, - isMobile: options.mobile + isMobile: options.mobile, + routing: options.routing, + inlineStyle: options.inlineStyle, + inlineTemplate: options.inlineTemplate }; }, files: function() { var fileList = getFiles.call(this); if (this.options && this.options.mobile) { - fileList = fileList.filter(p => p.indexOf('__name__.component.html') < 0); - fileList = fileList.filter(p => p.indexOf('__name__.component.__styleext__') < 0); + fileList = fileList.filter(p => p.indexOf('app.component.html') < 0); + fileList = fileList.filter(p => p.indexOf('app.component.__styleext__') < 0); + } + if (this.options && !this.options.routing) { + fileList = fileList.filter(p => p.indexOf('app-routing.module.ts') < 0); + } + if (this.options && this.options.inlineTemplate) { + fileList = fileList.filter(p => p.indexOf('app.component.html') < 0); + } + if (this.options && this.options.inlineStyle) { + fileList = fileList.filter(p => p.indexOf('app.component.__styleext__') < 0); } return fileList; diff --git a/packages/angular-cli/blueprints/pipe/index.js b/packages/angular-cli/blueprints/pipe/index.js index 13f4d0902891..bc4b0710ca60 100644 --- a/packages/angular-cli/blueprints/pipe/index.js +++ b/packages/angular-cli/blueprints/pipe/index.js @@ -3,6 +3,7 @@ var dynamicPathParser = require('../../utilities/dynamic-path-parser'); const stringUtils = require('ember-cli-string-utils'); const astUtils = require('../../utilities/ast-utils'); const findParentModule = require('../../utilities/find-parent-module').default; +const NodeHost = require('@angular-cli/ast-tools').NodeHost; module.exports = { description: '', @@ -61,7 +62,7 @@ module.exports = { if (!options['skip-import']) { returns.push( astUtils.addDeclarationToModule(this.pathToModule, className, importPath) - .then(change => change.apply())); + .then(change => change.apply(NodeHost))); } return Promise.all(returns); diff --git a/packages/angular-cli/commands/build.ts b/packages/angular-cli/commands/build.ts index 1072ed950d9e..744b405fe567 100644 --- a/packages/angular-cli/commands/build.ts +++ b/packages/angular-cli/commands/build.ts @@ -10,6 +10,7 @@ export interface BuildOptions { watcher?: string; supressSizes: boolean; baseHref?: string; + aot?: boolean; } const BuildCommand = Command.extend({ @@ -25,11 +26,12 @@ const BuildCommand = Command.extend({ aliases: ['t', { 'dev': 'development' }, { 'prod': 'production' }] }, { name: 'environment', type: String, default: '', aliases: ['e'] }, - { name: 'output-path', type: 'Path', default: 'dist/', aliases: ['o'] }, - { name: 'watch', type: Boolean, default: false, aliases: ['w'] }, + { name: 'output-path', type: 'Path', default: null, aliases: ['o'] }, + { name: 'watch', type: Boolean, default: false, aliases: ['w'] }, { name: 'watcher', type: String }, { name: 'suppress-sizes', type: Boolean, default: false }, { name: 'base-href', type: String, default: null, aliases: ['bh'] }, + { name: 'aot', type: Boolean, default: false } ], run: function (commandOptions: BuildOptions) { diff --git a/packages/angular-cli/commands/init.ts b/packages/angular-cli/commands/init.ts index f8f082214c76..1b300e43b588 100644 --- a/packages/angular-cli/commands/init.ts +++ b/packages/angular-cli/commands/init.ts @@ -18,7 +18,6 @@ const InitCommand: any = Command.extend({ availableOptions: [ { name: 'dry-run', type: Boolean, default: false, aliases: ['d'] }, { name: 'verbose', type: Boolean, default: false, aliases: ['v'] }, - { name: 'blueprint', type: String, aliases: ['b'] }, { name: 'link-cli', type: Boolean, default: false, aliases: ['lc'] }, { name: 'skip-npm', type: Boolean, default: false, aliases: ['sn'] }, { name: 'skip-bower', type: Boolean, default: true, aliases: ['sb'] }, @@ -26,15 +25,14 @@ const InitCommand: any = Command.extend({ { name: 'source-dir', type: String, default: 'src', aliases: ['sd'] }, { name: 'style', type: String, default: 'css' }, { name: 'prefix', type: String, default: 'app', aliases: ['p'] }, - { name: 'mobile', type: Boolean, default: false } + { name: 'mobile', type: Boolean, default: false }, + { name: 'routing', type: Boolean, default: false }, + { name: 'inline-style', type: Boolean, default: false, aliases: ['is'] }, + { name: 'inline-template', type: Boolean, default: false, aliases: ['it'] } ], anonymousOptions: [''], - _defaultBlueprint: function () { - return 'ng2'; - }, - run: function (commandOptions: any, rawArgs: string[]) { if (commandOptions.dryRun) { commandOptions.skipNpm = true; @@ -97,14 +95,17 @@ const InitCommand: any = Command.extend({ const blueprintOpts = { dryRun: commandOptions.dryRun, - blueprint: commandOptions.blueprint || this._defaultBlueprint(), + blueprint: 'ng2', rawName: packageName, targetFiles: rawArgs || '', rawArgs: rawArgs.toString(), sourceDir: commandOptions.sourceDir, style: commandOptions.style, prefix: commandOptions.prefix, - mobile: commandOptions.mobile + mobile: commandOptions.mobile, + routing: commandOptions.routing, + inlineStyle: commandOptions.inlineStyle, + inlineTemplate: commandOptions.inlineTemplate }; if (!validProjectName(packageName)) { diff --git a/packages/angular-cli/commands/new.ts b/packages/angular-cli/commands/new.ts index 0c0d518744c7..5737c0afaee8 100644 --- a/packages/angular-cli/commands/new.ts +++ b/packages/angular-cli/commands/new.ts @@ -1,11 +1,11 @@ import * as chalk from 'chalk'; +import InitCommand from './init'; + const Command = require('ember-cli/lib/models/command'); const Project = require('ember-cli/lib/models/project'); const SilentError = require('silent-error'); const validProjectName = require('ember-cli/lib/utilities/valid-project-name'); -const normalizeBlueprint = require('ember-cli/lib/utilities/normalize-blueprint-option'); -import InitCommand from './init'; const NewCommand = Command.extend({ name: 'new', @@ -15,7 +15,6 @@ const NewCommand = Command.extend({ availableOptions: [ { name: 'dry-run', type: Boolean, default: false, aliases: ['d'] }, { name: 'verbose', type: Boolean, default: false, aliases: ['v'] }, - { name: 'blueprint', type: String, default: 'ng2', aliases: ['b'] }, { name: 'link-cli', type: Boolean, default: false, aliases: ['lc'] }, { name: 'skip-npm', type: Boolean, default: false, aliases: ['sn'] }, { name: 'skip-bower', type: Boolean, default: true, aliases: ['sb'] }, @@ -24,7 +23,10 @@ const NewCommand = Command.extend({ { name: 'source-dir', type: String, default: 'src', aliases: ['sd'] }, { name: 'style', type: String, default: 'css' }, { name: 'prefix', type: String, default: 'app', aliases: ['p'] }, - { name: 'mobile', type: Boolean, default: false } + { name: 'mobile', type: Boolean, default: false }, + { name: 'routing', type: Boolean, default: false }, + { name: 'inline-style', type: Boolean, default: false, aliases: ['is'] }, + { name: 'inline-template', type: Boolean, default: false, aliases: ['it'] } ], run: function (commandOptions: any, rawArgs: string[]) { @@ -59,8 +61,6 @@ const NewCommand = Command.extend({ )); } - commandOptions.blueprint = normalizeBlueprint(commandOptions.blueprint); - if (!commandOptions.directory) { commandOptions.directory = packageName; } diff --git a/packages/angular-cli/commands/serve.ts b/packages/angular-cli/commands/serve.ts index c820102a2eb3..936a341b4cfc 100644 --- a/packages/angular-cli/commands/serve.ts +++ b/packages/angular-cli/commands/serve.ts @@ -22,10 +22,10 @@ export interface ServeTaskOptions { liveReloadLiveCss?: boolean; target?: string; environment?: string; - outputPath?: string; ssl?: boolean; sslKey?: string; sslCert?: string; + aot?: boolean; } const ServeCommand = Command.extend({ @@ -78,7 +78,8 @@ const ServeCommand = Command.extend({ { name: 'environment', type: String, default: '', aliases: ['e'] }, { name: 'ssl', type: Boolean, default: false }, { name: 'ssl-key', type: String, default: 'ssl/server.key' }, - { name: 'ssl-cert', type: String, default: 'ssl/server.crt' } + { name: 'ssl-cert', type: String, default: 'ssl/server.crt' }, + { name: 'aot', type: Boolean, default: false } ], run: function(commandOptions: ServeTaskOptions) { diff --git a/packages/angular-cli/custom-typings.d.ts b/packages/angular-cli/custom-typings.d.ts index 1fbb264de74a..6b4552345a83 100644 --- a/packages/angular-cli/custom-typings.d.ts +++ b/packages/angular-cli/custom-typings.d.ts @@ -3,7 +3,7 @@ interface IWebpackDevServerConfigurationOptions { hot?: boolean; historyApiFallback?: boolean; compress?: boolean; - proxy?: {[key: string]: string}; + proxy?: { [key: string]: string }; staticOptions?: any; quiet?: boolean; noInfo?: boolean; @@ -17,6 +17,13 @@ interface IWebpackDevServerConfigurationOptions { headers?: { [key: string]: string }; stats?: { [key: string]: boolean }; inline: boolean; +<<<<<<< HEAD + https?: boolean; +======= + https?:boolean; +>>>>>>> Fixed the ssl command not working anymore + key?: string; + cert?: string; } interface WebpackProgressPluginOutputOptions { diff --git a/packages/angular-cli/lib/config/schema.d.ts b/packages/angular-cli/lib/config/schema.d.ts index f660f183f221..2c09a9471ca3 100644 --- a/packages/angular-cli/lib/config/schema.d.ts +++ b/packages/angular-cli/lib/config/schema.d.ts @@ -59,5 +59,10 @@ export interface CliConfig { defaults?: { styleExt?: string; prefixInterfaces?: boolean; + poll?: number; + inline?: { + style?: boolean; + template?: boolean; + }; }; } diff --git a/packages/angular-cli/lib/config/schema.json b/packages/angular-cli/lib/config/schema.json index af5d76267e71..8abcc7870466 100644 --- a/packages/angular-cli/lib/config/schema.json +++ b/packages/angular-cli/lib/config/schema.json @@ -135,10 +135,26 @@ }, "prefixInterfaces": { "type": "boolean" + }, + "poll": { + "type": "number" + }, + "inline": { + "type": "object", + "properties": { + "style": { + "type": "boolean", + "default": false + }, + "template": { + "type": "boolean", + "default": false + } + } } }, "additionalProperties": false } }, "additionalProperties": false -} \ No newline at end of file +} diff --git a/packages/angular-cli/models/find-lazy-modules.ts b/packages/angular-cli/models/find-lazy-modules.ts index 34d3ba74b4f5..cb838b6e6f83 100644 --- a/packages/angular-cli/models/find-lazy-modules.ts +++ b/packages/angular-cli/models/find-lazy-modules.ts @@ -55,7 +55,12 @@ export function findLazyModules(projectRoot: any): {[key: string]: string} { glob.sync(path.join(projectRoot, '/**/*.ts')) .forEach(tsPath => { findLoadChildren(tsPath).forEach(moduleName => { - const fileName = path.resolve(projectRoot, moduleName) + '.ts'; + let fileName = path.resolve(projectRoot, moduleName) + '.ts'; + // If path is a relative path + if (moduleName.startsWith('.')) { + const tsPathInfo = path.parse(tsPath); + fileName = path.resolve(tsPathInfo.dir, moduleName) + '.ts'; + } if (fs.existsSync(fileName)) { // Put the moduleName as relative to the main.ts. result[moduleName] = fileName; diff --git a/packages/angular-cli/models/json-schema/schema-tree.ts b/packages/angular-cli/models/json-schema/schema-tree.ts index 68a46dee9394..51c46b7f013b 100644 --- a/packages/angular-cli/models/json-schema/schema-tree.ts +++ b/packages/angular-cli/models/json-schema/schema-tree.ts @@ -281,7 +281,7 @@ export abstract class LeafSchemaTreeNode extends SchemaTreeNode { constructor(metaData: TreeNodeConstructorArgument) { super(metaData); - this._defined = metaData.value !== undefined; + this._defined = !(metaData.value === undefined || metaData.value === null); if ('default' in metaData.schema) { this._default = metaData.schema['default']; } diff --git a/packages/angular-cli/models/webpack-build-common.ts b/packages/angular-cli/models/webpack-build-common.ts index 6de3b9d3f6c0..ec755c36979a 100644 --- a/packages/angular-cli/models/webpack-build-common.ts +++ b/packages/angular-cli/models/webpack-build-common.ts @@ -1,11 +1,9 @@ +import * as webpack from 'webpack'; import * as path from 'path'; +import {BaseHrefWebpackPlugin} from '@angular-cli/base-href-webpack'; + const CopyWebpackPlugin = require('copy-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); -import * as webpack from 'webpack'; -const atl = require('awesome-typescript-loader'); - -import { BaseHrefWebpackPlugin } from '@angular-cli/base-href-webpack'; -import { findLazyModules } from './find-lazy-modules'; export function getWebpackCommonConfig( @@ -23,7 +21,6 @@ export function getWebpackCommonConfig( const scripts = appConfig.scripts ? appConfig.scripts.map((script: string) => path.resolve(appRoot, script)) : []; - const lazyModules = findLazyModules(appRoot); let entry: { [key: string]: string[] } = { main: [appMain] @@ -36,8 +33,7 @@ export function getWebpackCommonConfig( return { devtool: 'source-map', resolve: { - extensions: ['', '.ts', '.js'], - root: appRoot + extensions: ['.ts', '.js'] }, context: path.resolve(__dirname, './'), entry: entry, @@ -46,32 +42,15 @@ export function getWebpackCommonConfig( filename: '[name].bundle.js' }, module: { - preLoaders: [ + rules: [ { + enforce: 'pre', test: /\.js$/, loader: 'source-map-loader', exclude: [ /node_modules/ ] - } - ], - loaders: [ - { - test: /\.ts$/, - loaders: [ - { - loader: 'awesome-typescript-loader', - query: { - useForkChecker: true, - tsconfig: path.resolve(appRoot, appConfig.tsconfig) - } - }, { - loader: 'angular2-template-loader' - } - ], - exclude: [/\.(spec|e2e)\.ts$/] }, - // in main, load css as raw text        { exclude: styles, @@ -115,7 +94,7 @@ export function getWebpackCommonConfig(        { test: /\.json$/, loader: 'json-loader' },        { test: /\.(jpg|png|gif)$/, loader: 'url-loader?limit=10000' }, -        { test: /\.html$/, loader: 'raw-loader' }, +        { test: /\.html$/, loader: 'html-loader' }, { test: /\.(otf|woff|ttf|svg)$/, loader: 'url?limit=10000' }, { test: /\.woff2$/, loader: 'url?limit=10000&mimetype=font/woff2' }, @@ -123,8 +102,6 @@ export function getWebpackCommonConfig( ] }, plugins: [ - new webpack.ContextReplacementPlugin(/.*/, appRoot, lazyModules), - new atl.ForkCheckerPlugin(), new HtmlWebpackPlugin({ template: path.resolve(appRoot, appConfig.index), chunksSortMode: 'dependency' @@ -159,7 +136,7 @@ export function getWebpackCommonConfig( ], node: { fs: 'empty', - global: 'window', + global: true, crypto: 'empty', module: false, clearImmediate: false, diff --git a/packages/angular-cli/models/webpack-build-development.ts b/packages/angular-cli/models/webpack-build-development.ts index 50c5d9a55efc..5fb423d6edb2 100644 --- a/packages/angular-cli/models/webpack-build-development.ts +++ b/packages/angular-cli/models/webpack-build-development.ts @@ -1,5 +1,17 @@ const path = require('path'); +import * as webpack from 'webpack'; + +declare module 'webpack' { + export interface LoaderOptionsPlugin {} + export interface LoaderOptionsPluginStatic { + new (optionsObject: any): LoaderOptionsPlugin; + } + interface Webpack { + LoaderOptionsPlugin: LoaderOptionsPluginStatic; + } +}; + export const getWebpackDevConfigPartial = function(projectRoot: string, appConfig: any) { return { devtool: 'source-map', @@ -9,14 +21,20 @@ export const getWebpackDevConfigPartial = function(projectRoot: string, appConfi sourceMapFilename: '[name].map', chunkFilename: '[id].chunk.js' }, - tslint: { - emitErrors: false, - failOnHint: false, - resourcePath: path.resolve(projectRoot, appConfig.root) - }, + plugins: [ + new webpack.LoaderOptionsPlugin({ + options: { + tslint: { + emitErrors: false, + failOnHint: false, + resourcePath: path.resolve(projectRoot, appConfig.root) + }, + } + }) + ], node: { fs: 'empty', - global: 'window', + global: true, crypto: 'empty', process: true, module: false, diff --git a/packages/angular-cli/models/webpack-build-production.ts b/packages/angular-cli/models/webpack-build-production.ts index da7c9b636281..d1c7b24b1a45 100644 --- a/packages/angular-cli/models/webpack-build-production.ts +++ b/packages/angular-cli/models/webpack-build-production.ts @@ -5,7 +5,6 @@ import * as webpack from 'webpack'; export const getWebpackProdConfigPartial = function(projectRoot: string, appConfig: any) { return { - debug: false, devtool: 'source-map', output: { path: path.resolve(projectRoot, appConfig.outDir), @@ -25,27 +24,31 @@ export const getWebpackProdConfigPartial = function(projectRoot: string, appConf test: /\.js$|\.html$/, threshold: 10240, minRatio: 0.8 + }), + new webpack.LoaderOptionsPlugin({ + options: { + tslint: { + emitErrors: true, + failOnHint: true, + resourcePath: path.resolve(projectRoot, appConfig.root) + }, + htmlLoader: { + minimize: true, + removeAttributeQuotes: false, + caseSensitive: true, + customAttrSurround: [ + [/#/, /(?:)/], + [/\*/, /(?:)/], + [/\[?\(?/, /(?:)/] + ], + customAttrAssign: [/\)?\]?=/] + } + } }) ], - tslint: { - emitErrors: true, - failOnHint: true, - resourcePath: path.resolve(projectRoot, appConfig.root) - }, - htmlLoader: { - minimize: true, - removeAttributeQuotes: false, - caseSensitive: true, - customAttrSurround: [ - [/#/, /(?:)/], - [/\*/, /(?:)/], - [/\[?\(?/, /(?:)/] - ], - customAttrAssign: [/\)?\]?=/] - }, node: { fs: 'empty', - global: 'window', + global: true, crypto: 'empty', process: true, module: false, diff --git a/packages/angular-cli/models/webpack-build-test.js b/packages/angular-cli/models/webpack-build-test.js index dfb6deba34ff..e1d45510ec70 100644 --- a/packages/angular-cli/models/webpack-build-test.js +++ b/packages/angular-cli/models/webpack-build-test.js @@ -2,6 +2,7 @@ const path = require('path'); const webpack = require('webpack'); +const atl = require('awesome-typescript-loader'); const getWebpackTestConfig = function (projectRoot, environment, appConfig) { @@ -11,8 +12,12 @@ const getWebpackTestConfig = function (projectRoot, environment, appConfig) { devtool: 'inline-source-map', context: path.resolve(__dirname, './'), resolve: { - extensions: ['', '.ts', '.js'], - root: appRoot + extensions: ['.ts', '.js'], + plugins: [ + new atl.TsConfigPathsPlugin({ + tsconfig: path.resolve(appRoot, appConfig.tsconfig) + }) + ] }, entry: { test: path.resolve(appRoot, appConfig.test) @@ -22,9 +27,10 @@ const getWebpackTestConfig = function (projectRoot, environment, appConfig) { filename: '[name].bundle.js' }, module: { - preLoaders: [ + rules: [ { test: /\.ts$/, + enforce: 'pre', loader: 'tslint-loader', exclude: [ path.resolve(projectRoot, 'node_modules') @@ -32,14 +38,13 @@ const getWebpackTestConfig = function (projectRoot, environment, appConfig) { }, { test: /\.js$/, + enforce: 'pre', loader: 'source-map-loader', exclude: [ path.resolve(projectRoot, 'node_modules/rxjs'), path.resolve(projectRoot, 'node_modules/@angular') ] - } - ], - loaders: [ + }, { test: /\.ts$/, loaders: [ @@ -58,23 +63,22 @@ const getWebpackTestConfig = function (projectRoot, environment, appConfig) { ], exclude: [/\.e2e\.ts$/] }, - { test: /\.json$/, loader: 'json-loader' }, - { test: /\.css$/, loaders: ['raw-loader', 'postcss-loader'] }, - { test: /\.styl$/, loaders: ['raw-loader', 'postcss-loader', 'stylus-loader'] }, - { test: /\.less$/, loaders: ['raw-loader', 'postcss-loader', 'less-loader'] }, - { test: /\.scss$|\.sass$/, loaders: ['raw-loader', 'postcss-loader', 'sass-loader'] }, - { test: /\.(jpg|png)$/, loader: 'url-loader?limit=128000' }, - { test: /\.html$/, loader: 'raw-loader', exclude: [path.resolve(appRoot, appConfig.index)] } - ], - postLoaders: [ { test: /\.(js|ts)$/, loader: 'sourcemap-istanbul-instrumenter-loader', + enforce: 'post', exclude: [ /\.(e2e|spec)\.ts$/, /node_modules/ ], query: { 'force-sourcemap': true } - } + }, + { test: /\.json$/, loader: 'json-loader' }, + { test: /\.css$/, loaders: ['raw-loader', 'postcss-loader'] }, + { test: /\.styl$/, loaders: ['raw-loader', 'postcss-loader', 'stylus-loader'] }, + { test: /\.less$/, loaders: ['raw-loader', 'postcss-loader', 'less-loader'] }, + { test: /\.scss$|\.sass$/, loaders: ['raw-loader', 'postcss-loader', 'sass-loader'] }, + { test: /\.(jpg|png)$/, loader: 'url-loader?limit=128000' }, + { test: /\.html$/, loader: 'raw-loader', exclude: [path.resolve(appRoot, appConfig.index)] } ] }, plugins: [ @@ -89,16 +93,20 @@ const getWebpackTestConfig = function (projectRoot, environment, appConfig) { new RegExp(path.resolve(appRoot, appConfig.environments['source']) .replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&')), path.resolve(appRoot, appConfig.environments[environment]) - ) + ), + new webpack.LoaderOptionsPlugin({ + options: { + tslint: { + emitErrors: false, + failOnHint: false, + resourcePath: `./${appConfig.root}` + } + } + }) ], - tslint: { - emitErrors: false, - failOnHint: false, - resourcePath: `./${appConfig.root}` - }, node: { fs: 'empty', - global: 'window', + global: true, process: false, crypto: 'empty', module: false, diff --git a/packages/angular-cli/models/webpack-build-typescript.ts b/packages/angular-cli/models/webpack-build-typescript.ts new file mode 100644 index 000000000000..d23ad7ab074c --- /dev/null +++ b/packages/angular-cli/models/webpack-build-typescript.ts @@ -0,0 +1,70 @@ +import * as path from 'path'; +import * as webpack from 'webpack'; +import {findLazyModules} from './find-lazy-modules'; +import {NgcWebpackPlugin} from '@ngtools/webpack'; + +const atl = require('awesome-typescript-loader'); + +const g: any = global; +const webpackLoader: string = g['angularCliIsLocal'] + ? g.angularCliPackages['@ngtools/webpack'].main + : '@ngtools/webpack'; + + +export const getWebpackNonAotConfigPartial = function(projectRoot: string, appConfig: any) { + const appRoot = path.resolve(projectRoot, appConfig.root); + const lazyModules = findLazyModules(appRoot); + + return { + resolve: { + plugins: [ + new atl.TsConfigPathsPlugin({ + tsconfig: path.resolve(appRoot, appConfig.tsconfig) + }) + ] + }, + module: { + rules: [ + { + test: /\.ts$/, + loaders: [{ + loader: 'awesome-typescript-loader', + query: { + useForkChecker: true, + tsconfig: path.resolve(appRoot, appConfig.tsconfig) + } + }, { + loader: 'angular2-template-loader' + }], + exclude: [/\.(spec|e2e)\.ts$/] + } + ], + }, + plugins: [ + new webpack.ContextReplacementPlugin(/.*/, appRoot, lazyModules), + new atl.ForkCheckerPlugin(), + ] + }; +}; + +export const getWebpackAotConfigPartial = function(projectRoot: string, appConfig: any) { + return { + module: { + rules: [ + { + test: /\.ts$/, + loader: webpackLoader, + exclude: [/\.(spec|e2e)\.ts$/] + } + ] + }, + plugins: [ + new NgcWebpackPlugin({ + project: path.resolve(projectRoot, appConfig.root, appConfig.tsconfig), + baseDir: path.resolve(projectRoot, ''), + main: path.join(projectRoot, appConfig.root, appConfig.main), + genDir: path.resolve(projectRoot, '') + }), + ] + }; +}; diff --git a/packages/angular-cli/models/webpack-config.ts b/packages/angular-cli/models/webpack-config.ts index 85c8c90fea54..6317fef937a3 100644 --- a/packages/angular-cli/models/webpack-config.ts +++ b/packages/angular-cli/models/webpack-config.ts @@ -1,3 +1,7 @@ +import { + getWebpackAotConfigPartial, + getWebpackNonAotConfigPartial +} from './webpack-build-typescript'; const webpackMerge = require('webpack-merge'); import { CliConfig } from './config'; import { @@ -12,50 +16,54 @@ export class NgCliWebpackConfig { // TODO: When webpack2 types are finished lets replace all these any types // so this is more maintainable in the future for devs public config: any; - private devConfigPartial: any; - private prodConfigPartial: any; - private baseConfig: any; constructor( public ngCliProject: any, public target: string, public environment: string, outputDir?: string, - baseHref?: string + baseHref?: string, + isAoT = false ) { const config: CliConfig = CliConfig.fromProject(); const appConfig = config.config.apps[0]; appConfig.outDir = outputDir || appConfig.outDir; - this.baseConfig = getWebpackCommonConfig( + let baseConfig = getWebpackCommonConfig( this.ngCliProject.root, environment, appConfig, baseHref ); - this.devConfigPartial = getWebpackDevConfigPartial(this.ngCliProject.root, appConfig); - this.prodConfigPartial = getWebpackProdConfigPartial(this.ngCliProject.root, appConfig); + let targetConfigPartial = this.getTargetConfig(this.ngCliProject.root, appConfig); + const typescriptConfigPartial = isAoT + ? getWebpackAotConfigPartial(this.ngCliProject.root, appConfig) + : getWebpackNonAotConfigPartial(this.ngCliProject.root, appConfig); if (appConfig.mobile) { let mobileConfigPartial = getWebpackMobileConfigPartial(this.ngCliProject.root, appConfig); let mobileProdConfigPartial = getWebpackMobileProdConfigPartial(this.ngCliProject.root, appConfig); - this.baseConfig = webpackMerge(this.baseConfig, mobileConfigPartial); - this.prodConfigPartial = webpackMerge(this.prodConfigPartial, mobileProdConfigPartial); + baseConfig = webpackMerge(baseConfig, mobileConfigPartial); + if (this.target == 'production') { + targetConfigPartial = webpackMerge(targetConfigPartial, mobileProdConfigPartial); + } } - this.generateConfig(); + this.config = webpackMerge( + baseConfig, + targetConfigPartial, + typescriptConfigPartial + ); } - generateConfig(): void { + getTargetConfig(projectRoot: string, appConfig: any): any { switch (this.target) { case 'development': - this.config = webpackMerge(this.baseConfig, this.devConfigPartial); - break; + return getWebpackDevConfigPartial(projectRoot, appConfig); case 'production': - this.config = webpackMerge(this.baseConfig, this.prodConfigPartial); - break; + return getWebpackProdConfigPartial(projectRoot, appConfig); default: throw new Error("Invalid build target. Only 'development' and 'production' are available."); } diff --git a/packages/angular-cli/package.json b/packages/angular-cli/package.json index 3ce25ae89fa4..15f55fd89a29 100644 --- a/packages/angular-cli/package.json +++ b/packages/angular-cli/package.json @@ -1,6 +1,6 @@ { "name": "angular-cli", - "version": "1.0.0-beta.15", + "version": "1.0.0-beta.16", "description": "CLI tool for Angular", "main": "lib/cli/index.js", "trackingCode": "UA-8594346-19", @@ -33,11 +33,12 @@ "@angular/platform-browser": "^2.0.0", "@angular/platform-server": "^2.0.0", "@angular/tsc-wrapped": "^0.3.0", + "@ngtools/webpack": "latest", "angular2-template-loader": "^0.5.0", "awesome-typescript-loader": "^2.2.3", "chalk": "^1.1.3", "common-tags": "^1.3.1", - "compression-webpack-plugin": "^0.3.1", + "compression-webpack-plugin": "github:webpack/compression-webpack-plugin#7e55907cd54a2e91b96d25a660acc6a2a6453f54", "copy-webpack-plugin": "^3.0.1", "core-js": "^2.4.0", "css-loader": "^0.23.1", @@ -53,6 +54,7 @@ "fs.realpath": "^1.0.0", "glob": "^7.0.3", "handlebars": "^4.0.5", + "html-loader": "^0.4.4", "html-webpack-plugin": "^2.19.0", "istanbul-instrumenter-loader": "^0.2.0", "json-loader": "^0.5.4", @@ -91,7 +93,7 @@ "typedoc": "^0.4.2", "typescript": "2.0.2", "url-loader": "^0.5.7", - "webpack": "2.1.0-beta.22", + "webpack": "2.1.0-beta.25", "webpack-dev-server": "2.1.0-beta.3", "webpack-md5-hash": "0.0.5", "webpack-merge": "^0.14.0", diff --git a/packages/angular-cli/plugins/karma.js b/packages/angular-cli/plugins/karma.js index cfd414212116..1c17e28e3771 100644 --- a/packages/angular-cli/plugins/karma.js +++ b/packages/angular-cli/plugins/karma.js @@ -12,8 +12,8 @@ const init = (config) => { const environment = config.angularCli.environment || 'dev'; // add webpack config - config.webpack = getWebpackTestConfig(config.basePath, environment, appConfig); - config.webpackMiddleware = { + const webpackConfig = getWebpackTestConfig(config.basePath, environment, appConfig); + const webpackMiddlewareConfig = { noInfo: true, // Hide webpack output because its noisy. stats: { // Also prevent chunk and module display output, cleaner look. Only emit errors. assets: false, @@ -25,6 +25,8 @@ const init = (config) => { chunkModules: false } }; + config.webpack = Object.assign(webpackConfig, config.webpack); + config.webpackMiddleware = Object.assign(webpackMiddlewareConfig, config.webpackMiddleware); // replace the angular-cli preprocessor with webpack+sourcemap Object.keys(config.preprocessors) diff --git a/packages/angular-cli/tasks/build-webpack-watch.ts b/packages/angular-cli/tasks/build-webpack-watch.ts index a85d4f1112c5..008168dac399 100644 --- a/packages/angular-cli/tasks/build-webpack-watch.ts +++ b/packages/angular-cli/tasks/build-webpack-watch.ts @@ -21,7 +21,8 @@ export default Task.extend({ runTaskOptions.target, runTaskOptions.environment, runTaskOptions.outputPath, - runTaskOptions.baseHref + runTaskOptions.baseHref, + runTaskOptions.aot ).config; const webpackCompiler: any = webpack(config); diff --git a/packages/angular-cli/tasks/build-webpack.ts b/packages/angular-cli/tasks/build-webpack.ts index 49abc0687b57..80fc0b9e62a5 100644 --- a/packages/angular-cli/tasks/build-webpack.ts +++ b/packages/angular-cli/tasks/build-webpack.ts @@ -5,25 +5,30 @@ import * as webpack from 'webpack'; import { BuildOptions } from '../commands/build'; import { NgCliWebpackConfig } from '../models/webpack-config'; import { webpackOutputOptions } from '../models/'; +import { CliConfig } from '../models/config'; // Configure build and output; let lastHash: any = null; export default Task.extend({ - // Options: String outputPath run: function (runTaskOptions: BuildOptions) { const project = this.cliProject; - rimraf.sync(path.resolve(project.root, runTaskOptions.outputPath)); + const outputDir = runTaskOptions.outputPath || CliConfig.fromProject().config.apps[0].outDir; + rimraf.sync(path.resolve(project.root, outputDir)); const config = new NgCliWebpackConfig( project, runTaskOptions.target, runTaskOptions.environment, - runTaskOptions.outputPath, - runTaskOptions.baseHref + outputDir, + runTaskOptions.baseHref, + runTaskOptions.aot ).config; + // fail on build error + config.bail = true; + const webpackCompiler: any = webpack(config); const ProgressPlugin = require('webpack/lib/ProgressPlugin'); @@ -40,11 +45,8 @@ export default Task.extend({ if (err) { lastHash = null; - console.error(err.stack || err); - if (err.details) { - console.error(err.details); - } - reject(err.details); + console.error(err.details || err); + reject(err.details || err); } if (stats.hash !== lastHash) { diff --git a/packages/angular-cli/tasks/serve-webpack.ts b/packages/angular-cli/tasks/serve-webpack.ts index 4dfb1dffff1f..1bb0906ad2df 100644 --- a/packages/angular-cli/tasks/serve-webpack.ts +++ b/packages/angular-cli/tasks/serve-webpack.ts @@ -13,14 +13,19 @@ import { CliConfig } from '../models/config'; import { oneLine } from 'common-tags'; export default Task.extend({ - run: function(commandOptions: ServeTaskOptions) { + run: function (commandOptions: ServeTaskOptions) { const ui = this.ui; let webpackCompiler: any; let config = new NgCliWebpackConfig( - this.project, commandOptions.target, - commandOptions.environment + this.project, + commandOptions.target, + commandOptions.environment, + undefined, + undefined, + commandOptions.aot, + ).config; // This allows for live reload of page when changes are made to repo. @@ -46,6 +51,20 @@ export default Task.extend({ } } + let sslKey: string = null; + let sslCert: string = null; + + if (commandOptions.ssl) { + const keyPath = path.resolve(this.project.root, commandOptions.sslKey); + if (fs.existsSync(keyPath)) { + sslKey = fs.readFileSync(keyPath, 'utf-8'); + } + const certPath = path.resolve(this.project.root, commandOptions.sslCert); + if (fs.existsSync(certPath)) { + sslCert = fs.readFileSync(certPath, 'utf-8'); + } + } + const webpackDevServerConfiguration: IWebpackDevServerConfigurationOptions = { contentBase: path.resolve( this.project.root, @@ -54,19 +73,28 @@ export default Task.extend({ historyApiFallback: true, stats: webpackDevServerOutputOptions, inline: true, - proxy: proxyConfig + https: commandOptions.ssl, + proxy: proxyConfig, + watchOptions: { + poll: CliConfig.fromProject().config.defaults.poll + } }; + if (sslKey != null && sslCert != null) { + webpackDevServerConfiguration.key = sslKey; + webpackDevServerConfiguration.cert = sslCert; + } + ui.writeLine(chalk.green(oneLine` ** NG Live Development Server is running on - http://${commandOptions.host}:${commandOptions.port}. + http${commandOptions.ssl ? 's' : ''}://${commandOptions.host}:${commandOptions.port}. ** `)); const server = new WebpackDevServer(webpackCompiler, webpackDevServerConfiguration); return new Promise((resolve, reject) => { - server.listen(commandOptions.port, `${commandOptions.host}`, function(err: any, stats: any) { + server.listen(commandOptions.port, `${commandOptions.host}`, function (err: any, stats: any) { if (err) { console.error(err.stack || err); if (err.details) { console.error(err.details); } diff --git a/packages/angular-cli/tsconfig.json b/packages/angular-cli/tsconfig.json index 0e08554fd4c8..9623f183e150 100644 --- a/packages/angular-cli/tsconfig.json +++ b/packages/angular-cli/tsconfig.json @@ -8,22 +8,26 @@ "moduleResolution": "node", "noEmitOnError": true, "noImplicitAny": true, - "outDir": "../../dist/", - "rootDir": "..", + "outDir": "../../dist/angular-cli", + "rootDir": ".", "sourceMap": true, "sourceRoot": "/", "target": "es5", "lib": ["es6"], + "skipLibCheck": true, "typeRoots": [ "../../node_modules/@types" ], "baseUrl": "", "paths": { - "@angular-cli/ast-tools": [ "../../packages/ast-tools/src" ], - "@angular-cli/base-href-webpack": [ "../../packages/base-href-webpack/src" ], - "@angular-cli/webpack": [ "../../packages/webpack/src" ] + "@angular-cli/ast-tools": [ "../../dist/ast-tools/src" ], + "@angular-cli/base-href-webpack": [ "../../dist/base-href-webpack/src" ], + "@ngtools/webpack": [ "../../dist/webpack/src" ] } }, + "include": [ + "**/*" + ], "exclude": [ "blueprints/*/files/**/*" ] diff --git a/packages/angular-cli/utilities/completion.sh b/packages/angular-cli/utilities/completion.sh index bcb16f28b879..81ff665a793b 100644 --- a/packages/angular-cli/utilities/completion.sh +++ b/packages/angular-cli/utilities/completion.sh @@ -7,15 +7,15 @@ ng_opts='b build completion doc e2e g generate get github-pages:deploy gh-pages:deploy h help i init install lint make-this-awesome new s serve server set t test v version -h --help' -build_opts='--environment --output-path --suppress-sizes --target --watch --watcher -dev -e -prod' -generate_opts='class component directive enum module pipe route service --generate -d --dry-run --verbose -v --pod -p --classic -c --dummy -dum -id --in-repo --in-repo-addon -ir' -github_pages_deploy_opts='--environment --gh-token --gh-username --skip-build --user-page --message' +build_opts='--aot --base-href --environment --output-path --suppress-sizes --target --watch --watcher -bh -dev -e -o -prod -t -w' +generate_opts='class component directive enum module pipe route service c cl d e m p r s --help' +github_pages_deploy_opts='--base-href --environment --gh-token --gh-username --message --skip-build --target --user-page -bh -e -t' help_opts='--json --verbose -v' -init_opts='--blueprint --dry-run --link-cli --mobile --name --prefix --skip-bower --skip-npm --source-dir --style --verbose -b -d -lc -n -p -sb -sd -sn -v' -new_opts='--blueprint --directory --dry-run --link-cli --mobile --prefix --skip-bower --skip-git --skip-npm --source-dir --style --verbose -b -d -dir -lc -p -sb -sd -sg -sn -v' -serve_opts='--environment --host --insecure-proxy --inspr --live-reload --live-reload-base-url --live-reload-host --live-reload-live-css --live-reload-port --output-path --port --proxy --ssl --ssl-cert --ssl-key --target --watcher -H -dev -e -lr -lrbu -lrh -lrp -op -out -p -pr -prod -pxy -t -w' +init_opts='--dry-run inline-style inline-template --link-cli --mobile --name --prefix --routing --skip-bower --skip-npm --source-dir --style --verbose -d -is -it -lc -n -p -sb -sd -sn -v' +new_opts='--directory --dry-run inline-style inline-template --link-cli --mobile --prefix --routing --skip-bower --skip-git --skip-npm --source-dir --style --verbose -d -dir -is -it -lc -p -sb -sd -sg -sn -v' +serve_opts='--aot --environment --host --live-reload --live-reload-base-url --live-reload-host --live-reload-live-css --live-reload-port --port --proxy-config --ssl --ssl-cert --ssl-key --target --watcher -H -e -lr -lrbu -lrh -lrp -p -pc -t -w' set_opts='--global -g' -test_opts='--browsers --colors --config-file --environment --filter --host --launch --log-level --module --path --port --query --reporter --server --silent --test-page --test-port --watch -H -c -cf -e -f -m -r -s -tp -w' +test_opts='--browsers --build --colors --log-level --port --reporters --watch -w' version_opts='--verbose' if type complete &>/dev/null; then @@ -30,6 +30,7 @@ if type complete &>/dev/null; then ng) opts=$ng_opts ;; b|build) opts=$build_opts ;; g|generate) opts=$generate_opts ;; + gh-pages:deploy|github-pages:deploy) opts=$github_pages_deploy_opts h|help|-h|--help) opts=$help_opts ;; init) opts=$init_opts ;; new) opts=$new_opts ;; @@ -57,6 +58,7 @@ elif type compctl &>/dev/null; then ng) opts=$ng_opts ;; b|build) opts=$build_opts ;; g|generate) opts=$generate_opts ;; + gh-pages:deploy|github-pages:deploy) opts=$github_pages_deploy_opts h|help|-h|--help) opts=$help_opts ;; init) opts=$init_opts ;; new) opts=$new_opts ;; @@ -75,4 +77,4 @@ elif type compctl &>/dev/null; then compctl -K _ng_completion ng fi -###-end-ng-completion### \ No newline at end of file +###-end-ng-completion### diff --git a/packages/angular-cli/utilities/find-parent-module.ts b/packages/angular-cli/utilities/find-parent-module.ts index 7648560ad5ca..7a2273e19068 100644 --- a/packages/angular-cli/utilities/find-parent-module.ts +++ b/packages/angular-cli/utilities/find-parent-module.ts @@ -11,10 +11,9 @@ export default function findParentModule(project: any, currentDir: string): stri let pathToCheck = path.join(sourceRoot, currentDir); while (pathToCheck.length >= sourceRoot.length) { - // let files: string[] = fs.readdirSync(pathToCheck); - - // files = files.filter(file => file.indexOf('.module.ts') > 0); + // TODO: refactor to not be based upon file name const files = fs.readdirSync(pathToCheck) + .filter(fileName => !fileName.endsWith('routing.module.ts')) .filter(fileName => fileName.endsWith('.module.ts')) .filter(fileName => fs.statSync(path.join(pathToCheck, fileName)).isFile()); diff --git a/packages/angular-cli/utilities/module-resolver.ts b/packages/angular-cli/utilities/module-resolver.ts index 3b889116b53d..020b25a11389 100644 --- a/packages/angular-cli/utilities/module-resolver.ts +++ b/packages/angular-cli/utilities/module-resolver.ts @@ -5,7 +5,8 @@ import * as ts from 'typescript'; import * as dependentFilesUtils from './get-dependent-files'; -import { Change, ReplaceChange } from './change'; +import {Change, ReplaceChange} from './change'; +import {NodeHost, Host} from '@angular-cli/ast-tools'; /** * Rewrites import module of dependent files when the file is moved. @@ -21,13 +22,14 @@ export class ModuleResolver { * then apply() method is called sequentially. * * @param changes {Change []} + * @param host {Host} * @return Promise after all apply() method of Change class is called * to all Change instances sequentially. */ - applySortedChangePromise(changes: Change[]): Promise { + applySortedChangePromise(changes: Change[], host: Host = NodeHost): Promise { return changes .sort((currentChange, nextChange) => nextChange.order - currentChange.order) - .reduce((newChange, change) => newChange.then(() => change.apply()), Promise.resolve()); + .reduce((newChange, change) => newChange.then(() => change.apply(host)), Promise.resolve()); } /** diff --git a/packages/ast-tools/package.json b/packages/ast-tools/package.json index cb551bfe1586..6d7cec56b0f3 100644 --- a/packages/ast-tools/package.json +++ b/packages/ast-tools/package.json @@ -1,6 +1,6 @@ { "name": "@angular-cli/ast-tools", - "version": "1.0.1", + "version": "1.0.2", "description": "CLI tool for Angular", "main": "./src/index.js", "keywords": [ diff --git a/packages/ast-tools/src/ast-utils.spec.ts b/packages/ast-tools/src/ast-utils.spec.ts index 001ff9a9beab..17c0444e417b 100644 --- a/packages/ast-tools/src/ast-utils.spec.ts +++ b/packages/ast-tools/src/ast-utils.spec.ts @@ -3,7 +3,7 @@ import mockFs = require('mock-fs'); import ts = require('typescript'); import fs = require('fs'); -import {InsertChange, RemoveChange} from './change'; +import {InsertChange, NodeHost, RemoveChange} from './change'; import {insertAfterLastOccurrence, addDeclarationToModule} from './ast-utils'; import {findNodes} from './node'; import {it} from './spec-utils'; @@ -31,7 +31,7 @@ describe('ast-utils: findNodes', () => { it('finds no imports', () => { let editedFile = new RemoveChange(sourceFile, 0, `import * as myTest from 'tests' \n`); return editedFile - .apply() + .apply(NodeHost) .then(() => { let rootNode = getRootNode(sourceFile); let nodes = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration); @@ -47,10 +47,10 @@ describe('ast-utils: findNodes', () => { // remove new line and add an inline import let editedFile = new RemoveChange(sourceFile, 32, '\n'); return editedFile - .apply() + .apply(NodeHost) .then(() => { let insert = new InsertChange(sourceFile, 32, `import {Routes} from '@angular/routes'`); - return insert.apply(); + return insert.apply(NodeHost); }) .then(() => { let rootNode = getRootNode(sourceFile); @@ -61,7 +61,7 @@ describe('ast-utils: findNodes', () => { it('finds two imports from new line separated declarations', () => { let editedFile = new InsertChange(sourceFile, 33, `import {Routes} from '@angular/routes'`); return editedFile - .apply() + .apply(NodeHost) .then(() => { let rootNode = getRootNode(sourceFile); let nodes = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration); @@ -89,7 +89,7 @@ describe('ast-utils: insertAfterLastOccurrence', () => { let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile); return insertAfterLastOccurrence(imports, `\nimport { Router } from '@angular/router';`, sourceFile, 0) - .apply() + .apply(NodeHost) .then(() => { return readFile(sourceFile, 'utf8'); }).then((content) => { @@ -106,12 +106,12 @@ describe('ast-utils: insertAfterLastOccurrence', () => { let content = `import { foo, bar } from 'fizz';`; let editedFile = new InsertChange(sourceFile, 0, content); return editedFile - .apply() + .apply(NodeHost) .then(() => { let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile); return insertAfterLastOccurrence(imports, ', baz', sourceFile, 0, ts.SyntaxKind.Identifier) - .apply(); + .apply(NodeHost); }) .then(() => { return readFile(sourceFile, 'utf8'); @@ -122,12 +122,12 @@ describe('ast-utils: insertAfterLastOccurrence', () => { let content = `import * from 'foo' \n import { bar } from 'baz'`; let editedFile = new InsertChange(sourceFile, 0, content); return editedFile - .apply() + .apply(NodeHost) .then(() => { let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile); return insertAfterLastOccurrence(imports, `\nimport Router from '@angular/router'`, sourceFile) - .apply(); + .apply(NodeHost); }) .then(() => { return readFile(sourceFile, 'utf8'); @@ -142,12 +142,12 @@ describe('ast-utils: insertAfterLastOccurrence', () => { let content = `import {} from 'foo'`; let editedFile = new InsertChange(sourceFile, 0, content); return editedFile - .apply() + .apply(NodeHost) .then(() => { let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile); return insertAfterLastOccurrence(imports, ', bar', sourceFile, undefined, ts.SyntaxKind.Identifier) - .apply(); + .apply(NodeHost); }) .catch(() => { return readFile(sourceFile, 'utf8'); @@ -160,7 +160,7 @@ describe('ast-utils: insertAfterLastOccurrence', () => { ts.SyntaxKind.CloseBraceToken).pop().pos; return insertAfterLastOccurrence(imports, ' bar ', sourceFile, pos, ts.SyntaxKind.Identifier) - .apply(); + .apply(NodeHost); }) .then(() => { return readFile(sourceFile, 'utf8'); @@ -211,7 +211,7 @@ class Module {}` it('works with empty array', () => { return addDeclarationToModule('1.ts', 'MyClass', 'MyImportPath') - .then(change => change.apply()) + .then(change => change.apply(NodeHost)) .then(() => readFile('1.ts', 'utf-8')) .then(content => { expect(content).toEqual( @@ -229,7 +229,7 @@ class Module {}` it('works with array with declarations', () => { return addDeclarationToModule('2.ts', 'MyClass', 'MyImportPath') - .then(change => change.apply()) + .then(change => change.apply(NodeHost)) .then(() => readFile('2.ts', 'utf-8')) .then(content => { expect(content).toEqual( @@ -250,7 +250,7 @@ class Module {}` it('works without any declarations', () => { return addDeclarationToModule('3.ts', 'MyClass', 'MyImportPath') - .then(change => change.apply()) + .then(change => change.apply(NodeHost)) .then(() => readFile('3.ts', 'utf-8')) .then(content => { expect(content).toEqual( @@ -268,7 +268,7 @@ class Module {}` it('works without a declaration field', () => { return addDeclarationToModule('4.ts', 'MyClass', 'MyImportPath') - .then(change => change.apply()) + .then(change => change.apply(NodeHost)) .then(() => readFile('4.ts', 'utf-8')) .then(content => { expect(content).toEqual( diff --git a/packages/ast-tools/src/ast-utils.ts b/packages/ast-tools/src/ast-utils.ts index 41fd8664c764..8d0980102116 100644 --- a/packages/ast-tools/src/ast-utils.ts +++ b/packages/ast-tools/src/ast-utils.ts @@ -222,8 +222,8 @@ function _addSymbolToNgModuleMetadata(ngModulePath: string, metadataField: strin 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}]`; + if (text.match('^\r?\r?\n')) { + toInsert = `,${text.match(/^\r?\n\s+/)[0]}${metadataField}: [${symbolName}]`; } else { toInsert = `, ${metadataField}: [${symbolName}]`; } @@ -235,8 +235,8 @@ function _addSymbolToNgModuleMetadata(ngModulePath: string, metadataField: strin } 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}`; + if (text.match(/^\r?\n/)) { + toInsert = `,${text.match(/^\r?\n(\r?)\s+/)[0]}${symbolName}`; } else { toInsert = `, ${symbolName}`; } diff --git a/packages/ast-tools/src/change.spec.ts b/packages/ast-tools/src/change.spec.ts index d4abb85d5aad..0bebb3230169 100644 --- a/packages/ast-tools/src/change.spec.ts +++ b/packages/ast-tools/src/change.spec.ts @@ -4,7 +4,7 @@ let mockFs = require('mock-fs'); import {it} from './spec-utils'; -import {InsertChange, RemoveChange, ReplaceChange} from './change'; +import {InsertChange, NodeHost, RemoveChange, ReplaceChange} from './change'; import fs = require('fs'); let path = require('path'); @@ -35,7 +35,7 @@ describe('Change', () => { it('adds text to the source code', () => { let changeInstance = new InsertChange(sourceFile, 6, ' world!'); return changeInstance - .apply() + .apply(NodeHost) .then(() => readFile(sourceFile, 'utf8')) .then(contents => { expect(contents).toEqual('hello world!'); @@ -47,7 +47,7 @@ describe('Change', () => { it('adds nothing in the source code if empty string is inserted', () => { let changeInstance = new InsertChange(sourceFile, 6, ''); return changeInstance - .apply() + .apply(NodeHost) .then(() => readFile(sourceFile, 'utf8')) .then(contents => { expect(contents).toEqual('hello'); @@ -61,7 +61,7 @@ describe('Change', () => { it('removes given text from the source code', () => { let changeInstance = new RemoveChange(sourceFile, 9, 'as foo'); return changeInstance - .apply() + .apply(NodeHost) .then(() => readFile(sourceFile, 'utf8')) .then(contents => { expect(contents).toEqual('import * from "./bar"'); @@ -73,7 +73,7 @@ describe('Change', () => { it('does not change the file if told to remove empty string', () => { let changeInstance = new RemoveChange(sourceFile, 9, ''); return changeInstance - .apply() + .apply(NodeHost) .then(() => readFile(sourceFile, 'utf8')) .then(contents => { expect(contents).toEqual('import * as foo from "./bar"'); @@ -86,7 +86,7 @@ describe('Change', () => { let sourceFile = path.join(sourcePath, 'remove-replace-file.txt'); let changeInstance = new ReplaceChange(sourceFile, 7, '* as foo', '{ fooComponent }'); return changeInstance - .apply() + .apply(NodeHost) .then(() => readFile(sourceFile, 'utf8')) .then(contents => { expect(contents).toEqual('import { fooComponent } from "./bar"'); @@ -96,11 +96,22 @@ describe('Change', () => { let sourceFile = path.join(sourcePath, 'remove-replace-file.txt'); expect(() => new ReplaceChange(sourceFile, -6, 'hello', ' world!')).toThrow(); }); + it('fails for invalid replacement', () => { + let sourceFile = path.join(sourcePath, 'replace-file.txt'); + let changeInstance = new ReplaceChange(sourceFile, 0, 'foobar', ''); + return changeInstance + .apply(NodeHost) + .then(() => expect(false).toBe(true), err => { + // Check that the message contains the string to replace and the string from the file. + expect(err.message).toContain('foobar'); + expect(err.message).toContain('import'); + }); + }); it('adds string to the position of an empty string', () => { let sourceFile = path.join(sourcePath, 'replace-file.txt'); let changeInstance = new ReplaceChange(sourceFile, 9, '', 'BarComponent, '); return changeInstance - .apply() + .apply(NodeHost) .then(() => readFile(sourceFile, 'utf8')) .then(contents => { expect(contents).toEqual('import { BarComponent, FooComponent } from "./baz"'); @@ -108,9 +119,9 @@ describe('Change', () => { }); it('removes the given string only if an empty string to add is given', () => { let sourceFile = path.join(sourcePath, 'remove-replace-file.txt'); - let changeInstance = new ReplaceChange(sourceFile, 9, ' as foo', ''); + let changeInstance = new ReplaceChange(sourceFile, 8, ' as foo', ''); return changeInstance - .apply() + .apply(NodeHost) .then(() => readFile(sourceFile, 'utf8')) .then(contents => { expect(contents).toEqual('import * from "./bar"'); diff --git a/packages/ast-tools/src/change.ts b/packages/ast-tools/src/change.ts index dde0bc5523a4..14e537575b3c 100644 --- a/packages/ast-tools/src/change.ts +++ b/packages/ast-tools/src/change.ts @@ -4,8 +4,19 @@ import denodeify = require('denodeify'); const readFile = (denodeify(fs.readFile) as (...args: any[]) => Promise); const writeFile = (denodeify(fs.writeFile) as (...args: any[]) => Promise); +export interface Host { + write(path: string, content: string): Promise; + read(path: string): Promise; +} + +export const NodeHost: Host = { + write: (path: string, content: string) => writeFile(path, content, 'utf8'), + read: (path: string) => readFile(path, 'utf8') +}; + + export interface Change { - apply(): Promise; + apply(host: Host): Promise; // The file this change should be applied to. Some changes might not apply to // a file (maybe the config). @@ -61,11 +72,11 @@ export class MultiChange implements Change { get order() { return Math.max(...this._changes.map(c => c.order)); } get path() { return this._path; } - apply() { + apply(host: Host) { return this._changes .sort((a: Change, b: Change) => b.order - a.order) .reduce((promise, change) => { - return promise.then(() => change.apply()); + return promise.then(() => change.apply(host)); }, Promise.resolve()); } } @@ -90,11 +101,11 @@ export class InsertChange implements Change { /** * This method does not insert spaces if there is none in the original string. */ - apply(): Promise { - return readFile(this.path, 'utf8').then(content => { + apply(host: Host): Promise { + return host.read(this.path).then(content => { let prefix = content.substring(0, this.pos); let suffix = content.substring(this.pos); - return writeFile(this.path, `${prefix}${this.toAdd}${suffix}`); + return host.write(this.path, `${prefix}${this.toAdd}${suffix}`); }); } } @@ -115,12 +126,12 @@ export class RemoveChange implements Change { this.order = pos; } - apply(): Promise { - return readFile(this.path, 'utf8').then(content => { + apply(host: Host): Promise { + return host.read(this.path).then(content => { let prefix = content.substring(0, this.pos); let suffix = content.substring(this.pos + this.toRemove.length); // TODO: throw error if toRemove doesn't match removed string. - return writeFile(this.path, `${prefix}${suffix}`); + return host.write(this.path, `${prefix}${suffix}`); }); } } @@ -141,12 +152,17 @@ export class ReplaceChange implements Change { this.order = pos; } - apply(): Promise { - return readFile(this.path, 'utf8').then(content => { - let prefix = content.substring(0, this.pos); - let suffix = content.substring(this.pos + this.oldText.length); + apply(host: Host): Promise { + return host.read(this.path).then(content => { + const prefix = content.substring(0, this.pos); + const suffix = content.substring(this.pos + this.oldText.length); + const text = content.substring(this.pos, this.pos + this.oldText.length); + + if (text !== this.oldText) { + return Promise.reject(new Error(`Invalid replace: "${text}" != "${this.oldText}".`)); + } // TODO: throw error if oldText doesn't match removed string. - return writeFile(this.path, `${prefix}${this.newText}${suffix}`); + return host.write(this.path, `${prefix}${this.newText}${suffix}`); }); } } diff --git a/packages/ast-tools/src/route-utils.spec.ts b/packages/ast-tools/src/route-utils.spec.ts index fb221f20013c..4405742a35f4 100644 --- a/packages/ast-tools/src/route-utils.spec.ts +++ b/packages/ast-tools/src/route-utils.spec.ts @@ -2,7 +2,7 @@ import * as mockFs from 'mock-fs'; import * as fs from 'fs'; import * as nru from './route-utils'; import * as path from 'path'; -import { InsertChange, RemoveChange } from './change'; +import { NodeHost, InsertChange, RemoveChange } from './change'; import denodeify = require('denodeify'); import * as _ from 'lodash'; import {it} from './spec-utils'; @@ -29,8 +29,8 @@ describe('route utils', () => { 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()) + return editedFile.apply(NodeHost) + .then(() => nru.insertImport(sourceFile, 'Router', '@angular/router').apply(NodeHost)) .then(() => readFile(sourceFile, 'utf8')) .then(newContent => { expect(newContent).toEqual(content + `\nimport { Router } from '@angular/router';`); @@ -39,7 +39,7 @@ describe('route utils', () => { 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() + return editedFile.apply(NodeHost) .then(() => nru.insertImport(sourceFile, 'Router', '@angular/router')) .then(() => readFile(sourceFile, 'utf8')) .then(newContent => { @@ -49,8 +49,8 @@ describe('route utils', () => { 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()) + return editedFile.apply(NodeHost) + .then(() => nru.insertImport(sourceFile, 'baz', 'fizz').apply(NodeHost)) .then(() => readFile(sourceFile, 'utf8')) .then(newContent => { expect(newContent).toEqual(`'use strict'\n import { foo, bar, baz } from 'fizz'`); @@ -59,7 +59,7 @@ describe('route utils', () => { it('understands * imports', () => { let content = `\nimport * as myTest from 'tests' \n`; let editedFile = new InsertChange(sourceFile, 0, content); - return editedFile.apply() + return editedFile.apply(NodeHost) .then(() => nru.insertImport(sourceFile, 'Test', 'tests')) .then(() => readFile(sourceFile, 'utf8')) .then(newContent => { @@ -69,8 +69,8 @@ describe('route utils', () => { 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()) + return editedFile.apply(NodeHost) + .then(() => nru.insertImport(sourceFile, 'Router', '@angular/router').apply(NodeHost)) .then(() => readFile(sourceFile, 'utf8')) .then(newContent => { expect(newContent).toEqual( @@ -78,7 +78,7 @@ describe('route utils', () => { }); }); it('inserts inserts at beginning of file if no imports exist', () => { - return nru.insertImport(sourceFile, 'Router', '@angular/router').apply() + return nru.insertImport(sourceFile, 'Router', '@angular/router').apply(NodeHost) .then(() => readFile(sourceFile, 'utf8')) .then(newContent => { expect(newContent).toEqual(`import { Router } from '@angular/router';\n`); @@ -86,7 +86,7 @@ describe('route utils', () => { }); it('inserts subcomponent in win32 environment', () => { let content = './level1\\level2/level2.component'; - return nru.insertImport(sourceFile, 'level2', content).apply() + return nru.insertImport(sourceFile, 'level2', content).apply(NodeHost) .then(() => readFile(sourceFile, 'utf8')) .then(newContent => { if (process.platform.startsWith('win')) { @@ -132,7 +132,7 @@ describe('route utils', () => { }); }); xit('does not add a provideRouter import if it exits already', () => { - return nru.insertImport(mainFile, 'provideRouter', '@angular/router').apply() + return nru.insertImport(mainFile, 'provideRouter', '@angular/router').apply(NodeHost) .then(() => nru.applyChanges(nru.bootstrapItem(mainFile, routes, toBootstrap))) .then(() => readFile(mainFile, 'utf8')) .then(content => { @@ -145,7 +145,7 @@ describe('route utils', () => { xit('does not duplicate import to route.ts ', () => { let editedFile = new InsertChange(mainFile, 100, `\nimport routes from './routes';`); return editedFile - .apply() + .apply(NodeHost) .then(() => nru.applyChanges(nru.bootstrapItem(mainFile, routes, toBootstrap))) .then(() => readFile(mainFile, 'utf8')) .then(content => { @@ -163,7 +163,7 @@ describe('route utils', () => { }); it('adds provideRouter to bootstrap if absent and empty providers array', () => { let editFile = new InsertChange(mainFile, 124, ', []'); - return editFile.apply() + return editFile.apply(NodeHost) .then(() => nru.applyChanges(nru.bootstrapItem(mainFile, routes, toBootstrap))) .then(() => readFile(mainFile, 'utf8')) .then(content => { @@ -173,7 +173,7 @@ describe('route utils', () => { }); it('adds provideRouter to bootstrap if absent and non-empty providers array', () => { let editedFile = new InsertChange(mainFile, 124, ', [ HTTP_PROVIDERS ]'); - return editedFile.apply() + return editedFile.apply(NodeHost) .then(() => nru.applyChanges(nru.bootstrapItem(mainFile, routes, toBootstrap))) .then(() => readFile(mainFile, 'utf8')) .then(content => { @@ -185,7 +185,7 @@ describe('route utils', () => { let editedFile = new InsertChange(mainFile, 124, ', [ HTTP_PROVIDERS, provideRouter(routes) ]'); - return editedFile.apply() + return editedFile.apply(NodeHost) .then(() => nru.applyChanges(nru.bootstrapItem(mainFile, routes, toBootstrap))) .then(() => readFile(mainFile, 'utf8')) .then(content => { @@ -195,7 +195,7 @@ describe('route utils', () => { }); it('inserts into the correct array', () => { let editedFile = new InsertChange(mainFile, 124, ', [ HTTP_PROVIDERS, {provide: [BAR]}]'); - return editedFile.apply() + return editedFile.apply(NodeHost) .then(() => nru.applyChanges(nru.bootstrapItem(mainFile, routes, toBootstrap))) .then(() => readFile(mainFile, 'utf8')) .then(content => { @@ -205,7 +205,7 @@ describe('route utils', () => { }); it('throws an error if there is no or multiple bootstrap expressions', () => { let editedFile = new InsertChange(mainFile, 126, '\n bootstrap(moreStuff);'); - return editedFile.apply() + return editedFile.apply(NodeHost) .then(() => nru.bootstrapItem(mainFile, routes, toBootstrap)) .catch(e => expect(e.message).toEqual('Did not bootstrap provideRouter in' + @@ -214,7 +214,7 @@ describe('route utils', () => { }); 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() + return editedFile.apply(NodeHost) .then(() => nru.applyChanges(nru.bootstrapItem(mainFile, routes, toBootstrap))) .then(() => readFile(mainFile, 'utf8')) .then(content => { @@ -262,7 +262,7 @@ 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 editedFile.apply(NodeHost).then(() => { return nru.addPathToRoutes(routesFile, _.merge({route: 'new-route'}, options)); }).catch(e => { expect(e.message).toEqual('Did not insert path in routes.ts because ' @@ -271,7 +271,7 @@ export default [\n { path: 'new-route', component: NewRouteComponent }\n];`); }); it('throws error if no export defaults exists', () => { let editedFile = new RemoveChange(routesFile, 0, 'export default []'); - return editedFile.apply().then(() => { + return editedFile.apply(NodeHost).then(() => { return nru.addPathToRoutes(routesFile, _.merge({route: 'new-route'}, options)); }).catch(e => { expect(e.message).toEqual('Did not insert path in routes.ts because ' @@ -281,7 +281,7 @@ export default [\n { path: 'new-route', component: NewRouteComponent }\n];`); it('treats positional params correctly', () => { let editedFile = new InsertChange(routesFile, 16, `\n { path: 'home', component: HomeComponent }\n`); - return editedFile.apply().then(() => { + return editedFile.apply(NodeHost).then(() => { options.dasherizedName = 'about'; options.component = 'AboutComponent'; return nru.applyChanges( @@ -293,13 +293,13 @@ export default [\n { path: 'new-route', component: NewRouteComponent }\n];`); `\nexport default [\n` + ` { path: 'home', component: HomeComponent,\n` + ` children: [\n` + - ` { path: 'about/:id', component: AboutComponent } ` + + ` { path: 'about/:id', component: AboutComponent }` + `\n ]\n }\n];`); }); }); it('inserts under parent, mid', () => { let editedFile = new InsertChange(routesFile, 16, nestedRoutes); - return editedFile.apply().then(() => { + return editedFile.apply(NodeHost).then(() => { options.dasherizedName = 'details'; options.component = 'DetailsComponent'; return nru.applyChanges( @@ -324,7 +324,7 @@ export default [ }); it('inserts under parent, deep', () => { let editedFile = new InsertChange(routesFile, 16, nestedRoutes); - return editedFile.apply().then(() => { + return editedFile.apply(NodeHost).then(() => { options.dasherizedName = 'sections'; options.component = 'SectionsComponent'; return nru.applyChanges( @@ -360,7 +360,7 @@ export default [ ] }\n`; let editedFile = new InsertChange(routesFile, 16, paths); - return editedFile.apply().then(() => { + return editedFile.apply(NodeHost).then(() => { options.dasherizedName = 'about'; options.component = 'AboutComponent_1'; return nru.applyChanges( @@ -383,7 +383,7 @@ export default [ }); it('throws error if repeating child, shallow', () => { let editedFile = new InsertChange(routesFile, 16, nestedRoutes); - return editedFile.apply().then(() => { + return editedFile.apply(NodeHost).then(() => { options.dasherizedName = 'home'; options.component = 'HomeComponent'; return nru.addPathToRoutes(routesFile, _.merge({route: '/home'}, options)); @@ -393,7 +393,7 @@ export default [ }); it('throws error if repeating child, mid', () => { let editedFile = new InsertChange(routesFile, 16, nestedRoutes); - return editedFile.apply().then(() => { + return editedFile.apply(NodeHost).then(() => { options.dasherizedName = 'about'; options.component = 'AboutComponent'; return nru.addPathToRoutes(routesFile, _.merge({route: 'home/about/'}, options)); @@ -403,7 +403,7 @@ export default [ }); it('throws error if repeating child, deep', () => { let editedFile = new InsertChange(routesFile, 16, nestedRoutes); - return editedFile.apply().then(() => { + return editedFile.apply(NodeHost).then(() => { options.dasherizedName = 'more'; options.component = 'MoreComponent'; return nru.addPathToRoutes(routesFile, _.merge({route: 'home/about/more'}, options)); @@ -413,7 +413,7 @@ export default [ }); it('does not report false repeat', () => { let editedFile = new InsertChange(routesFile, 16, nestedRoutes); - return editedFile.apply().then(() => { + return editedFile.apply(NodeHost).then(() => { options.dasherizedName = 'more'; options.component = 'MoreComponent'; return nru.applyChanges(nru.addPathToRoutes(routesFile, _.merge({route: 'more'}, options))); @@ -448,7 +448,7 @@ export default [ },\n { path: 'trap-queen', component: TrapQueenComponent}\n`; let editedFile = new InsertChange(routesFile, 16, routes); - return editedFile.apply().then(() => { + return editedFile.apply(NodeHost).then(() => { options.dasherizedName = 'trap-queen'; options.component = 'TrapQueenComponent'; return nru.applyChanges( @@ -475,10 +475,10 @@ export default [ it('resolves imports correctly', () => { let editedFile = new InsertChange(routesFile, 16, `\n { path: 'home', component: HomeComponent }\n`); - return editedFile.apply().then(() => { + return editedFile.apply(NodeHost).then(() => { let editedFile = new InsertChange(routesFile, 0, `import { HomeComponent } from './app/home/home.component';\n`); - return editedFile.apply(); + return editedFile.apply(NodeHost); }) .then(() => { options.dasherizedName = 'home'; @@ -507,12 +507,12 @@ export default [ { path: 'details', component: DetailsComponent } ] }`); - return editedFile.apply().then(() => { + return editedFile.apply(NodeHost).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`); // tslint:disable-line - return editedFile.apply(); + return editedFile.apply(NodeHost); }).then(() => { options.dasherizedName = 'details'; options.component = 'DetailsComponent'; @@ -524,7 +524,7 @@ import { DetailsComponent as DetailsComponent_1 } from './app/about/description/ 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(() => { + return editedFile.apply(NodeHost).then(() => { let toInsert = {'home': ['canActivate', '[ MyGuard ]'] }; return nru.applyChanges(nru.addItemsToRouteProperties(routesFile, toInsert)); }) @@ -539,7 +539,7 @@ import { DetailsComponent as DetailsComponent_1 } from './app/about/description/ 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(() => { + return editedFile.apply(NodeHost).then(() => { options.dasherizedName = 'more'; options.component = 'MoreComponent'; return nru.applyChanges( @@ -609,14 +609,14 @@ export default [ }); it('finds component in the presence of decorators: confirmComponentExport', () => { let editedFile = new InsertChange(componentFile, 0, '@Component{}\n'); - return editedFile.apply().then(() => { + return editedFile.apply(NodeHost).then(() => { let exportExists = nru.confirmComponentExport(componentFile, 'AboutComponent'); expect(exportExists).toBeTruthy(); }); }); it('report absence of component name: confirmComponentExport', () => { let editedFile = new RemoveChange(componentFile, 21, 'onent'); - return editedFile.apply().then(() => { + return editedFile.apply(NodeHost).then(() => { let exportExists = nru.confirmComponentExport(componentFile, 'AboutComponent'); expect(exportExists).not.toBeTruthy(); }); diff --git a/packages/ast-tools/src/route-utils.ts b/packages/ast-tools/src/route-utils.ts index db40e0a41766..4455bafdc814 100644 --- a/packages/ast-tools/src/route-utils.ts +++ b/packages/ast-tools/src/route-utils.ts @@ -4,6 +4,7 @@ import * as path from 'path'; import {Change, InsertChange, NoopChange} from './change'; import {findNodes} from './node'; import {insertAfterLastOccurrence} from './ast-utils'; +import {NodeHost, Host} from './change'; /** * Adds imports to mainFile and adds toBootstrap to the array of providers @@ -368,13 +369,14 @@ export function resolveComponentPath(projectRoot: string, currentDir: string, fi /** * Sort changes in decreasing order and apply them. * @param changes + * @param host * @return Promise */ -export function applyChanges(changes: Change[]): Promise { +export function applyChanges(changes: Change[], host: Host = NodeHost): Promise { return changes .filter(change => !!change) .sort((curr, next) => next.order - curr.order) - .reduce((newChange, change) => newChange.then(() => change.apply()), Promise.resolve()); + .reduce((newChange, change) => newChange.then(() => change.apply(host)), Promise.resolve()); } /** * Helper for addPathToRoutes. Adds child array to the appropriate position in the routes.ts file @@ -416,11 +418,11 @@ function addChildPath (parentObject: ts.Node, pathOptions: any, route: string) { if (childrenNode.length !== 0) { // add to beginning of children array pos = childrenNode[0].getChildAt(2).getChildAt(1).pos; // open bracket - newContent = `\n${spaces}${content}, `; + 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} ` + + newContent = `,\n${spaces.substring(2)}children: [\n${spaces}${content}` + `\n${spaces.substring(2)}]\n${spaces.substring(5)}`; } return {newContent: newContent, pos: pos}; diff --git a/packages/webpack/package.json b/packages/webpack/package.json new file mode 100644 index 000000000000..90218f2ab6e0 --- /dev/null +++ b/packages/webpack/package.json @@ -0,0 +1,15 @@ +{ + "name": "@ngtools/webpack", + "version": "1.0.0", + "description": "", + "main": "./src/index.js", + "license": "MIT", + "dependencies": { + "@angular-cli/ast-tools": "^1.0.0" + }, + "peerDependencies": { + "typescript": "2.0.2", + "@angular/compiler-cli": "^0.6.0", + "@angular/core": "^2.0.0" + } +} diff --git a/packages/webpack/src/compiler.ts b/packages/webpack/src/compiler.ts new file mode 100644 index 000000000000..87dd33414619 --- /dev/null +++ b/packages/webpack/src/compiler.ts @@ -0,0 +1,16 @@ +import * as tscWrapped from '@angular/tsc-wrapped/src/compiler_host'; +import * as ts from 'typescript'; + + +export class NgcWebpackCompilerHost extends tscWrapped.DelegatingHost { + fileCache = new Map(); + + constructor(delegate: ts.CompilerHost) { + super(delegate); + } +} + +export function createCompilerHost(tsConfig: any) { + const delegateHost = ts.createCompilerHost(tsConfig['compilerOptions']); + return new NgcWebpackCompilerHost(delegateHost); +} diff --git a/packages/webpack/src/compiler_host.ts b/packages/webpack/src/compiler_host.ts new file mode 100644 index 000000000000..a65c46053896 --- /dev/null +++ b/packages/webpack/src/compiler_host.ts @@ -0,0 +1,195 @@ +import * as ts from 'typescript'; +import {basename, dirname} from 'path'; +import * as fs from 'fs'; + + +export interface OnErrorFn { + (message: string): void; +} + + +const dev = Math.floor(Math.random() * 10000); + + +export class VirtualStats implements fs.Stats { + protected _ctime = new Date(); + protected _mtime = new Date(); + protected _atime = new Date(); + protected _btime = new Date(); + protected _dev = dev; + protected _ino = Math.floor(Math.random() * 100000); + protected _mode = parseInt('777', 8); // RWX for everyone. + protected _uid = process.env['UID'] || 0; + protected _gid = process.env['GID'] || 0; + + constructor(protected _path: string) {} + + isFile() { return false; } + isDirectory() { return false; } + isBlockDevice() { return false; } + isCharacterDevice() { return false; } + isSymbolicLink() { return false; } + isFIFO() { return false; } + isSocket() { return false; } + + get dev() { return this._dev; } + get ino() { return this._ino; } + get mode() { return this._mode; } + get nlink() { return 1; } // Default to 1 hard link. + get uid() { return this._uid; } + get gid() { return this._gid; } + get rdev() { return 0; } + get size() { return 0; } + get blksize() { return 512; } + get blocks() { return Math.ceil(this.size / this.blksize); } + get atime() { return this._atime; } + get mtime() { return this._mtime; } + get ctime() { return this._ctime; } + get birthtime() { return this._btime; } +} + +export class VirtualDirStats extends VirtualStats { + constructor(_fileName: string) { + super(_fileName); + } + + isDirectory() { return true; } + + get size() { return 1024; } +} + +export class VirtualFileStats extends VirtualStats { + private _sourceFile: ts.SourceFile; + constructor(_fileName: string, private _content: string) { + super(_fileName); + } + + get content() { return this._content; } + set content(v: string) { + this._content = v; + this._mtime = new Date(); + } + getSourceFile(languageVersion: ts.ScriptTarget, setParentNodes: boolean) { + if (!this._sourceFile) { + this._sourceFile = ts.createSourceFile( + this._path, + this._content, + languageVersion, + setParentNodes); + } + + return this._sourceFile; + } + + isFile() { return true; } + + get size() { return this._content.length; } +} + + +export class WebpackCompilerHost implements ts.CompilerHost { + private _delegate: ts.CompilerHost; + private _files: {[path: string]: VirtualFileStats} = Object.create(null); + private _directories: {[path: string]: VirtualDirStats} = Object.create(null); + + constructor(private _options: ts.CompilerOptions, private _setParentNodes = true) { + this._delegate = ts.createCompilerHost(this._options, this._setParentNodes); + } + + private _setFileContent(fileName: string, content: string) { + this._files[fileName] = new VirtualFileStats(fileName, content); + + let p = dirname(fileName); + while (p && !this._directories[p]) { + this._directories[p] = new VirtualDirStats(p); + p = dirname(p); + } + } + + populateWebpackResolver(resolver: any) { + const fs = resolver.fileSystem; + + for (const fileName of Object.keys(this._files)) { + const stats = this._files[fileName]; + fs._statStorage.data[fileName] = [null, stats]; + fs._readFileStorage.data[fileName] = [null, stats.content]; + } + for (const path of Object.keys(this._directories)) { + const stats = this._directories[path]; + const dirs = this.getDirectories(path); + const files = this.getFiles(path); + fs._statStorage.data[path] = [null, stats]; + fs._readdirStorage.data[path] = [null, files.concat(dirs)]; + } + } + + fileExists(fileName: string): boolean { + return fileName in this._files || this._delegate.fileExists(fileName); + } + + readFile(fileName: string): string { + return (fileName in this._files) + ? this._files[fileName].content + : this._delegate.readFile(fileName); + } + + directoryExists(directoryName: string): boolean { + return (directoryName in this._directories) || this._delegate.directoryExists(directoryName); + } + + getFiles(path: string): string[] { + return Object.keys(this._files) + .filter(fileName => dirname(fileName) == path) + .map(path => basename(path)); + } + + getDirectories(path: string): string[] { + const subdirs = Object.keys(this._directories) + .filter(fileName => dirname(fileName) == path) + .map(path => basename(path)); + + let delegated: string[]; + try { + delegated = this._delegate.getDirectories(path); + } catch (e) { + delegated = []; + } + return delegated.concat(subdirs); + } + + getSourceFile(fileName: string, languageVersion: ts.ScriptTarget, onError?: OnErrorFn) { + if (!(fileName in this._files)) { + return this._delegate.getSourceFile(fileName, languageVersion, onError); + } + + return this._files[fileName].getSourceFile(languageVersion, this._setParentNodes); + } + + getCancellationToken() { + return this._delegate.getCancellationToken(); + } + + getDefaultLibFileName(options: ts.CompilerOptions) { + return this._delegate.getDefaultLibFileName(options); + } + + writeFile(fileName: string, data: string, writeByteOrderMark: boolean, onError?: OnErrorFn) { + this._setFileContent(fileName, data); + } + + getCurrentDirectory(): string { + return this._delegate.getCurrentDirectory(); + } + + getCanonicalFileName(fileName: string): string { + return this._delegate.getCanonicalFileName(fileName); + } + + useCaseSensitiveFileNames(): boolean { + return this._delegate.useCaseSensitiveFileNames(); + } + + getNewLine(): string { + return this._delegate.getNewLine(); + } +} diff --git a/packages/webpack/src/entry_resolver.ts b/packages/webpack/src/entry_resolver.ts new file mode 100644 index 000000000000..a96ab266269f --- /dev/null +++ b/packages/webpack/src/entry_resolver.ts @@ -0,0 +1,178 @@ +import * as fs from 'fs'; +import {dirname, join, resolve} from 'path'; +import * as ts from 'typescript'; + + +function _createSource(path: string): ts.SourceFile { + return ts.createSourceFile(path, fs.readFileSync(path, 'utf-8'), ts.ScriptTarget.Latest); +} + +function _findNodes(sourceFile: ts.SourceFile, node: ts.Node, kind: ts.SyntaxKind, + keepGoing = false): ts.Node[] { + if (node.kind == kind && !keepGoing) { + return [node]; + } + + return node.getChildren(sourceFile).reduce((result, n) => { + return result.concat(_findNodes(sourceFile, n, kind, keepGoing)); + }, node.kind == kind ? [node] : []); +} + +function _recursiveSymbolExportLookup(sourcePath: string, + sourceFile: ts.SourceFile, + symbolName: string): string | null { + // Check this file. + const hasSymbol = _findNodes(sourceFile, sourceFile, ts.SyntaxKind.ClassDeclaration) + .some((cd: ts.ClassDeclaration) => { + return cd.name && cd.name.text == symbolName; + }); + if (hasSymbol) { + return sourcePath; + } + + // We found the bootstrap variable, now we just need to get where it's imported. + const exports = _findNodes(sourceFile, sourceFile, ts.SyntaxKind.ExportDeclaration, false) + .map(node => node as ts.ExportDeclaration); + + for (const decl of exports) { + if (!decl.moduleSpecifier || decl.moduleSpecifier.kind !== ts.SyntaxKind.StringLiteral) { + continue; + } + + const module = resolve(dirname(sourcePath), (decl.moduleSpecifier as ts.StringLiteral).text); + if (!decl.exportClause) { + const moduleTs = module + '.ts'; + if (fs.existsSync(moduleTs)) { + const moduleSource = _createSource(moduleTs); + const maybeModule = _recursiveSymbolExportLookup(module, moduleSource, symbolName); + if (maybeModule) { + return maybeModule; + } + } + continue; + } + + const binding = decl.exportClause as ts.NamedExports; + for (const specifier of binding.elements) { + if (specifier.name.text == symbolName) { + // If it's a directory, load its index and recursively lookup. + if (fs.statSync(module).isDirectory()) { + const indexModule = join(module, 'index.ts'); + if (fs.existsSync(indexModule)) { + const maybeModule = _recursiveSymbolExportLookup( + indexModule, _createSource(indexModule), symbolName); + if (maybeModule) { + return maybeModule; + } + } + } + + // Create the source and verify that the symbol is at least a class. + const source = _createSource(module); + const hasSymbol = _findNodes(source, source, ts.SyntaxKind.ClassDeclaration) + .some((cd: ts.ClassDeclaration) => { + return cd.name && cd.name.text == symbolName; + }); + + if (hasSymbol) { + return module; + } else { + return null; + } + } + } + } + + return null; +} + +function _symbolImportLookup(sourcePath: string, + sourceFile: ts.SourceFile, + symbolName: string): string | null { + // We found the bootstrap variable, now we just need to get where it's imported. + const imports = _findNodes(sourceFile, sourceFile, ts.SyntaxKind.ImportDeclaration, false) + .map(node => node as ts.ImportDeclaration); + + for (const decl of imports) { + if (!decl.importClause || !decl.moduleSpecifier) { + continue; + } + if (decl.moduleSpecifier.kind !== ts.SyntaxKind.StringLiteral) { + continue; + } + + const module = resolve(dirname(sourcePath), (decl.moduleSpecifier as ts.StringLiteral).text); + + if (decl.importClause.namedBindings.kind == ts.SyntaxKind.NamespaceImport) { + const binding = decl.importClause.namedBindings as ts.NamespaceImport; + if (binding.name.text == symbolName) { + // This is a default export. + return module; + } + } else if (decl.importClause.namedBindings.kind == ts.SyntaxKind.NamedImports) { + const binding = decl.importClause.namedBindings as ts.NamedImports; + for (const specifier of binding.elements) { + if (specifier.name.text == symbolName) { + // If it's a directory, load its index and recursively lookup. + if (fs.statSync(module).isDirectory()) { + const indexModule = join(module, 'index.ts'); + if (fs.existsSync(indexModule)) { + const maybeModule = _recursiveSymbolExportLookup( + indexModule, _createSource(indexModule), symbolName); + if (maybeModule) { + return maybeModule; + } + } + } + + // Create the source and verify that the symbol is at least a class. + const source = _createSource(module); + const hasSymbol = _findNodes(source, source, ts.SyntaxKind.ClassDeclaration) + .some((cd: ts.ClassDeclaration) => { + return cd.name && cd.name.text == symbolName; + }); + + if (hasSymbol) { + return module; + } else { + return null; + } + } + } + } + } + return null; +} + + +export function resolveEntryModuleFromMain(mainPath: string) { + const source = _createSource(mainPath); + + const bootstrap = _findNodes(source, source, ts.SyntaxKind.CallExpression, false) + .map(node => node as ts.CallExpression) + .filter(call => { + const access = call.expression as ts.PropertyAccessExpression; + return access.kind == ts.SyntaxKind.PropertyAccessExpression + && access.name.kind == ts.SyntaxKind.Identifier + && (access.name.text == 'bootstrapModule' + || access.name.text == 'bootstrapModuleFactory'); + }); + + if (bootstrap.length != 1 + || bootstrap[0].arguments[0].kind !== ts.SyntaxKind.Identifier) { + throw new Error('Tried to find bootstrap code, but could not. Specify either ' + + 'statically analyzable bootstrap code or pass in an entryModule ' + + 'to the plugins options.'); + } + + const bootstrapSymbolName = (bootstrap[0].arguments[0] as ts.Identifier).text; + const module = _symbolImportLookup(mainPath, source, bootstrapSymbolName); + if (module) { + return `${resolve(dirname(mainPath), module)}#${bootstrapSymbolName}`; + } + + // shrug... something bad happened and we couldn't find the import statement. + throw new Error('Tried to find bootstrap code, but could not. Specify either ' + + 'statically analyzable bootstrap code or pass in an entryModule ' + + 'to the plugins options.'); +} diff --git a/packages/webpack/src/index.ts b/packages/webpack/src/index.ts new file mode 100644 index 000000000000..8fd3bad21818 --- /dev/null +++ b/packages/webpack/src/index.ts @@ -0,0 +1,4 @@ +import 'reflect-metadata'; + +export * from './plugin' +export {ngcLoader as default} from './loader' diff --git a/packages/webpack/src/loader.ts b/packages/webpack/src/loader.ts new file mode 100644 index 000000000000..ba162a5d18fd --- /dev/null +++ b/packages/webpack/src/loader.ts @@ -0,0 +1,149 @@ +import * as path from 'path'; +import * as ts from 'typescript'; +import {NgcWebpackPlugin} from './plugin'; +import {MultiChange, ReplaceChange, insertImport} from '@angular-cli/ast-tools'; + +// TODO: move all this to ast-tools. +function _findNodes(sourceFile: ts.SourceFile, node: ts.Node, kind: ts.SyntaxKind, + keepGoing = false): ts.Node[] { + if (node.kind == kind && !keepGoing) { + return [node]; + } + + return node.getChildren(sourceFile).reduce((result, n) => { + return result.concat(_findNodes(sourceFile, n, kind, keepGoing)); + }, node.kind == kind ? [node] : []); +} + +function _removeDecorators(fileName: string, source: string): string { + const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest); + // Find all decorators. + const decorators = _findNodes(sourceFile, sourceFile, ts.SyntaxKind.Decorator); + decorators.sort((a, b) => b.pos - a.pos); + + decorators.forEach(d => { + source = source.slice(0, d.pos) + source.slice(d.end); + }); + + return source; +} + + +function _replaceBootstrap(fileName: string, + source: string, + plugin: NgcWebpackPlugin): Promise { + // If bootstrapModule can't be found, bail out early. + if (!source.match(/\bbootstrapModule\b/)) { + return Promise.resolve(source); + } + + let changes = new MultiChange(); + + // Calculate the base path. + const basePath = path.normalize(plugin.angularCompilerOptions.basePath); + const genDir = path.normalize(plugin.genDir); + const dirName = path.normalize(path.dirname(fileName)); + const [entryModulePath, entryModuleName] = plugin.entryModule.split('#'); + const entryModuleFileName = path.normalize(entryModulePath + '.ngfactory'); + const relativeEntryModulePath = path.relative(basePath, entryModuleFileName); + const fullEntryModulePath = path.resolve(genDir, relativeEntryModulePath); + const relativeNgFactoryPath = path.relative(dirName, fullEntryModulePath); + const ngFactoryPath = './' + relativeNgFactoryPath.replace(/\\/g, '/'); + + const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest); + + const allCalls = _findNodes( + sourceFile, sourceFile, ts.SyntaxKind.CallExpression, true) as ts.CallExpression[]; + + const bootstraps = allCalls + .filter(call => call.expression.kind == ts.SyntaxKind.PropertyAccessExpression) + .map(call => call.expression as ts.PropertyAccessExpression) + .filter(access => { + return access.name.kind == ts.SyntaxKind.Identifier + && access.name.text == 'bootstrapModule'; + }); + + const calls: ts.Node[] = bootstraps + .reduce((previous, access) => { + return previous.concat(_findNodes(sourceFile, access, ts.SyntaxKind.CallExpression, true)); + }, []) + .filter(call => { + return call.expression.kind == ts.SyntaxKind.Identifier + && call.expression.text == 'platformBrowserDynamic'; + }); + + if (calls.length == 0) { + // Didn't find any dynamic bootstrapping going on. + return Promise.resolve(source); + } + + // Create the changes we need. + allCalls + .filter(call => bootstraps.some(bs => bs == call.expression)) + .forEach((call: ts.CallExpression) => { + changes.appendChange(new ReplaceChange(fileName, call.arguments[0].getStart(sourceFile), + entryModuleName, entryModuleName + 'NgFactory')); + }); + + calls + .forEach(call => { + changes.appendChange(new ReplaceChange(fileName, call.getStart(sourceFile), + 'platformBrowserDynamic', 'platformBrowser')); + }); + + bootstraps + .forEach((bs: ts.PropertyAccessExpression) => { + // This changes the call. + changes.appendChange(new ReplaceChange(fileName, bs.name.getStart(sourceFile), + 'bootstrapModule', 'bootstrapModuleFactory')); + }); + changes.appendChange(insertImport(fileName, 'platformBrowser', '@angular/platform-browser')); + changes.appendChange(insertImport(fileName, entryModuleName + 'NgFactory', ngFactoryPath)); + + let sourceText = source; + return changes.apply({ + read: (path: string) => Promise.resolve(sourceText), + write: (path: string, content: string) => Promise.resolve(sourceText = content) + }).then(() => sourceText); +} + + +// Super simple TS transpiler loader for testing / isolated usage. does not type check! +export function ngcLoader(source: string) { + this.cacheable(); + + const plugin = this._compilation._ngToolsWebpackPluginInstance as NgcWebpackPlugin; + if (plugin && plugin instanceof NgcWebpackPlugin) { + const cb: any = this.async(); + + plugin.done + .then(() => _removeDecorators(this.resource, source)) + .then(sourceText => _replaceBootstrap(this.resource, sourceText, plugin)) + .then(sourceText => { + const result = ts.transpileModule(sourceText, { + compilerOptions: { + target: ts.ScriptTarget.ES5, + module: ts.ModuleKind.ES2015, + } + }); + + if (result.diagnostics && result.diagnostics.length) { + let message = ''; + result.diagnostics.forEach(d => { + message += d.messageText + '\n'; + }); + cb(new Error(message)); + } + + cb(null, result.outputText, result.sourceMapText ? JSON.parse(result.sourceMapText) : null); + }) + .catch(err => cb(err)); + } else { + return ts.transpileModule(source, { + compilerOptions: { + target: ts.ScriptTarget.ES5, + module: ts.ModuleKind.ES2015, + } + }).outputText; + } +} diff --git a/packages/webpack/src/plugin.ts b/packages/webpack/src/plugin.ts new file mode 100644 index 000000000000..92201ad1c9d8 --- /dev/null +++ b/packages/webpack/src/plugin.ts @@ -0,0 +1,240 @@ +import * as ts from 'typescript'; +import * as path from 'path'; + +import {NgModule} from '@angular/core'; +import * as ngCompiler from '@angular/compiler-cli'; +import {tsc} from '@angular/tsc-wrapped/src/tsc'; + +import {patchReflectorHost} from './reflector_host'; +import {WebpackResourceLoader} from './resource_loader'; +import {createResolveDependenciesFromContextMap} from './utils'; +import { AngularCompilerOptions } from '@angular/tsc-wrapped'; +import {WebpackCompilerHost} from './compiler_host'; +import {resolveEntryModuleFromMain} from './entry_resolver'; + + +/** + * Option Constants + */ +export interface AngularWebpackPluginOptions { + tsconfigPath?: string; + providers?: any[]; + entryModule?: string; + project: string; + baseDir: string; + basePath?: string; + genDir?: string; + main?: string; +} + + +export class NgcWebpackPlugin { + projectPath: string; + rootModule: string; + rootModuleName: string; + reflector: ngCompiler.StaticReflector; + reflectorHost: ngCompiler.ReflectorHost; + program: ts.Program; + compilerHost: WebpackCompilerHost; + compilerOptions: ts.CompilerOptions; + angularCompilerOptions: AngularCompilerOptions; + files: any[]; + lazyRoutes: any; + loader: any; + genDir: string; + entryModule: string; + + done: Promise; + + nmf: any = null; + cmf: any = null; + compiler: any = null; + compilation: any = null; + + constructor(public options: AngularWebpackPluginOptions) { + const tsConfig = tsc.readConfiguration(options.project, options.baseDir); + this.compilerOptions = tsConfig.parsed.options; + this.files = tsConfig.parsed.fileNames; + this.angularCompilerOptions = Object.assign({}, tsConfig.ngOptions, options); + + this.angularCompilerOptions.basePath = options.baseDir || process.cwd(); + this.genDir = this.options.genDir + || path.resolve(process.cwd(), this.angularCompilerOptions.genDir + '/app'); + this.entryModule = options.entryModule || (this.angularCompilerOptions as any).entryModule; + if (!options.entryModule && options.main) { + this.entryModule = resolveEntryModuleFromMain(options.main); + } + + const entryModule = this.entryModule; + const [rootModule, rootNgModule] = entryModule.split('#'); + this.projectPath = options.project; + this.rootModule = rootModule; + this.rootModuleName = rootNgModule; + this.compilerHost = new WebpackCompilerHost(this.compilerOptions); + this.program = ts.createProgram(this.files, this.compilerOptions, this.compilerHost); + this.reflectorHost = new ngCompiler.ReflectorHost( + this.program, this.compilerHost, this.angularCompilerOptions); + this.reflector = new ngCompiler.StaticReflector(this.reflectorHost); + } + + // registration hook for webpack plugin + apply(compiler: any) { + this.compiler = compiler; + compiler.plugin('normal-module-factory', (nmf: any) => this.nmf = nmf); + compiler.plugin('context-module-factory', (cmf: any) => { + this.cmf = cmf; + cmf.plugin('before-resolve', (request: any, callback: (err?: any, request?: any) => void) => { + if (!request) { + return callback(); + } + + request.request = this.genDir; + request.recursive = true; + request.dependencies.forEach((d: any) => d.critical = false); + return callback(null, request); + }); + cmf.plugin('after-resolve', (result: any, callback: (err?: any, request?: any) => void) => { + if (!result) { + return callback(); + } + + result.resource = this.genDir; + result.recursive = true; + result.dependencies.forEach((d: any) => d.critical = false); + result.resolveDependencies = createResolveDependenciesFromContextMap((_: any, cb: any) => { + return cb(null, this.lazyRoutes); + }); + + return callback(null, result); + }); + }); + + compiler.plugin('make', (compilation: any, cb: any) => this._make(compilation, cb)); + compiler.plugin('after-emit', (compilation: any, cb: any) => { + this.done = null; + this.compilation = null; + compilation._ngToolsWebpackPluginInstance = null; + cb(); + }); + + // Virtual file system. + compiler.resolvers.normal.plugin('resolve', (request: any, cb?: () => void) => { + // populate the file system cache with the virtual module + this.compilerHost.populateWebpackResolver(compiler.resolvers.normal); + if (cb) { + cb(); + } + }); + } + + private _make(compilation: any, cb: (err?: any, request?: any) => void) { + const rootModulePath = path.normalize(this.rootModule + '.ts'); + const rootModuleName = this.rootModuleName; + this.compilation = compilation; + + if (this.compilation._ngToolsWebpackPluginInstance) { + cb(new Error('A ngtools/webpack plugin already exist for this compilation.')); + } + this.compilation._ngToolsWebpackPluginInstance = this; + + this.loader = new WebpackResourceLoader(compilation); + + const i18nOptions: any = { + i18nFile: undefined, + i18nFormat: undefined, + locale: undefined, + basePath: this.options.baseDir + }; + + // Create the Code Generator. + const codeGenerator = ngCompiler.CodeGenerator.create( + this.angularCompilerOptions, + i18nOptions, + this.program, + this.compilerHost, + new ngCompiler.NodeReflectorHostContext(this.compilerHost), + this.loader + ); + + // We need to temporarily patch the CodeGenerator until either it's patched or allows us + // to pass in our own ReflectorHost. + patchReflectorHost(codeGenerator); + this.done = codeGenerator.codegen() + .then(() => { + // process the lazy routes + const lazyModules = this._processNgModule(rootModulePath, rootModuleName, rootModulePath) + .map(moduleKey => moduleKey.split('#')[0]); + this.lazyRoutes = lazyModules.reduce((lazyRoutes: any, lazyModule: any) => { + const genDir = this.genDir; + lazyRoutes[`${lazyModule}.ngfactory`] = path.join(genDir, lazyModule + '.ngfactory.ts'); + return lazyRoutes; + }, {}); + }) + .then(() => cb(), (err) => cb(err)); + } + + private _processNgModule(mod: string, ngModuleName: string, containingFile: string): string[] { + const staticSymbol = this.reflectorHost.findDeclaration(mod, ngModuleName, containingFile); + const entryNgModuleMetadata = this.getNgModuleMetadata(staticSymbol); + const loadChildren = this.extractLoadChildren(entryNgModuleMetadata); + + return loadChildren.reduce((res, lc) => { + const [childModule, childNgModule] = lc.split('#'); + + // TODO calculate a different containingFile for relative paths + + const children = this._processNgModule(childModule, childNgModule, containingFile); + return res.concat(children); + }, loadChildren); + } + + private getNgModuleMetadata(staticSymbol: ngCompiler.StaticSymbol) { + const ngModules = this.reflector.annotations(staticSymbol).filter(s => s instanceof NgModule); + if (ngModules.length === 0) { + throw new Error(`${staticSymbol.name} is not an NgModule`); + } + return ngModules[0]; + } + + private extractLoadChildren(ngModuleDecorator: any): any[] { + const routes = ngModuleDecorator.imports.reduce((mem: any[], m: any) => { + return mem.concat(this.collectRoutes(m.providers)); + }, this.collectRoutes(ngModuleDecorator.providers)); + return this.collectLoadChildren(routes); + } + + private collectRoutes(providers: any[]): any[] { + if (!providers) { + return []; + } + const ROUTES = this.reflectorHost.findDeclaration( + '@angular/router/src/router_config_loader', 'ROUTES', undefined); + + return providers.reduce((m, p) => { + if (p.provide === ROUTES) { + return m.concat(p.useValue); + } else if (Array.isArray(p)) { + return m.concat(this.collectRoutes(p)); + } else { + return m; + } + }, []); + } + + private collectLoadChildren(routes: any[]): any[] { + if (!routes) { + return []; + } + return routes.reduce((m, r) => { + if (r.loadChildren) { + return m.concat(r.loadChildren); + } else if (Array.isArray(r)) { + return m.concat(this.collectLoadChildren(r)); + } else if (r.children) { + return m.concat(this.collectLoadChildren(r.children)); + } else { + return m; + } + }, []); + } +} diff --git a/packages/webpack/src/reflector_host.ts b/packages/webpack/src/reflector_host.ts new file mode 100644 index 000000000000..0e995c2ee007 --- /dev/null +++ b/packages/webpack/src/reflector_host.ts @@ -0,0 +1,26 @@ +import {CodeGenerator} from '@angular/compiler-cli'; + + +/** + * Patch the CodeGenerator instance to use a custom reflector host. + */ +export function patchReflectorHost(codeGenerator: CodeGenerator) { + const reflectorHost = (codeGenerator as any).reflectorHost; + const oldGIP = reflectorHost.getImportPath; + + reflectorHost.getImportPath = function(containingFile: string, importedFile: string): string { + // Hack together SCSS and LESS files URLs so that they match what the default ReflectorHost + // is expected. We only do that for shimmed styles. + const m = importedFile.match(/(.*)(\..+)(\.shim)(\..+)/); + if (!m) { + return oldGIP.call(this, containingFile, importedFile); + } + + // We call the original, with `css` in its name instead of the extension, and replace the + // extension from the result. + const [, baseDirAndName, styleExt, shim, ext] = m; + const result = oldGIP.call(this, containingFile, baseDirAndName + '.css' + shim + ext); + + return result.replace(/\.css\./, styleExt + '.'); + }; +} diff --git a/packages/webpack/src/resource_loader.ts b/packages/webpack/src/resource_loader.ts new file mode 100644 index 000000000000..4b146ea1e310 --- /dev/null +++ b/packages/webpack/src/resource_loader.ts @@ -0,0 +1,114 @@ +import {ResourceLoader} from '@angular/compiler'; +import {readFileSync} from 'fs'; +import * as vm from 'vm'; +import * as path from 'path'; + +const NodeTemplatePlugin = require('webpack/lib/node/NodeTemplatePlugin'); +const NodeTargetPlugin = require('webpack/lib/node/NodeTargetPlugin'); +const LoaderTargetPlugin = require('webpack/lib/LoaderTargetPlugin'); +const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin'); + + + +export class WebpackResourceLoader implements ResourceLoader { + private _context: string; + private _uniqueId = 0; + + constructor(private _parentCompilation: any) { + this._context = _parentCompilation.context; + } + + private _compile(filePath: string, content: string): Promise { + const compilerName = `compiler(${this._uniqueId++})`; + const outputOptions = { filename: filePath }; + const relativePath = path.relative(this._context || '', filePath); + const childCompiler = this._parentCompilation.createChildCompiler(relativePath, outputOptions); + childCompiler.context = this._context; + childCompiler.apply( + new NodeTemplatePlugin(outputOptions), + new NodeTargetPlugin(), + new SingleEntryPlugin(this._context, filePath), + new LoaderTargetPlugin('node') + ); + + // Store the result of the parent compilation before we start the child compilation + let assetsBeforeCompilation = Object.assign( + {}, + this._parentCompilation.assets[outputOptions.filename] + ); + + // Fix for "Uncaught TypeError: __webpack_require__(...) is not a function" + // Hot module replacement requires that every child compiler has its own + // cache. @see https://github.com/ampedandwired/html-webpack-plugin/pull/179 + childCompiler.plugin('compilation', function (compilation: any) { + if (compilation.cache) { + if (!compilation.cache[compilerName]) { + compilation.cache[compilerName] = {}; + } + compilation.cache = compilation.cache[compilerName]; + } + }); + + // Compile and return a promise + return new Promise((resolve, reject) => { + childCompiler.runAsChild((err: Error, entries: any[], childCompilation: any) => { + // Resolve / reject the promise + if (childCompilation && childCompilation.errors && childCompilation.errors.length) { + const errorDetails = childCompilation.errors.map(function (error: any) { + return error.message + (error.error ? ':\n' + error.error : ''); + }).join('\n'); + reject(new Error('Child compilation failed:\n' + errorDetails)); + } else if (err) { + reject(err); + } else { + // Replace [hash] placeholders in filename + const outputName = this._parentCompilation.mainTemplate.applyPluginsWaterfall( + 'asset-path', outputOptions.filename, { + hash: childCompilation.hash, + chunk: entries[0] + }); + + // Restore the parent compilation to the state like it was before the child compilation. + this._parentCompilation.assets[outputName] = assetsBeforeCompilation[outputName]; + if (assetsBeforeCompilation[outputName] === undefined) { + // If it wasn't there - delete it. + delete this._parentCompilation.assets[outputName]; + } + + resolve({ + // Hash of the template entry point. + hash: entries[0].hash, + // Output name. + outputName: outputName, + // Compiled code. + content: childCompilation.assets[outputName].source() + }); + } + }); + }); + } + + private _evaluate(fileName: string, source: string): Promise { + try { + const vmContext = vm.createContext(Object.assign({require: require}, global)); + const vmScript = new vm.Script(source, {filename: fileName}); + + // Evaluate code and cast to string + let newSource: string; + newSource = vmScript.runInContext(vmContext); + + if (typeof newSource == 'string') { + return Promise.resolve(newSource); + } + + return Promise.reject('The loader "' + fileName + '" didn\'t return a string.'); + } catch (e) { + return Promise.reject(e); + } + } + + get(filePath: string): Promise { + return this._compile(filePath, readFileSync(filePath, 'utf8')) + .then((result: any) => this._evaluate(result.outputName, result.content)); + } +} diff --git a/packages/webpack/src/utils.ts b/packages/webpack/src/utils.ts new file mode 100644 index 000000000000..954cc206ad9b --- /dev/null +++ b/packages/webpack/src/utils.ts @@ -0,0 +1,16 @@ +const ContextElementDependency = require('webpack/lib/dependencies/ContextElementDependency'); + +export function createResolveDependenciesFromContextMap(createContextMap: Function) { + return (fs: any, resource: any, recursive: any, regExp: RegExp, callback: any) => { + createContextMap(fs, function(err: Error, map: any) { + if (err) { + return callback(err); + } + + const dependencies = Object.keys(map) + .map((key) => new ContextElementDependency(map[key], key)); + + callback(null, dependencies); + }); + }; +} diff --git a/packages/webpack/tsconfig.json b/packages/webpack/tsconfig.json new file mode 100644 index 000000000000..68fa995ac3a7 --- /dev/null +++ b/packages/webpack/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "declaration": true, + "experimentalDecorators": true, + "mapRoot": "", + "module": "commonjs", + "moduleResolution": "node", + "noEmitOnError": true, + "noImplicitAny": true, + "outDir": "../../dist/webpack", + "rootDir": ".", + "lib": ["es2015", "es6", "dom"], + "target": "es5", + "sourceMap": true, + "sourceRoot": "/", + "baseUrl": ".", + "paths": { + "@angular-cli/ast-tools": [ "../../dist/ast-tools/src" ] + }, + "typeRoots": [ + "../../node_modules/@types" + ], + "types": [ + "jasmine", + "node" + ] + } +} diff --git a/scripts/publish/build.js b/scripts/publish/build.js index b5bda9d3a951..642a353f6a1a 100755 --- a/scripts/publish/build.js +++ b/scripts/publish/build.js @@ -40,6 +40,18 @@ Promise.resolve() .then(() => { const packages = require('../../lib/packages'); return Object.keys(packages) + // Order packages in order of dependency. + .sort((a, b) => { + const aPackageJson = require(packages[a].packageJson); + const bPackageJson = require(packages[b].packageJson); + if (Object.keys(aPackageJson['dependencies'] || {}).indexOf(b) == -1) { + return 0; + } else if (Object.keys(bPackageJson['dependencies'] || {}).indexOf(a) == -1) { + return 1; + } else { + return -1; + } + }) .reduce((promise, packageName) => { const pkg = packages[packageName]; const name = path.relative(packagesRoot, pkg.root); diff --git a/scripts/run-packages-spec.js b/scripts/run-packages-spec.js index 33b924b6ea81..987b3ea3d87e 100644 --- a/scripts/run-packages-spec.js +++ b/scripts/run-packages-spec.js @@ -14,6 +14,8 @@ const projectBaseDir = path.join(__dirname, '../packages'); const jasmine = new Jasmine({ projectBaseDir: projectBaseDir }); jasmine.loadConfig({}); jasmine.addReporter(new JasmineSpecReporter()); +// Manually set exit code (needed with custom reporters) +jasmine.onComplete((success) => process.exitCode = !success); // Run the tests. const allTests = diff --git a/tests/acceptance/generate-class.spec.js b/tests/acceptance/generate-class.spec.js index 9215bd32cfc9..d0c38f964135 100644 --- a/tests/acceptance/generate-class.spec.js +++ b/tests/acceptance/generate-class.spec.js @@ -17,6 +17,7 @@ describe('Acceptance: ng generate class', function () { after(conf.restore); beforeEach(function () { + this.timeout(10000); return tmp.setup('./tmp').then(function () { process.chdir('./tmp'); }).then(function () { @@ -25,8 +26,6 @@ describe('Acceptance: ng generate class', function () { }); afterEach(function () { - this.timeout(10000); - return tmp.teardown('./tmp'); }); diff --git a/tests/acceptance/generate-component.spec.js b/tests/acceptance/generate-component.spec.js index c5abd319faf7..6416ef2fff76 100644 --- a/tests/acceptance/generate-component.spec.js +++ b/tests/acceptance/generate-component.spec.js @@ -43,7 +43,7 @@ describe('Acceptance: ng generate component', function () { .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); + expect(content).matches(/declarations:\s*\[[^\]]+?,\r?\n\s+MyCompComponent\r?\n/m); }); }); diff --git a/tests/acceptance/generate-directive.spec.js b/tests/acceptance/generate-directive.spec.js index 6a2b93266d06..3620720b2018 100644 --- a/tests/acceptance/generate-directive.spec.js +++ b/tests/acceptance/generate-directive.spec.js @@ -51,7 +51,7 @@ describe('Acceptance: ng generate directive', function () { .then(() => readFile(appModulePath, 'utf-8')) .then(content => { expect(content).matches(/import.*\bMyDirDirective\b.*from '.\/my-dir\/my-dir.directive';/); - expect(content).matches(/declarations:\s*\[[^\]]+?,\n\s+MyDirDirective\n/m); + expect(content).matches(/declarations:\s*\[[^\]]+?,\r?\n\s+MyDirDirective\r?\n/m); }); }); @@ -156,4 +156,15 @@ describe('Acceptance: ng generate directive', function () { expect(err).to.equal(`Invalid path: "..${path.sep}my-dir" cannot be above the "src${path.sep}app" directory`); }); }); + + it('converts dash-cased-name to a camelCasedSelector', () => { + const appRoot = path.join(root, 'tmp/foo'); + const directivePath = path.join(appRoot, 'src/app/my-dir.directive.ts'); + return ng(['generate', 'directive', 'my-dir']) + .then(() => readFile(directivePath, 'utf-8')) + .then(content => { + // expect(content).matches(/selector: [app-my-dir]/m); + expect(content).matches(/selector: '\[appMyDir\]'/); + }); + }); }); diff --git a/tests/acceptance/generate-pipe.spec.js b/tests/acceptance/generate-pipe.spec.js index c6463697e2b0..2d98f8605895 100644 --- a/tests/acceptance/generate-pipe.spec.js +++ b/tests/acceptance/generate-pipe.spec.js @@ -44,7 +44,7 @@ describe('Acceptance: ng generate pipe', function () { .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); + expect(content).matches(/declarations:\s*\[[^\]]+?,\r?\n\s+MyPipePipe\r?\n/m); }); }); diff --git a/tests/acceptance/init.spec.js b/tests/acceptance/init.spec.js index 964397d8db78..6be6287337c2 100644 --- a/tests/acceptance/init.spec.js +++ b/tests/acceptance/init.spec.js @@ -17,6 +17,7 @@ var unique = require('lodash/uniq'); var forEach = require('lodash/forEach'); var any = require('lodash/some'); var EOL = require('os').EOL; +var existsSync = require('exists-sync'); var defaultIgnoredFiles = Blueprint.ignoredFiles; @@ -44,7 +45,8 @@ describe('Acceptance: ng init', function () { return tmp.teardown('./tmp'); }); - function confirmBlueprinted(isMobile) { + function confirmBlueprinted(isMobile, routing) { + routing = !!routing; var blueprintPath = path.join(root, 'blueprints', 'ng2', 'files'); var mobileBlueprintPath = path.join(root, 'blueprints', 'mobile', 'files'); var expected = unique(walkSync(blueprintPath).concat(isMobile ? walkSync(mobileBlueprintPath) : []).sort()); @@ -55,14 +57,18 @@ describe('Acceptance: ng init', function () { }); expected.forEach(function (file, index) { - expected[index] = file.replace(/__name__/g, 'tmp'); + expected[index] = file.replace(/__name__/g, 'app'); expected[index] = expected[index].replace(/__styleext__/g, 'css'); expected[index] = expected[index].replace(/__path__/g, 'src'); }); - + if (isMobile) { - expected = expected.filter(p => p.indexOf('tmp.component.html') < 0); - expected = expected.filter(p => p.indexOf('tmp.component.css') < 0); + expected = expected.filter(p => p.indexOf('app.component.html') < 0); + expected = expected.filter(p => p.indexOf('app.component.css') < 0); + } + + if (!routing) { + expected = expected.filter(p => p.indexOf('app-routing.module.ts') < 0); } removeIgnored(expected); @@ -200,4 +206,20 @@ describe('Acceptance: ng init', function () { }) .then(confirmBlueprinted); }); + + it('ng init --inline-template does not generate a template file', () => { + return ng(['init', '--skip-npm', '--skip-git', '--inline-template']) + .then(() => { + const templateFile = path.join('src', 'app', 'app.component.html'); + expect(existsSync(templateFile)).to.equal(false); + }); + }); + + it('ng init --inline-style does not gener a style file', () => { + return ng(['init', '--skip-npm', '--skip-git', '--inline-style']) + .then(() => { + const styleFile = path.join('src', 'app', 'app.component.css'); + expect(existsSync(styleFile)).to.equal(false); + }); + }); }); diff --git a/tests/acceptance/new.spec.js b/tests/acceptance/new.spec.js index cb3449f6d83b..1cdcc640b214 100644 --- a/tests/acceptance/new.spec.js +++ b/tests/acceptance/new.spec.js @@ -103,42 +103,6 @@ describe('Acceptance: ng new', function () { .then(confirmBlueprinted); }); - it('ng new with blueprint uses the specified blueprint directory with a relative path', - function () { - return tmp.setup('./tmp/my_blueprint') - .then(function () { - return tmp.setup('./tmp/my_blueprint/files'); - }) - .then(function () { - fs.writeFileSync('./tmp/my_blueprint/files/gitignore'); - process.chdir('./tmp'); - - return ng([ - 'new', 'foo', '--skip-npm', '--skip-bower', '--skip-git', - '--blueprint=./my_blueprint' - ]); - }) - .then(confirmBlueprintedForDir('tmp/my_blueprint')); - }); - - it('ng new with blueprint uses the specified blueprint directory with an absolute path', - function () { - return tmp.setup('./tmp/my_blueprint') - .then(function () { - return tmp.setup('./tmp/my_blueprint/files'); - }) - .then(function () { - fs.writeFileSync('./tmp/my_blueprint/files/gitignore'); - process.chdir('./tmp'); - - return ng([ - 'new', 'foo', '--skip-npm', '--skip-bower', '--skip-git', - '--blueprint=' + path.resolve(process.cwd(), './my_blueprint') - ]); - }) - .then(confirmBlueprintedForDir('tmp/my_blueprint')); - }); - it('ng new without skip-git flag creates .git dir', function () { return ng(['new', 'foo', '--skip-npm', '--skip-bower']).then(function () { expect(existsSync('.git')); @@ -168,4 +132,20 @@ describe('Acceptance: ng new', function () { expect(pkgJson.name).to.equal('foo', 'uses app name for package name'); }); }); + + it('ng new --inline-template does not generate a template file', () => { + return ng(['new', 'foo', '--skip-npm', '--skip-git', '--inline-template']) + .then(() => { + const templateFile = path.join('src', 'app', 'app.component.html'); + expect(existsSync(templateFile)).to.equal(false); + }); + }); + + it('ng new --inline-style does not gener a style file', () => { + return ng(['new', 'foo', '--skip-npm', '--skip-git', '--inline-style']) + .then(() => { + const styleFile = path.join('src', 'app', 'app.component.css'); + expect(existsSync(styleFile)).to.equal(false); + }); + }); }); diff --git a/tests/e2e/assets/webpack/test-app/app/app.component.html b/tests/e2e/assets/webpack/test-app/app/app.component.html new file mode 100644 index 000000000000..5a532db9308f --- /dev/null +++ b/tests/e2e/assets/webpack/test-app/app/app.component.html @@ -0,0 +1,5 @@ +
+

hello world

+ lazy + +
diff --git a/tests/e2e/assets/webpack/test-app/app/app.component.scss b/tests/e2e/assets/webpack/test-app/app/app.component.scss new file mode 100644 index 000000000000..5cde7b922336 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app/app/app.component.scss @@ -0,0 +1,3 @@ +:host { + background-color: blue; +} diff --git a/tests/e2e/assets/webpack/test-app/app/app.component.ts b/tests/e2e/assets/webpack/test-app/app/app.component.ts new file mode 100644 index 000000000000..2fc4df46be32 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app/app/app.component.ts @@ -0,0 +1,10 @@ +// Code generated by angular2-stress-test + +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'] +}) +export class AppComponent { } diff --git a/tests/e2e/assets/webpack/test-app/app/app.module.ts b/tests/e2e/assets/webpack/test-app/app/app.module.ts new file mode 100644 index 000000000000..79e133f3eeeb --- /dev/null +++ b/tests/e2e/assets/webpack/test-app/app/app.module.ts @@ -0,0 +1,28 @@ +// Code generated by angular2-stress-test +import { NgModule, Component } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { RouterModule } from '@angular/router'; +import { AppComponent } from './app.component'; + +@Component({ + selector: 'home-view', + template: 'home!' +}) +export class HomeView {} + + +@NgModule({ + declarations: [ + AppComponent, + HomeView + ], + imports: [ + BrowserModule, + RouterModule.forRoot([ + {path: 'lazy', loadChildren: './lazy.module#LazyModule'}, + {path: '', component: HomeView} + ]) + ], + bootstrap: [AppComponent] +}) +export class AppModule { } diff --git a/tests/e2e/assets/webpack/test-app/app/feature/feature.module.ts b/tests/e2e/assets/webpack/test-app/app/feature/feature.module.ts new file mode 100644 index 000000000000..f464ca028b05 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app/app/feature/feature.module.ts @@ -0,0 +1,20 @@ +import {NgModule, Component} from '@angular/core'; +import {RouterModule} from '@angular/router'; + +@Component({ + selector: 'feature-component', + template: 'foo.html' +}) +export class FeatureComponent {} + +@NgModule({ + declarations: [ + FeatureComponent + ], + imports: [ + RouterModule.forChild([ + { path: '', component: FeatureComponent} + ]) + ] +}) +export class FeatureModule {} diff --git a/tests/e2e/assets/webpack/test-app/app/lazy.module.ts b/tests/e2e/assets/webpack/test-app/app/lazy.module.ts new file mode 100644 index 000000000000..29dab5d11d35 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app/app/lazy.module.ts @@ -0,0 +1,25 @@ +import {NgModule, Component} from '@angular/core'; +import {RouterModule} from '@angular/router'; +import {HttpModule, Http} from '@angular/http'; + +@Component({ + selector: 'lazy-comp', + template: 'lazy!' +}) +export class LazyComponent {} + +@NgModule({ + imports: [ + RouterModule.forChild([ + {path: '', component: LazyComponent, pathMatch: 'full'}, + {path: 'feature', loadChildren: './feature/feature.module#FeatureModule'} + ]), + HttpModule + ], + declarations: [LazyComponent] +}) +export class LazyModule { + constructor(http: Http) {} +} + +export class SecondModule {} diff --git a/tests/e2e/assets/webpack/test-app/app/main.aot.ts b/tests/e2e/assets/webpack/test-app/app/main.aot.ts new file mode 100644 index 000000000000..1b4503a81e31 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app/app/main.aot.ts @@ -0,0 +1,5 @@ +import 'core-js/es7/reflect'; +import {platformBrowser} from '@angular/platform-browser'; +import {AppModuleNgFactory} from './ngfactory/app/app.module.ngfactory'; + +platformBrowser().bootstrapModuleFactory(AppModuleNgFactory); diff --git a/tests/e2e/assets/webpack/test-app/app/main.jit.ts b/tests/e2e/assets/webpack/test-app/app/main.jit.ts new file mode 100644 index 000000000000..4f083729991e --- /dev/null +++ b/tests/e2e/assets/webpack/test-app/app/main.jit.ts @@ -0,0 +1,5 @@ +import 'reflect-metadata'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {AppModule} from './app.module'; + +platformBrowserDynamic().bootstrapModule(AppModule); diff --git a/tests/e2e/assets/webpack/test-app/index.html b/tests/e2e/assets/webpack/test-app/index.html new file mode 100644 index 000000000000..89fb0893c35d --- /dev/null +++ b/tests/e2e/assets/webpack/test-app/index.html @@ -0,0 +1,12 @@ + + + + Document + + + + + + + + diff --git a/tests/e2e/assets/webpack/test-app/package.json b/tests/e2e/assets/webpack/test-app/package.json new file mode 100644 index 000000000000..a82238a71050 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app/package.json @@ -0,0 +1,26 @@ +{ + "name": "test", + "license": "MIT", + "dependencies": { + "@angular/common": "^2.0.0", + "@angular/compiler": "^2.0.0", + "@angular/compiler-cli": "0.6.2", + "@angular/core": "^2.0.0", + "@angular/http": "^2.0.0", + "@angular/platform-browser": "^2.0.0", + "@angular/platform-browser-dynamic": "^2.0.0", + "@angular/platform-server": "^2.0.0", + "@angular/router": "^3.0.0", + "core-js": "^2.4.1", + "rxjs": "^5.0.0-beta.12", + "zone.js": "^0.6.21" + }, + "devDependencies": { + "node-sass": "^3.7.0", + "performance-now": "^0.2.0", + "raw-loader": "^0.5.1", + "sass-loader": "^3.2.0", + "typescript": "2.0.2", + "webpack": "2.1.0-beta.22" + } +} diff --git a/tests/e2e/assets/webpack/test-app/tsconfig.json b/tests/e2e/assets/webpack/test-app/tsconfig.json new file mode 100644 index 000000000000..585586e38d21 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "module": "es2015", + "moduleResolution": "node", + "target": "es5", + "noImplicitAny": false, + "sourceMap": true, + "mapRoot": "", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "lib": [ + "es2015", + "dom" + ], + "outDir": "lib", + "skipLibCheck": true, + "rootDir": "." + }, + "angularCompilerOptions": { + "genDir": "./app/ngfactory", + "entryModule": "app/app.module#AppModule" + } +} diff --git a/tests/e2e/assets/webpack/test-app/webpack.config.js b/tests/e2e/assets/webpack/test-app/webpack.config.js new file mode 100644 index 000000000000..cfb6ea75f284 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app/webpack.config.js @@ -0,0 +1,31 @@ +const NgcWebpackPlugin = require('@ngtools/webpack').NgcWebpackPlugin; +const path = require('path'); + +module.exports = { + resolve: { + extensions: ['.ts', '.js'] + }, + entry: './app/main.aot.ts', + output: { + path: './dist', + publicPath: 'dist/', + filename: 'app.main.js' + }, + plugins: [ + new NgcWebpackPlugin({ + project: './tsconfig.json', + baseDir: path.resolve(__dirname, '') + }) + ], + module: { + loaders: [ + { test: /\.scss$/, loaders: ['raw-loader', 'sass-loader'] }, + { test: /\.css$/, loader: 'raw-loader' }, + { test: /\.html$/, loader: 'raw-loader' }, + { test: /\.ts$/, loader: '@ngtools/webpack' } + ] + }, + devServer: { + historyApiFallback: true + } +}; diff --git a/tests/e2e/setup/100-npm-link.ts b/tests/e2e/setup/100-npm-link.ts index fbe66d23b70c..396b974d94ec 100644 --- a/tests/e2e/setup/100-npm-link.ts +++ b/tests/e2e/setup/100-npm-link.ts @@ -1,5 +1,8 @@ import {join} from 'path'; import {npm, exec} from '../utils/process'; +import {updateJsonFile} from '../utils/project'; + +const packages = require('../../../lib/packages'); export default function (argv: any) { @@ -12,8 +15,24 @@ export default function (argv: any) { const distAngularCli = join(__dirname, '../../../dist/angular-cli'); const oldCwd = process.cwd(); process.chdir(distAngularCli); - return npm('link') - .then(() => process.chdir(oldCwd)); + + // Update the package.json of each packages. + return Promise.all(Object.keys(packages).map(pkgName => { + const p = packages[pkgName]; + + return updateJsonFile(join(p.dist, 'package.json'), json => { + for (const pkgName of Object.keys(packages)) { + if (json['dependencies'] && json['dependencies'][pkgName]) { + json['dependencies'][pkgName] = packages[pkgName].dist; + } + if (json['devDependencies'] && json['devDependencies'][pkgName]) { + json['devDependencies'][pkgName] = packages[pkgName].dist; + } + } + }); + })) + .then(() => npm('link')) + .then(() => process.chdir(oldCwd)); }) .then(() => exec(process.platform.startsWith('win') ? 'where' : 'which', 'ng')); } diff --git a/tests/e2e/setup/200-create-tmp-dir.ts b/tests/e2e/setup/200-create-tmp-dir.ts index e1a6dbfffc91..8cd88ec70a11 100644 --- a/tests/e2e/setup/200-create-tmp-dir.ts +++ b/tests/e2e/setup/200-create-tmp-dir.ts @@ -1,9 +1,12 @@ -const temp = require('temp'); +import {setGlobalVariable} from '../utils/env'; + +const temp = require('temp'); export default function(argv: any) { // Get to a temporary directory. let tempRoot = argv.reuse || temp.mkdirSync('angular-cli-e2e-'); console.log(` Using "${tempRoot}" as temporary directory for a new project.`); + setGlobalVariable('tmp-root', tempRoot); process.chdir(tempRoot); } diff --git a/tests/e2e/setup/300-log-environment.ts b/tests/e2e/setup/300-log-environment.ts new file mode 100644 index 000000000000..de8823beee8a --- /dev/null +++ b/tests/e2e/setup/300-log-environment.ts @@ -0,0 +1,21 @@ +import {node, ng, npm} from '../utils/process'; + +const packages = require('../../../lib/packages'); + + +export default function() { + return Promise.resolve() + .then(() => console.log('Environment:')) + .then(() => { + Object.keys(process.env).forEach(envName => { + console.log(` ${envName}: ${process.env[envName].replace(/[\n\r]+/g, '\n ')}`); + }); + }) + .then(() => { + console.log('Packages:'); + console.log(JSON.stringify(packages, null, 2)); + }) + .then(() => node('--version')) + .then(() => npm('--version')) + .then(() => ng('version')); +} diff --git a/tests/e2e/setup/500-create-project.ts b/tests/e2e/setup/500-create-project.ts index dd7fb0481a26..e2ff78992b76 100644 --- a/tests/e2e/setup/500-create-project.ts +++ b/tests/e2e/setup/500-create-project.ts @@ -9,8 +9,11 @@ import {gitClean, gitCommit} from '../utils/git'; export default function(argv: any) { let createProject = null; - // If we're set to reuse an existing project, just chdir to it and clean it. - if (argv.reuse) { + // This is a dangerous flag, but is useful for testing packages only. + if (argv.noproject) { + return Promise.resolve(); + } else if (argv.reuse) { + // If we're set to reuse an existing project, just chdir to it and clean it. createProject = Promise.resolve() .then(() => process.chdir(argv.reuse)) .then(() => gitClean()); @@ -29,6 +32,7 @@ export default function(argv: any) { json['devDependencies']['angular-cli'] = join(dist, 'angular-cli'); json['devDependencies']['@angular-cli/ast-tools'] = join(dist, 'ast-tools'); json['devDependencies']['@angular-cli/base-href-webpack'] = join(dist, 'base-href-webpack'); + json['devDependencies']['@ngtools/webpack'] = join(dist, 'webpack'); })) .then(() => { if (argv.nightly) { @@ -57,5 +61,6 @@ export default function(argv: any) { })) .then(() => git('config', 'user.email', 'angular-core+e2e@google.com')) .then(() => git('config', 'user.name', 'Angular CLI E2e')) + .then(() => git('config', 'commit.gpgSign', 'false')) .then(() => gitCommit('tsconfig-e2e-update')); } diff --git a/tests/e2e/tests/build/aot.ts b/tests/e2e/tests/build/aot.ts new file mode 100644 index 000000000000..0cfe62bc7364 --- /dev/null +++ b/tests/e2e/tests/build/aot.ts @@ -0,0 +1,8 @@ +import {ng} from '../../utils/process'; +import {expectFileToMatch} from '../../utils/fs'; + +export default function() { + return ng('build', '--aot') + .then(() => expectFileToMatch('dist/main.bundle.js', + /bootstrapModuleFactory.*\/\* AppModuleNgFactory \*\//)); +} diff --git a/tests/e2e/tests/build/fail-build.ts b/tests/e2e/tests/build/fail-build.ts new file mode 100644 index 000000000000..3bbc8eff3158 --- /dev/null +++ b/tests/e2e/tests/build/fail-build.ts @@ -0,0 +1,9 @@ +import {ng} from '../../utils/process'; +import {expectToFail} from '../../utils/utils'; +import {deleteFile} from '../../utils/fs'; + +export default function() { + return deleteFile('src/app/app.component.ts') + // This is supposed to fail since there's a missing file + .then(() => expectToFail(() => ng('build'))); +} diff --git a/tests/e2e/tests/build/output-dir.ts b/tests/e2e/tests/build/output-dir.ts index 837ee7f49ed0..0e34c8450fb9 100644 --- a/tests/e2e/tests/build/output-dir.ts +++ b/tests/e2e/tests/build/output-dir.ts @@ -2,11 +2,20 @@ import {ng} from '../../utils/process'; import {expectFileToExist} from '../../utils/fs'; import {expectToFail} from '../../utils/utils'; import {expectGitToBeClean} from '../../utils/git'; +import {updateJsonFile} from '../../utils/project'; export default function() { return ng('build', '-o', './build-output') .then(() => expectFileToExist('./build-output/index.html')) .then(() => expectFileToExist('./build-output/main.bundle.js')) + .then(() => expectToFail(expectGitToBeClean)) + .then(() => updateJsonFile('angular-cli.json', configJson => { + const app = configJson['apps'][0]; + app['outDir'] = 'config-build-output'; + })) + .then(() => ng('build')) + .then(() => expectFileToExist('./config-build-output/index.html')) + .then(() => expectFileToExist('./config-build-output/main.bundle.js')) .then(() => expectToFail(expectGitToBeClean)); } diff --git a/tests/e2e/tests/build/styles/less.ts b/tests/e2e/tests/build/styles/less.ts index 840d0bde2483..279742be2ceb 100644 --- a/tests/e2e/tests/build/styles/less.ts +++ b/tests/e2e/tests/build/styles/less.ts @@ -28,6 +28,6 @@ export default function() { .then(() => replaceInFile('src/app/app.component.ts', './app.component.css', './app.component.less')) .then(() => ng('build')) - .then(() => expectFileToMatch('dist/main.bundle.js', '.outer .inner')) + .then(() => expectFileToMatch('dist/main.bundle.js', /.outer.*.inner.*background:\s*#[fF]+/)) .then(() => moveFile('src/app/app.component.less', 'src/app/app.component.css')); } diff --git a/tests/e2e/tests/build/styles/scss.ts b/tests/e2e/tests/build/styles/scss.ts index 48b4792312a4..52d7d46fbd42 100644 --- a/tests/e2e/tests/build/styles/scss.ts +++ b/tests/e2e/tests/build/styles/scss.ts @@ -35,7 +35,7 @@ export default function() { .then(() => replaceInFile('src/app/app.component.ts', './app.component.css', './app.component.scss')) .then(() => ng('build')) - .then(() => expectFileToMatch('dist/main.bundle.js', '.outer .inner')) - .then(() => expectFileToMatch('dist/main.bundle.js', '.partial .inner')) + .then(() => expectFileToMatch('dist/main.bundle.js', /\.outer.*\.inner.*background.*#def/)) + .then(() => expectFileToMatch('dist/main.bundle.js', /\.partial.*\.inner.*background.*#def/)) .then(() => moveFile('src/app/app.component.scss', 'src/app/app.component.css')); } diff --git a/tests/e2e/tests/build/ts-paths.ts b/tests/e2e/tests/build/ts-paths.ts new file mode 100644 index 000000000000..f5e5b125854b --- /dev/null +++ b/tests/e2e/tests/build/ts-paths.ts @@ -0,0 +1,35 @@ +import {updateTsConfig} from '../../utils/project'; +import {writeMultipleFiles, appendToFile} from '../../utils/fs'; +import {ng} from '../../utils/process'; +import {stripIndents} from 'common-tags'; + + +export default function() { + return updateTsConfig(json => { + json['compilerOptions']['baseUrl'] = '.'; + json['compilerOptions']['paths'] = { + '@shared': [ + 'app/shared' + ], + '@shared/*': [ + 'app/shared/*' + ] + }; + }) + .then(() => writeMultipleFiles({ + 'src/app/shared/meaning.ts': 'export var meaning = 42;', + 'src/app/shared/index.ts': `export * from './meaning'` + })) + .then(() => appendToFile('src/app/app.component.ts', stripIndents` + import { meaning } from 'app/shared/meaning'; + import { meaning as meaning2 } from '@shared'; + import { meaning as meaning3 } from '@shared/meaning'; + + // need to use imports otherwise they are ignored and + // no error is outputted, even if baseUrl/paths don't work + console.log(meaning) + console.log(meaning2) + console.log(meaning3) + `)) + .then(() => ng('build')); +} diff --git a/tests/e2e/tests/generate/module/module-routing.ts b/tests/e2e/tests/generate/module/module-routing.ts index d339ebf01740..96ab4d7f64dc 100644 --- a/tests/e2e/tests/generate/module/module-routing.ts +++ b/tests/e2e/tests/generate/module/module-routing.ts @@ -9,7 +9,7 @@ export default function() { return ng('generate', 'module', 'test-module', '--routing') .then(() => expectFileToExist(moduleDir)) .then(() => expectFileToExist(join(moduleDir, 'test-module.module.ts'))) - .then(() => expectFileToExist(join(moduleDir, 'test-module.routing.ts'))) + .then(() => expectFileToExist(join(moduleDir, 'test-module-routing.module.ts'))) .then(() => expectFileToExist(join(moduleDir, 'test-module.component.ts'))) .then(() => expectFileToExist(join(moduleDir, 'test-module.component.spec.ts'))) .then(() => expectFileToExist(join(moduleDir, 'test-module.component.html'))) diff --git a/tests/e2e/tests/misc/lazy-module.ts b/tests/e2e/tests/misc/lazy-module.ts index 9f25a42e663f..ad7b4cfab6ac 100644 --- a/tests/e2e/tests/misc/lazy-module.ts +++ b/tests/e2e/tests/misc/lazy-module.ts @@ -12,8 +12,6 @@ export default function(argv: any) { } let oldNumberOfFiles = 0; - let currentNumberOfDistFiles = 0; - return Promise.resolve() .then(() => ng('build')) .then(() => oldNumberOfFiles = readdirSync('dist').length) @@ -22,8 +20,16 @@ export default function(argv: any) { RouterModule.forRoot([{ path: "lazy", loadChildren: "app/lazy/lazy.module#LazyModule" }]) `, '@angular/router')) .then(() => ng('build')) - .then(() => currentNumberOfDistFiles = readdirSync('dist').length) - .then(() => { + .then(() => readdirSync('dist').length) + .then(currentNumberOfDistFiles => { + if (oldNumberOfFiles >= currentNumberOfDistFiles) { + throw new Error('A bundle for the lazy module was not created.'); + } + }) + // Check for AoT and lazy routes. + .then(() => ng('build', '--aot')) + .then(() => readdirSync('dist').length) + .then(currentNumberOfDistFiles => { if (oldNumberOfFiles >= currentNumberOfDistFiles) { throw new Error('A bundle for the lazy module was not created.'); } diff --git a/tests/e2e/tests/packages/webpack/test.ts b/tests/e2e/tests/packages/webpack/test.ts new file mode 100644 index 000000000000..26c848e97636 --- /dev/null +++ b/tests/e2e/tests/packages/webpack/test.ts @@ -0,0 +1,29 @@ +import {copyAssets} from '../../../utils/assets'; +import {exec, silentNpm} from '../../../utils/process'; +import {updateJsonFile} from '../../../utils/project'; +import {join} from 'path'; +import {expectFileSizeToBeUnder} from '../../../utils/fs'; + + +export default function(argv: any, skipCleaning: () => void) { + const currentDir = process.cwd(); + + if (process.platform.startsWith('win')) { + // Disable the test on Windows. + return Promise.resolve(); + } + + return Promise.resolve() + .then(() => copyAssets('webpack/test-app')) + .then(dir => process.chdir(dir)) + .then(() => updateJsonFile('package.json', json => { + const dist = '../../../../../dist/'; + json['dependencies']['@ngtools/webpack'] = join(__dirname, dist, 'webpack'); + })) + .then(() => silentNpm('install')) + .then(() => exec('node_modules/.bin/webpack', '-p')) + .then(() => expectFileSizeToBeUnder('dist/app.main.js', 400000)) + .then(() => expectFileSizeToBeUnder('dist/0.app.main.js', 40000)) + .then(() => process.chdir(currentDir)) + .then(() => skipCleaning()); +} diff --git a/tests/e2e/utils/assets.ts b/tests/e2e/utils/assets.ts new file mode 100644 index 000000000000..309d25702638 --- /dev/null +++ b/tests/e2e/utils/assets.ts @@ -0,0 +1,29 @@ +import {join} from 'path'; +import * as glob from 'glob'; +import {getGlobalVariable} from './env'; +import {relative} from 'path'; +import {copyFile} from './fs'; + + +export function assetDir(assetName: string) { + return join(__dirname, '../assets', assetName); +} + + +export function copyAssets(assetName: string) { + const tempRoot = join(getGlobalVariable('tmp-root'), 'assets', assetName); + const root = assetDir(assetName); + + return Promise.resolve() + .then(() => { + const allFiles = glob.sync(join(root, '**/*'), { dot: true, nodir: true }); + + return allFiles.reduce((promise, filePath) => { + const relPath = relative(root, filePath); + const toPath = join(tempRoot, relPath); + + return promise.then(() => copyFile(filePath, toPath)); + }, Promise.resolve()); + }) + .then(() => tempRoot); +} diff --git a/tests/e2e/utils/ast.ts b/tests/e2e/utils/ast.ts index d5a5376f9996..b23be570a8fd 100644 --- a/tests/e2e/utils/ast.ts +++ b/tests/e2e/utils/ast.ts @@ -1,15 +1,16 @@ import { insertImport as _insertImport, - addImportToModule as _addImportToModule + addImportToModule as _addImportToModule, + NodeHost } from '@angular-cli/ast-tools'; export function insertImport(file: string, symbol: string, module: string) { return _insertImport(file, symbol, module) - .then(change => change.apply()); + .then(change => change.apply(NodeHost)); } export function addImportToModule(file: string, symbol: string, module: string) { return _addImportToModule(file, symbol, module) - .then(change => change.apply()); + .then(change => change.apply(NodeHost)); } diff --git a/tests/e2e/utils/env.ts b/tests/e2e/utils/env.ts new file mode 100644 index 000000000000..dd80596f0698 --- /dev/null +++ b/tests/e2e/utils/env.ts @@ -0,0 +1,13 @@ +const global: {[name: string]: any} = Object.create(null); + + +export function setGlobalVariable(name: string, value: any) { + global[name] = value; +} + +export function getGlobalVariable(name: string): any { + if (!(name in global)) { + throw new Error(`Trying to access variable "${name}" but it's not defined.`); + } + return global[name]; +} diff --git a/tests/e2e/utils/fs.ts b/tests/e2e/utils/fs.ts index 5f42c98f1b93..5ff15116456e 100644 --- a/tests/e2e/utils/fs.ts +++ b/tests/e2e/utils/fs.ts @@ -1,4 +1,6 @@ import * as fs from 'fs'; +import {dirname} from 'path'; +import {stripIndents} from 'common-tags'; export function readFile(fileName: string) { @@ -52,6 +54,30 @@ export function moveFile(from: string, to: string) { } +function _recursiveMkDir(path: string) { + if (fs.existsSync(path)) { + return Promise.resolve(); + } else { + return _recursiveMkDir(dirname(path)) + .then(() => fs.mkdirSync(path)); + } +} + +export function copyFile(from: string, to: string) { + return _recursiveMkDir(dirname(to)) + .then(() => new Promise((resolve, reject) => { + const rd = fs.createReadStream(from); + rd.on('error', (err) => reject(err)); + + const wr = fs.createWriteStream(to); + wr.on('error', (err) => reject(err)); + wr.on('close', (ex) => resolve()); + + rd.pipe(wr); + })); +} + + export function writeMultipleFiles(fs: any) { return Object.keys(fs) .reduce((previous, curr) => { @@ -67,6 +93,11 @@ export function replaceInFile(filePath: string, match: RegExp, replacement: stri } +export function appendToFile(filePath: string, text: string) { + return readFile(filePath) + .then((content: string) => writeFile(filePath, content.concat(text))); +} + export function expectFileToExist(fileName: string) { return new Promise((resolve, reject) => { @@ -85,12 +116,29 @@ export function expectFileToMatch(fileName: string, regEx: RegExp | string) { .then(content => { if (typeof regEx == 'string') { if (content.indexOf(regEx) == -1) { - throw new Error(`File "${fileName}" did not contain "${regEx}"...`); + throw new Error(stripIndents`File "${fileName}" did not contain "${regEx}"... + Content: + ${content} + ------ + `); } } else { if (!content.match(regEx)) { - throw new Error(`File "${fileName}" did not match regex ${regEx}...`); + throw new Error(stripIndents`File "${fileName}" did not contain "${regEx}"... + Content: + ${content} + ------ + `); } } }); } + +export function expectFileSizeToBeUnder(fileName: string, sizeInBytes: number) { + return readFile(fileName) + .then(content => { + if (content.length > sizeInBytes) { + throw new Error(`File "${fileName}" exceeded file size of "${sizeInBytes}".`); + } + }); +} diff --git a/tests/e2e/utils/process.ts b/tests/e2e/utils/process.ts index d8d9a69be1d8..4746453ec424 100644 --- a/tests/e2e/utils/process.ts +++ b/tests/e2e/utils/process.ts @@ -20,8 +20,15 @@ function _exec(options: ExecOptions, cmd: string, args: string[]): Promise x !== undefined); - - console.log(blue(` Running \`${cmd} ${args.map(x => `"${x}"`).join(' ')}\`...`)); + const flags = [ + options.silent && 'silent', + options.waitForMatch && `matching(${options.waitForMatch})` + ] + .filter(x => !!x) // Remove false and undefined. + .join(', ') + .replace(/^(.+)$/, ' [$1]'); // Proper formatting. + + console.log(blue(` Running \`${cmd} ${args.map(x => `"${x}"`).join(' ')}\`${flags}...`)); console.log(blue(` CWD: ${cwd}`)); const spawnOptions: any = {cwd}; @@ -31,8 +38,8 @@ function _exec(options: ExecOptions, cmd: string, args: string[]): Promise { + const childProcess = child_process.spawn(cmd, args, spawnOptions); + childProcess.stdout.on('data', (data: Buffer) => { stdout += data.toString('utf-8'); if (options.silent) { return; @@ -42,7 +49,7 @@ function _exec(options: ExecOptions, cmd: string, args: string[]): Promise line !== '') .forEach(line => console.log(' ' + line)); }); - npmProcess.stderr.on('data', (data: Buffer) => { + childProcess.stderr.on('data', (data: Buffer) => { stderr += data.toString('utf-8'); data.toString('utf-8') .split(/[\n\r]+/) @@ -50,13 +57,13 @@ function _exec(options: ExecOptions, cmd: string, args: string[]): Promise console.error(yellow(' ' + line))); }); - _processes.push(npmProcess); + _processes.push(childProcess); // Create the error here so the stack shows who called this function. const err = new Error(`Running "${cmd} ${args.join(' ')}" returned error code `); return new Promise((resolve, reject) => { - npmProcess.on('exit', (error: any) => { - _processes = _processes.filter(p => p !== npmProcess); + childProcess.on('exit', (error: any) => { + _processes = _processes.filter(p => p !== childProcess); if (!error) { resolve(stdout); @@ -67,7 +74,7 @@ function _exec(options: ExecOptions, cmd: string, args: string[]): Promise { + childProcess.stdout.on('data', (data: Buffer) => { if (data.toString().match(options.waitForMatch)) { resolve(stdout); } @@ -113,6 +120,10 @@ export function npm(...args: string[]) { return _exec({}, 'npm', args); } +export function node(...args: string[]) { + return _exec({}, 'node', args); +} + export function git(...args: string[]) { return _exec({}, 'git', args); } diff --git a/tests/e2e_runner.js b/tests/e2e_runner.js index d173b9080ad9..bc96d6f0f535 100644 --- a/tests/e2e_runner.js +++ b/tests/e2e_runner.js @@ -2,6 +2,8 @@ 'use strict'; require('../lib/bootstrap-local'); +Error.stackTraceLimit = Infinity; + /** * This file is ran using the command line, not using Jasmine / Mocha. */ @@ -20,6 +22,7 @@ const white = chalk.white; /** * Here's a short description of those flags: * --debug If a test fails, block the thread so the temporary directory isn't deleted. + * --noproject Skip creating a project or using one. * --nolink Skip linking your local angular-cli directory. Can save a few seconds. * --nightly Install angular nightly builds over the test project. * --reuse=/path Use a path instead of create a new project. That project should have been @@ -28,7 +31,7 @@ const white = chalk.white; * If unnamed flags are passed in, the list of tests will be filtered to include only those passed. */ const argv = minimist(process.argv.slice(2), { - 'boolean': ['debug', 'nolink', 'nightly'], + 'boolean': ['debug', 'nolink', 'nightly', 'noproject'], 'string': ['reuse'] }); @@ -53,9 +56,9 @@ const testsToRun = allSetups } return argv._.some(argName => { - return path.join(process.cwd(), argName) == path.join(__dirname, name) - || argName == name - || argName == name.replace(/\.ts$/, ''); + return path.join(process.cwd(), argName) == path.join(__dirname, 'e2e', name) + || argName == name + || argName == name.replace(/\.ts$/, ''); }); })); @@ -88,12 +91,14 @@ testsToRun.reduce((previous, relativeName) => { throw new Error('Invalid test module.'); }; + let clean = true; return Promise.resolve() .then(() => printHeader(currentFileName)) - .then(() => fn(argv)) + .then(() => fn(argv, () => clean = false)) .then(() => { - // Only clean after a real test, not a setup step. - if (allSetups.indexOf(relativeName) == -1) { + // Only clean after a real test, not a setup step. Also skip cleaning if the test + // requested an exception. + if (allSetups.indexOf(relativeName) == -1 && clean) { return gitClean(); } }) diff --git a/tsconfig.json b/tsconfig.json index 8a6e1ba0aad8..1055b93bdb0f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,7 @@ "rootDir": ".", "sourceMap": true, "sourceRoot": "", + "inlineSourceMap": true, "target": "es5", "lib": ["es6"], "baseUrl": "", @@ -24,7 +25,7 @@ "angular-cli/*": [ "./packages/angular-cli/*" ], "@angular-cli/ast-tools": [ "./packages/ast-tools/src" ], "@angular-cli/base-href-webpack": [ "./packages/base-href-webpack/src" ], - "@angular-cli/webpack": [ "./packages/webpack/src" ] + "@ngtools/webpack": [ "./packages/webpack/src" ] } }, "exclude": [