From c268b5889fa66c3f8f925edf11115cf1c03cbdad Mon Sep 17 00:00:00 2001 From: Jason Jean Date: Mon, 22 May 2017 17:40:42 -0400 Subject: [PATCH] feat(@ngtools/webpack): replace server bootstrap code (#5194) --- packages/@ngtools/webpack/src/loader.ts | 32 ++++++++++++++++--- .../test-server-app/app/app.component.html | 5 +++ .../test-server-app/app/app.component.scss | 3 ++ .../test-server-app/app/app.component.ts | 15 +++++++++ .../webpack/test-server-app/app/app.module.ts | 27 ++++++++++++++++ .../app/feature/feature.module.ts | 20 ++++++++++++ .../app/feature/lazy-feature.module.ts | 23 +++++++++++++ .../webpack/test-server-app/app/injectable.ts | 8 +++++ .../test-server-app/app/lazy.module.ts | 26 +++++++++++++++ .../webpack/test-server-app/app/main.ts | 5 +++ .../assets/webpack/test-server-app/index.html | 12 +++++++ .../webpack/test-server-app/package.json | 28 ++++++++++++++++ .../webpack/test-server-app/tsconfig.json | 24 ++++++++++++++ .../webpack/test-server-app/webpack.config.js | 30 +++++++++++++++++ tests/e2e/tests/packages/webpack/server.ts | 20 ++++++++++++ 15 files changed, 274 insertions(+), 4 deletions(-) create mode 100644 tests/e2e/assets/webpack/test-server-app/app/app.component.html create mode 100644 tests/e2e/assets/webpack/test-server-app/app/app.component.scss create mode 100644 tests/e2e/assets/webpack/test-server-app/app/app.component.ts create mode 100644 tests/e2e/assets/webpack/test-server-app/app/app.module.ts create mode 100644 tests/e2e/assets/webpack/test-server-app/app/feature/feature.module.ts create mode 100644 tests/e2e/assets/webpack/test-server-app/app/feature/lazy-feature.module.ts create mode 100644 tests/e2e/assets/webpack/test-server-app/app/injectable.ts create mode 100644 tests/e2e/assets/webpack/test-server-app/app/lazy.module.ts create mode 100644 tests/e2e/assets/webpack/test-server-app/app/main.ts create mode 100644 tests/e2e/assets/webpack/test-server-app/index.html create mode 100644 tests/e2e/assets/webpack/test-server-app/package.json create mode 100644 tests/e2e/assets/webpack/test-server-app/tsconfig.json create mode 100644 tests/e2e/assets/webpack/test-server-app/webpack.config.js create mode 100644 tests/e2e/tests/packages/webpack/server.ts diff --git a/packages/@ngtools/webpack/src/loader.ts b/packages/@ngtools/webpack/src/loader.ts index 97bd6b1feda4..5dd0c0f7ab4d 100644 --- a/packages/@ngtools/webpack/src/loader.ts +++ b/packages/@ngtools/webpack/src/loader.ts @@ -4,9 +4,25 @@ import {AotPlugin} from './plugin'; import {TypeScriptFileRefactor} from './refactor'; import {LoaderContext, ModuleReason} from './webpack'; +interface Platform { + name: string; + importLocation: string; +} + const loaderUtils = require('loader-utils'); const NormalModule = require('webpack/lib/NormalModule'); +// This is a map of changes which need to be made +const changeMap: {[key: string]: Platform} = { + platformBrowserDynamic: { + name: 'platformBrowser', + importLocation: '@angular/platform-browser' + }, + platformDynamicServer: { + name: 'platformServer', + importLocation: '@angular/platform-server' + } +}; function _getContentOfKeyLiteral(_source: ts.SourceFile, node: ts.Node): string { if (!node) { @@ -205,9 +221,10 @@ function _replaceBootstrap(plugin: AotPlugin, refactor: TypeScriptFileRefactor) = refactor.findAstNodes(access, ts.SyntaxKind.CallExpression, true) as ts.CallExpression[]; return previous.concat(expressions); }, []) + .filter((call: ts.CallExpression) => call.expression.kind == ts.SyntaxKind.Identifier) .filter((call: ts.CallExpression) => { - return call.expression.kind == ts.SyntaxKind.Identifier - && (call.expression as ts.Identifier).text == 'platformBrowserDynamic'; + // Find if the expression matches one of the replacement targets + return !!changeMap[(call.expression as ts.Identifier).text]; }); if (calls.length == 0) { @@ -222,7 +239,15 @@ function _replaceBootstrap(plugin: AotPlugin, refactor: TypeScriptFileRefactor) refactor.replaceNode(call.arguments[0], entryModule.className + 'NgFactory'); }); - calls.forEach(call => refactor.replaceNode(call.expression, 'platformBrowser')); + calls.forEach(call => { + const platform = changeMap[(call.expression as ts.Identifier).text]; + + // Replace with mapped replacement + refactor.replaceNode(call.expression, platform.name); + + // Add the appropriate import + refactor.insertImport(platform.name, platform.importLocation); + }); bootstraps .forEach((bs: ts.PropertyAccessExpression) => { @@ -230,7 +255,6 @@ function _replaceBootstrap(plugin: AotPlugin, refactor: TypeScriptFileRefactor) refactor.replaceNode(bs.name, 'bootstrapModuleFactory'); }); - refactor.insertImport('platformBrowser', '@angular/platform-browser'); refactor.insertImport(entryModule.className + 'NgFactory', ngFactoryPath); } diff --git a/tests/e2e/assets/webpack/test-server-app/app/app.component.html b/tests/e2e/assets/webpack/test-server-app/app/app.component.html new file mode 100644 index 000000000000..5a532db9308f --- /dev/null +++ b/tests/e2e/assets/webpack/test-server-app/app/app.component.html @@ -0,0 +1,5 @@ +
+

hello world

+ lazy + +
diff --git a/tests/e2e/assets/webpack/test-server-app/app/app.component.scss b/tests/e2e/assets/webpack/test-server-app/app/app.component.scss new file mode 100644 index 000000000000..5cde7b922336 --- /dev/null +++ b/tests/e2e/assets/webpack/test-server-app/app/app.component.scss @@ -0,0 +1,3 @@ +:host { + background-color: blue; +} diff --git a/tests/e2e/assets/webpack/test-server-app/app/app.component.ts b/tests/e2e/assets/webpack/test-server-app/app/app.component.ts new file mode 100644 index 000000000000..82a4059565d3 --- /dev/null +++ b/tests/e2e/assets/webpack/test-server-app/app/app.component.ts @@ -0,0 +1,15 @@ +import {Component, ViewEncapsulation} from '@angular/core'; +import {MyInjectable} from './injectable'; + + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class AppComponent { + constructor(public inj: MyInjectable) { + console.log(inj); + } +} diff --git a/tests/e2e/assets/webpack/test-server-app/app/app.module.ts b/tests/e2e/assets/webpack/test-server-app/app/app.module.ts new file mode 100644 index 000000000000..6f8407fdfd29 --- /dev/null +++ b/tests/e2e/assets/webpack/test-server-app/app/app.module.ts @@ -0,0 +1,27 @@ +import { NgModule, Component } from '@angular/core'; +import { ServerModule } from '@angular/platform-server'; +import { RouterModule } from '@angular/router'; +import { AppComponent } from './app.component'; + +@Component({ + selector: 'home-view', + template: 'home!' +}) +export class HomeView {} + + +@NgModule({ + declarations: [ + AppComponent, + HomeView + ], + imports: [ + ServerModule, + RouterModule.forRoot([ + {path: 'lazy', loadChildren: './lazy.module#LazyModule'}, + {path: '', component: HomeView} + ]) + ], + bootstrap: [AppComponent] +}) +export class AppModule { } diff --git a/tests/e2e/assets/webpack/test-server-app/app/feature/feature.module.ts b/tests/e2e/assets/webpack/test-server-app/app/feature/feature.module.ts new file mode 100644 index 000000000000..f464ca028b05 --- /dev/null +++ b/tests/e2e/assets/webpack/test-server-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-server-app/app/feature/lazy-feature.module.ts b/tests/e2e/assets/webpack/test-server-app/app/feature/lazy-feature.module.ts new file mode 100644 index 000000000000..8fafca158b24 --- /dev/null +++ b/tests/e2e/assets/webpack/test-server-app/app/feature/lazy-feature.module.ts @@ -0,0 +1,23 @@ +import {NgModule, Component} from '@angular/core'; +import {RouterModule} from '@angular/router'; +import {HttpModule, Http} from '@angular/http'; + +@Component({ + selector: 'lazy-feature-comp', + template: 'lazy feature!' +}) +export class LazyFeatureComponent {} + +@NgModule({ + imports: [ + RouterModule.forChild([ + {path: '', component: LazyFeatureComponent, pathMatch: 'full'}, + {path: 'feature', loadChildren: './feature.module#FeatureModule'} + ]), + HttpModule + ], + declarations: [LazyFeatureComponent] +}) +export class LazyFeatureModule { + constructor(http: Http) {} +} diff --git a/tests/e2e/assets/webpack/test-server-app/app/injectable.ts b/tests/e2e/assets/webpack/test-server-app/app/injectable.ts new file mode 100644 index 000000000000..04d8486586c4 --- /dev/null +++ b/tests/e2e/assets/webpack/test-server-app/app/injectable.ts @@ -0,0 +1,8 @@ +import {Injectable, Inject, ViewContainerRef} from '@angular/core'; +import {DOCUMENT} from '@angular/platform-browser'; + + +@Injectable() +export class MyInjectable { + constructor(public viewContainer: ViewContainerRef, @Inject(DOCUMENT) public doc) {} +} diff --git a/tests/e2e/assets/webpack/test-server-app/app/lazy.module.ts b/tests/e2e/assets/webpack/test-server-app/app/lazy.module.ts new file mode 100644 index 000000000000..96da4de7515b --- /dev/null +++ b/tests/e2e/assets/webpack/test-server-app/app/lazy.module.ts @@ -0,0 +1,26 @@ +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'}, + {path: 'lazy-feature', loadChildren: './feature/lazy-feature.module#LazyFeatureModule'} + ]), + HttpModule + ], + declarations: [LazyComponent] +}) +export class LazyModule { + constructor(http: Http) {} +} + +export class SecondModule {} diff --git a/tests/e2e/assets/webpack/test-server-app/app/main.ts b/tests/e2e/assets/webpack/test-server-app/app/main.ts new file mode 100644 index 000000000000..9aae9848fbcc --- /dev/null +++ b/tests/e2e/assets/webpack/test-server-app/app/main.ts @@ -0,0 +1,5 @@ +import 'core-js/es7/reflect'; +import {platformDynamicServer} from '@angular/platform-server'; +import {AppModule} from './app.module'; + +platformDynamicServer().bootstrapModule(AppModule); diff --git a/tests/e2e/assets/webpack/test-server-app/index.html b/tests/e2e/assets/webpack/test-server-app/index.html new file mode 100644 index 000000000000..89fb0893c35d --- /dev/null +++ b/tests/e2e/assets/webpack/test-server-app/index.html @@ -0,0 +1,12 @@ + + + + Document + + + + + + + + diff --git a/tests/e2e/assets/webpack/test-server-app/package.json b/tests/e2e/assets/webpack/test-server-app/package.json new file mode 100644 index 000000000000..664d1312ce8b --- /dev/null +++ b/tests/e2e/assets/webpack/test-server-app/package.json @@ -0,0 +1,28 @@ +{ + "name": "test", + "license": "MIT", + "dependencies": { + "@angular/animations": "^4.0.0", + "@angular/common": "^4.0.0", + "@angular/compiler": "^4.0.0", + "@angular/compiler-cli": "^4.0.0", + "@angular/core": "^4.0.0", + "@angular/http": "^4.0.0", + "@angular/platform-browser": "^4.0.0", + "@angular/platform-browser-dynamic": "^4.0.0", + "@angular/platform-server": "^4.0.0", + "@angular/router": "^4.0.0", + "@ngtools/webpack": "0.0.0", + "core-js": "^2.4.1", + "rxjs": "^5.3.1", + "zone.js": "^0.8.10" + }, + "devDependencies": { + "node-sass": "^4.5.0", + "performance-now": "^0.2.0", + "raw-loader": "^0.5.1", + "sass-loader": "^6.0.3", + "typescript": "^2.3.2", + "webpack": "2.2.1" + } +} diff --git a/tests/e2e/assets/webpack/test-server-app/tsconfig.json b/tests/e2e/assets/webpack/test-server-app/tsconfig.json new file mode 100644 index 000000000000..cee38681d901 --- /dev/null +++ b/tests/e2e/assets/webpack/test-server-app/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "baseUrl": "", + "module": "es2015", + "moduleResolution": "node", + "target": "es5", + "noImplicitAny": false, + "sourceMap": true, + "mapRoot": "", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "lib": [ + "es2016", + "dom" + ], + "outDir": "lib", + "skipLibCheck": true, + "rootDir": "." + }, + "angularCompilerOptions": { + "genDir": "./app/ngfactory", + "entryModule": "app/app.module#AppModule" + } +} diff --git a/tests/e2e/assets/webpack/test-server-app/webpack.config.js b/tests/e2e/assets/webpack/test-server-app/webpack.config.js new file mode 100644 index 000000000000..8694fa6a6900 --- /dev/null +++ b/tests/e2e/assets/webpack/test-server-app/webpack.config.js @@ -0,0 +1,30 @@ +const ngToolsWebpack = require('@ngtools/webpack'); + +module.exports = { + resolve: { + extensions: ['.ts', '.js'] + }, + target: 'web', + entry: './app/main.ts', + output: { + path: './dist', + publicPath: 'dist/', + filename: 'app.main.js' + }, + plugins: [ + new ngToolsWebpack.AotPlugin({ + tsConfigPath: './tsconfig.json' + }) + ], + 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/tests/packages/webpack/server.ts b/tests/e2e/tests/packages/webpack/server.ts new file mode 100644 index 000000000000..1ccc7b73fba4 --- /dev/null +++ b/tests/e2e/tests/packages/webpack/server.ts @@ -0,0 +1,20 @@ +import {normalize} from 'path'; +import {createProjectFromAsset} from '../../../utils/assets'; +import {exec} from '../../../utils/process'; +import {expectFileToMatch} from '../../../utils/fs'; + + +export default function(skipCleaning: () => void) { + return Promise.resolve() + .then(() => createProjectFromAsset('webpack/test-server-app')) + .then(() => exec(normalize('node_modules/.bin/webpack'))) + .then(() => expectFileToMatch('dist/app.main.js', + new RegExp('.bootstrapModuleFactory')) + .then(() => expectFileToMatch('dist/app.main.js', + new RegExp('MyInjectable.ctorParameters = .*' + + 'type: .*ViewContainerRef.*' + + 'type: undefined, decorators.*Inject.*args: .*DOCUMENT.*')) + .then(() => expectFileToMatch('dist/app.main.js', + new RegExp('AppComponent.ctorParameters = .*MyInjectable')) + .then(() => skipCleaning()); +}