diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 98ab0c0fc..20b83bfe9 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,6 +4,7 @@ - [ ] Docs have been added / updated (for bug fixes / features) * **What modules are related to this pull-request** +- [ ] Express Engine * **What kind of change does this PR introduce?** (Bug fix, feature, docs update, ...) remove unused proxy imports from some test files diff --git a/modules/ng-express-engine/README.md b/modules/ng-express-engine/README.md new file mode 100644 index 000000000..371b30272 --- /dev/null +++ b/modules/ng-express-engine/README.md @@ -0,0 +1,71 @@ +# Angular Express Engine + +This is an Express Engine for running Angular Apps on the server for server side rendering. + +## Usage + +To use it, set the engine and then route requests to it + +```ts +import * as express from 'express'; +import { ngExpressEngine } from '@universal/ng-express-engine'; + +// Set the engine +app.engine('html', ngExpressEngine({ + bootstrap: ServerAppModule // Give it a module to bootstrap +})); + +app.set('view engine', 'html'); + +app.get('/**/*', (req: Request, res: Response) => { + req: req, + res: res +}); +``` + +## Extra Providers + +Extra Providers can be provided either on engine setup + +```ts +app.engine('html', ngExpressEngine({ + bootstrap: ServerAppModule, + providers: [ + ServerService + ] +})); +``` + +## Advanced Usage + +### Request based Bootstrap + +The Bootstrap module as well as more providers can be passed on request + +```ts +app.get('/**/*', (req: Request, res: Response) => { + req: req, + res: res, + bootstrap: OtherServerAppModule, + providers: [ + OtherServerService + ] +}); +``` + +### Using the Request and Response + +The Request and Response objects are injected into the app via injection tokens. +You can access them by @Inject + +```ts +import { Request } from 'express'; +import { REQUEST } from '@universal/ng-express-engine'; + +@Injectable() +export class RequestService { + constructor(@Inject(REQUEST) private request: Request) {} +} +``` + +If your app runs on the client side too, you will have to provide your own versions of these in the client app. diff --git a/modules/ng-express-engine/index.ts b/modules/ng-express-engine/index.ts new file mode 100644 index 000000000..bbc7f0ea3 --- /dev/null +++ b/modules/ng-express-engine/index.ts @@ -0,0 +1,2 @@ +export { ngExpressEngine, NgSetupOptions, RenderOptions } from './src/main'; +export { RESPONSE, REQUEST } from './src/tokens'; diff --git a/modules/ng-express-engine/package.json b/modules/ng-express-engine/package.json new file mode 100644 index 000000000..ae920c4e1 --- /dev/null +++ b/modules/ng-express-engine/package.json @@ -0,0 +1,47 @@ +{ + "name": "@universal/ng-express-engine", + "main": "dist/main.js", + "types": "dist/index.d.ts", + "version": "1.0.0-beta.0", + "description": "Express Engine for running Server Angular Apps", + "homepage": "https://github.com/angular/universal", + "license": "MIT", + "contributors": [ + "FrozenPandaz" + ], + "repository": { + "type": "git", + "url": "https://github.com/angular/universal" + }, + "bugs": { + "url": "https://github.com/angular/universal/issues" + }, + "config": { + "engine-strict": true + }, + "engines": { + "node": ">= 5.4.1 <= 7", + "npm": ">= 3" + }, + "scripts": { + "build": "tsc", + "prebuild": "rimraf dist" + }, + "peerDependencies": { + "@angular/core": "^4.0.0-rc.5", + "@angular/platform-server": "^4.0.0-rc.5", + "express": "^4.15.2" + }, + "devDependencies": { + "@angular/common": "^4.0.0-rc.5", + "@angular/compiler": "^4.0.0-rc.5", + "@angular/core": "^4.0.0-rc.5", + "@angular/platform-browser": "^4.0.0-rc.5", + "@angular/platform-server": "^4.0.0-rc.5", + "express": "^4.15.2", + "rimraf": "^2.6.1", + "rxjs": "^5.2.0", + "typescript": "^2.2.1", + "zone.js": "^0.8.4" + } +} diff --git a/modules/ng-express-engine/src/main.ts b/modules/ng-express-engine/src/main.ts new file mode 100644 index 000000000..9ca93dda1 --- /dev/null +++ b/modules/ng-express-engine/src/main.ts @@ -0,0 +1,98 @@ +import * as fs from 'fs'; +import { Request, Response, Send } from 'express'; + +import { Provider, NgModuleFactory } from '@angular/core'; +import { INITIAL_CONFIG, renderModuleFactory } from '@angular/platform-server'; + +import { REQUEST, RESPONSE } from './tokens'; + +/** + * These are the allowed options for the engine + */ +export interface NgSetupOptions { + bootstrap: NgModuleFactory<{}>; + providers?: Provider[]; +} + +/** + * These are the allowed options for the render + */ +export interface RenderOptions extends NgSetupOptions { + req: Request; + res?: Response; +} + +/** + * This holds a cached version of each index used. + */ +const templateCache: { [key: string]: string } = {}; + +/** + * This is an express engine for handling Angular Applications + */ +export function ngExpressEngine(setupOptions: NgSetupOptions) { + + setupOptions.providers = setupOptions.providers || []; + + return function (filePath: string, options: RenderOptions, callback: Send) { + + options.providers = options.providers || []; + + try { + const moduleFactory = options.bootstrap || setupOptions.bootstrap; + + if (!module) { + throw new Error('You must pass in a NgModule or NgModuleFactory to be bootstrapped'); + } + + const extraProviders = setupOptions.providers.concat( + options.providers, + getReqResProviders(options.req, options.res), + [ + { + provide: INITIAL_CONFIG, + useValue: { + document: getDocument(filePath), + url: options.req.originalUrl + } + } + ]); + + renderModuleFactory(moduleFactory, { + extraProviders: extraProviders + }) + .then((html: string) => { + callback(null, html); + }); + + } catch (e) { + callback(e); + } + }; +} + +/** + * Get providers of the request and response + */ +function getReqResProviders(req: Request, res: Response): Provider[] { + const providers: Provider[] = [ + { + provide: REQUEST, + useValue: req + } + ]; + if (res) { + providers.push({ + provide: RESPONSE, + useValue: res + }); + } + return providers; +} + +/** + * Get the document at the file path + */ +function getDocument(filePath: string): string { + return templateCache[filePath] = templateCache[filePath] || fs.readFileSync(filePath).toString(); +} diff --git a/modules/ng-express-engine/src/tokens.ts b/modules/ng-express-engine/src/tokens.ts new file mode 100644 index 000000000..53fb3bbd9 --- /dev/null +++ b/modules/ng-express-engine/src/tokens.ts @@ -0,0 +1,5 @@ +import { Request, Response } from 'express'; +import { InjectionToken } from '@angular/core'; + +export const REQUEST = new InjectionToken('REQUEST'); +export const RESPONSE = new InjectionToken('RESPONSE'); diff --git a/modules/ng-express-engine/tsconfig.json b/modules/ng-express-engine/tsconfig.json new file mode 100644 index 000000000..124b58bf8 --- /dev/null +++ b/modules/ng-express-engine/tsconfig.json @@ -0,0 +1,37 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "es2015", + "moduleResolution": "node", + "declaration": true, + "noImplicitAny": false, + "noUnusedLocals": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedParameters": true, + "removeComments": false, + "baseUrl": ".", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "sourceMap": true, + "inlineSources": true, + "rootDir": ".", + "outDir": "dist", + "lib": [ + "dom", + "es6" + ], + "types": [ + "jasmine", + "node" + ] + }, + "files": [ + "index.ts" + ], + "compileOnSave": false, + "buildOnSave": false, + "atom": { + "rewriteTsconfig": false + } +} \ No newline at end of file diff --git a/package.json b/package.json index 1adff6fb7..68e844c36 100644 --- a/package.json +++ b/package.json @@ -63,5 +63,15 @@ "test": "exit 0" }, "devDependencies": { + "@angular/common": "^4.0.0-rc.5", + "@angular/compiler": "^4.0.0-rc.5", + "@angular/core": "^4.0.0-rc.5", + "@angular/platform-browser": "^4.0.0-rc.5", + "@angular/platform-server": "^4.0.0-rc.5", + "express": "^4.15.2", + "rimraf": "^2.6.1", + "rxjs": "^5.2.0", + "typescript": "^2.2.1", + "zone.js": "^0.8.4" } }