├── .gitignore ├── .nvmrc ├── .travis.yml ├── LICENSE.md ├── Makefile ├── README.md ├── jest.config.js ├── package.json ├── src ├── index.ts ├── injection │ ├── index.ts │ └── tokens.ts └── module │ ├── index.ts │ ├── interfaces │ ├── config.ts │ ├── index.ts │ └── options.ts │ ├── ng-universal.module.ts │ ├── routes │ ├── index.ts │ └── universal │ │ ├── html.route.ts │ │ └── index.ts │ └── services │ ├── engine │ ├── index.ts │ └── ng.service.ts │ ├── index.ts │ ├── reply │ ├── http-server.reply.ts │ └── index.ts │ └── utils │ ├── http.utils.ts │ └── index.ts ├── test └── unit │ ├── html-universal.route.test.ts │ ├── http-server-reply.service.test.ts │ ├── http-utils.service.test.ts │ ├── ng-engine.service.test.ts │ └── ng-universal.module.test.ts ├── tools ├── files.json └── packaging.ts ├── tsconfig.build.json ├── tsconfig.build.tokens.json ├── tsconfig.json ├── tslint.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | 10 | # IDEs and editors 11 | .idea 12 | .project 13 | .classpath 14 | .c9/ 15 | *.launch 16 | .settings/ 17 | .vscode 18 | 19 | # misc 20 | /.sass-cache 21 | /connect.lock 22 | /coverage 23 | /libpeerconnection.log 24 | npm-debug.log 25 | testem.log 26 | /typings 27 | 28 | 29 | #System Files 30 | .DS_Store 31 | Thumbs.db -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 11.0.0 -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | script: 5 | - yarn run test 6 | after_script: 7 | - yarn run coveralls -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ==== 3 | 4 | **Copyright (c) 2017 [Hapiness](https://github.com/hapiness)** 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | pretest: 2 | @node ./node_modules/.bin/tslint -p ./tsconfig.json "./src/**/*.ts" "./test/**/*.ts" 3 | test: 4 | @node node_modules/.bin/jest 5 | coveralls: 6 | cat ./coverage/lcov.info | node ./node_modules/.bin/coveralls 7 | tsc: 8 | @node ./node_modules/.bin/tsc -p ./tsconfig.build.json 9 | ngc-tokens: 10 | @node ./node_modules/.bin/ngc -p ./tsconfig.build.tokens.json 11 | clean: 12 | @node ./node_modules/.bin/rimraf ./dist 13 | packaging: 14 | @node ./node_modules/.bin/ts-node ./tools/packaging.ts 15 | 16 | .PHONY: pretest test coveralls tsc ngc-tokens clean packaging -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Hapiness 2 | 3 |
4 |
5 | 6 | build 7 | 8 | 9 | coveralls 10 | 11 | 12 | dependencies 13 | 14 | 15 | devDependencies 16 | 17 |
18 |
19 | 20 | Typescript logo 22 | 23 | 24 | ReactiveX logo 26 | 27 | 28 | Fastify logo 30 | 31 | 32 | Angular logo 34 | 35 |
36 |
37 | 38 | # NG-Universal 39 | 40 | This is a [Hapiness](https://github.com/hapinessjs/hapiness) Engine for running [Angular](https://www.angular.io) Apps on the server for server side rendering. 41 | 42 |
43 | 44 | # Integrating NG-Universal into existing CLI Applications 45 | 46 | This story will show you how to set up Universal bundling for an existing `@angular/cli`. 47 | 48 | We support actually `@angular` `@8.1.0` and next so you must upgrade all packages inside your project. 49 | 50 | We use `yarn` as package manager. 51 | 52 | ## Table of contents 53 | 54 | - [Install Dependencies](#install-dependencies) 55 | - [Step 1: Prepare your App for Universal rendering](#step-1-prepare-your-app-for-universal-rendering) 56 | - [src/app/app.module.ts](#srcappappmodulets) 57 | - [src/app/app.server.module.ts](#srcappappservermodulets) 58 | - [src/main.ts](#srcmaints) 59 | - [Step 2: Create a server "main" file and tsconfig to build it](#step-2-create-a-server-main-file-and-tsconfig-to-build-it) 60 | - [src/main.server.ts](#srcmainserverts) 61 | - [./tsconfig.server.json](#tsconfigserverjson) 62 | - [Step 3: Create a new target in angular.json](#step-3-create-a-new-target-in-angularjson) 63 | - [angular.json](#angularjson) 64 | - [Building the bundle](#building-the-bundle) 65 | - [Step 4: Setting up a Hapiness Application to run our Universal bundles](#step-4-setting-up-a-hapiness-application-to-run-our-universal-bundles) 66 | - [./server.ts (root project level)](#serverts-root-project-level) 67 | - [Extra Providers](#extra-providers) 68 | - [Using the Request, Reply and Utils](#using-the-requestreply-and-utils) 69 | - [Step 5: Setup a webpack config to handle this Node server.ts file and serve your application!](#step-5-setup-a-webpack-config-to-handle-this-node-serverts-file-and-serve-your-application) 70 | - [./webpack.server.config.js (root project level)](#webpackserverconfigjs-root-project-level) 71 | - [Almost there](#almost-there) 72 | - [Contributing](#contributing) 73 | - [Change History](#change-history) 74 | - [Maintainers](#maintainers) 75 | - [License](#license) 76 | 77 |
78 | 79 | ## Install Dependencies 80 | 81 | Install `@angular/platform-server` into your project. Make sure you use the same version as the other `@angular` packages in your project. 82 | 83 | Install [Hapiness](https://github.com/hapinessjs/hapiness) modules into your project: [`@hapiness/core`](https://github.com/hapinessjs/hapiness), [`@hapiness/ng-universal`](https://github.com/hapinessjs/ng-universal-module) and [`@hapiness/ng-universal-transfer-http`](https://github.com/hapinessjs/ng-universal-transfer-http). 84 | 85 | > You also need : 86 | > - `ts-loader` and `webpack`, `webpack-cli` for your webpack build we'll show later and it's only in `devDependencies`. 87 | > - `@nguniversal/module-map-ngfactory-loader`, as it's used to handle lazy-loading in the context of a server-render. (by loading the chunks right away) 88 | 89 | 90 | ```bash 91 | $ yarn add --dev ts-loader webpack webpack-cli 92 | $ yarn add @angular/platform-server @nguniversal/module-map-ngfactory-loader @hapiness/core @hapiness/ng-universal @hapiness/ng-universal-transfer-http 93 | ``` 94 | 95 | ## Step 1: Prepare your App for Universal rendering 96 | 97 | The first thing you need to do is make your `AppModule` compatible with Universal by adding `.withServerTransition()` and an application ID to your `BrowserModule` import. 98 | 99 | `TransferHttpCacheModule` installs a Http interceptor that avoids duplicate `HttpClient` requests on the client, for requests that were already made when the application was rendered on the server side. 100 | 101 | When the module is installed in the application `NgModule`, it will intercept `HttpClient` requests on the server and store the response in the `TransferState` key-value store. This is transferred to the client, which then uses it to respond to the same `HttpClient` requests on the client. 102 | 103 | To use the `TransferHttpCacheModule` just install it as part of the top-level App module. 104 | 105 | ### src/app/app.module.ts: 106 | 107 | ```typescript 108 | import { BrowserModule } from '@angular/platform-browser'; 109 | import { NgModule } from '@angular/core'; 110 | import { TransferHttpCacheModule } from '@hapiness/ng-universal-transfer-http'; 111 | 112 | import { AppComponent } from './app.component'; 113 | 114 | @NgModule({ 115 | declarations: [ 116 | AppComponent 117 | ], 118 | imports: [ 119 | // Add .withServerTransition() to support Universal rendering. 120 | // The application ID can be any identifier which is unique on 121 | // the page. 122 | BrowserModule.withServerTransition({ appId: 'ng-universal-example' }), 123 | // Add TransferHttpCacheModule to install a Http interceptor 124 | TransferHttpCacheModule 125 | ], 126 | providers: [], 127 | bootstrap: [AppComponent] 128 | }) 129 | export class AppModule { 130 | } 131 | ``` 132 | 133 | Next, create a module specifically for your application when running on the server. It's recommended to call this module `AppServerModule`. 134 | 135 | This example places it alongside `app.module.ts` in a file named `app.server.module.ts`: 136 | 137 | ### src/app/app.server.module.ts: 138 | 139 | ```typescript 140 | import { NgModule } from '@angular/core'; 141 | import { ServerModule, ServerTransferStateModule } from '@angular/platform-server'; 142 | import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader'; 143 | 144 | import { AppModule } from './app.module'; 145 | import { AppComponent } from './app.component'; 146 | 147 | @NgModule({ 148 | imports: [ 149 | // The AppServerModule should import your AppModule followed 150 | // by the ServerModule from @angular/platform-server. 151 | AppModule, 152 | ServerModule, 153 | ModuleMapLoaderModule, 154 | ServerTransferStateModule 155 | ], 156 | // Since the bootstrapped component is not inherited from your 157 | // imported AppModule, it needs to be repeated here. 158 | bootstrap: [AppComponent] 159 | }) 160 | export class AppServerModule { 161 | } 162 | ``` 163 | 164 | Then, you must set an event on `DOMContentLoaded` to be sure `TransferState` will be passed between `server` and `client`. 165 | 166 | ### src/main.ts: 167 | 168 | ```typescript 169 | import { enableProdMode } from '@angular/core'; 170 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 171 | 172 | import { AppModule } from './app/app.module'; 173 | import { environment } from './environments/environment'; 174 | 175 | if (environment.production) { 176 | enableProdMode(); 177 | } 178 | 179 | document.addEventListener('DOMContentLoaded', () => { 180 | platformBrowserDynamic().bootstrapModule(AppModule) 181 | .catch(err => console.log(err)); 182 | }); 183 | ``` 184 | 185 | [back to top](#table-of-contents) 186 | 187 |
188 | 189 | ## Step 2: Create a server "main" file and tsconfig to build it 190 | 191 | Create a main file for your Universal bundle. This file only needs to export your `AppServerModule`. It can go in `src`. This example calls this file `main.server.ts`: 192 | 193 | ### src/main.server.ts: 194 | 195 | ```typescript 196 | import { enableProdMode } from '@angular/core'; 197 | 198 | import { environment } from './environments/environment'; 199 | 200 | if (environment.production) { 201 | enableProdMode(); 202 | } 203 | 204 | export { AppServerModule } from './app/app.server.module'; 205 | 206 | export { NgUniversalModule } from '@hapiness/ng-universal'; 207 | ``` 208 | 209 | Copy `tsconfig.app.json` to `tsconfig.server.json` and change it to build with a `"module"` target of `"commonjs"`. 210 | 211 | Add a section for `"angularCompilerOptions"` and set `"entryModule"` to your `AppServerModule`, specified as a path to the import with a hash (`#`) containing the symbol name. In this example, this would be `src/app/app.server.module#AppServerModule`. 212 | 213 | ### src/tsconfig.server.json: 214 | 215 | ``` 216 | { 217 | "extends": "./tsconfig.json", 218 | "compilerOptions": { 219 | "outDir": "./out-tsc/app", 220 | "baseUrl": "./", 221 | "module": "commonjs", 222 | "types": [] 223 | }, 224 | "include": [ 225 | "src/**/*.ts" 226 | ], 227 | "exclude": [ 228 | "test.ts", 229 | "**/*.spec.ts" 230 | ], 231 | "angularCompilerOptions": { 232 | "entryModule": "src/app/app.server.module#AppServerModule" 233 | } 234 | } 235 | ``` 236 | 237 | [back to top](#table-of-contents) 238 | 239 |
240 | 241 | ## Step 3: Create a new target in `angular.json` 242 | 243 | In `angular.json` locate the **architect** property inside your project, and add a new **server target**. 244 | 245 | In **build target**, adapt `options.outputPath` to `dist/browser`. 246 | 247 | ### angular.json: 248 | 249 | ``` 250 | { 251 | ... 252 | "architect": { 253 | "build": { 254 | "builder": "@angular-devkit/build-angular:browser", 255 | "options: { 256 | "outputPath": "dist/browser", 257 | ... 258 | }, 259 | ... 260 | } 261 | "server": { 262 | "builder": "@angular-devkit/build-angular:server", 263 | "options": { 264 | "outputPath": "dist/server", 265 | "main": "src/main.server.ts", 266 | "tsConfig": "tsconfig.server.json" 267 | }, 268 | "configurations": { 269 | "production": { 270 | "fileReplacements": [ 271 | { 272 | "replace": "src/environments/environment.ts", 273 | "with": "src/environments/environment.prod.ts" 274 | } 275 | ] 276 | } 277 | } 278 | } 279 | } 280 | ... 281 | } 282 | ``` 283 | 284 | ### Building the bundle: 285 | 286 | With these steps complete, you should be able to build a server bundle for your application: 287 | 288 | ```bash 289 | # This builds the client application in dist/browser/ 290 | $ ng build --prod 291 | ... 292 | # This builds the server bundle in dist/server/ 293 | $ ng run your-project-name:server 294 | 295 | # outputs: 296 | Date: 2017-10-21T21:54:49.240Z 297 | Hash: 3034f2772435757f234a 298 | Time: 3689ms 299 | chunk {0} main.js (main) 9.2 kB [entry] [rendered] 300 | chunk {1} styles.css (styles) 0 bytes [entry] [rendered] 301 | ``` 302 | 303 | [back to top](#table-of-contents) 304 | 305 |
306 | 307 | ## Step 4: Setting up a Hapiness Application to run our Universal bundles 308 | 309 | Now that we have everything set up to -make- the bundles, how we get everything running? 310 | 311 | We'll use Hapiness application and `@hapiness/ng-universal` module. 312 | 313 | Below we can see a TypeScript implementation of a -very- simple Hapiness application to fire everything up. 314 | 315 | > **Note:** 316 | > 317 | > This is a very bare bones Hapiness application, and is just for demonstrations sake. 318 | > 319 | > In a real production environment, you'd want to make sure you have other authentication and security things setup here as well. 320 | > 321 | > This is just meant just to show the specific things needed that are relevant to Universal itself. The rest is up to you! 322 | 323 | At the ROOT level of your project (where package.json / etc are), created a file named: `server.ts` 324 | 325 | ### server.ts (root project level): 326 | 327 | ```typescript 328 | // This is important and needed before anything else 329 | import 'zone.js/dist/zone-node'; 330 | 331 | import { Hapiness, Module } from '@hapiness/core'; 332 | import { HttpServer, HttpServerConfig } from '@hapiness/core/httpserver'; 333 | import { join } from 'path'; 334 | 335 | const BROWSER_FOLDER = join(process.cwd(), 'dist', 'browser'); 336 | 337 | // * NOTE :: leave this as require() since this file is built Dynamically from webpack 338 | const { AppServerModuleNgFactory, LAZY_MODULE_MAP, NgUniversalModule} = require('./dist/server/main'); 339 | 340 | // Create our Hapiness application 341 | @Module({ 342 | version: '1.0.0', 343 | imports: [ 344 | NgUniversalModule.setConfig({ 345 | bootstrap: AppServerModuleNgFactory, 346 | lazyModuleMap: LAZY_MODULE_MAP, 347 | staticContent: { 348 | indexFile: 'index.html', 349 | rootPath: BROWSER_FOLDER 350 | } 351 | }) 352 | ] 353 | }) 354 | class HapinessApplication { 355 | /** 356 | * OnStart process 357 | */ 358 | onStart(): void { 359 | console.log(`SSR application is running`); 360 | } 361 | 362 | /** 363 | * OnError process 364 | */ 365 | onError(error: Error): void { 366 | console.error(error); 367 | } 368 | } 369 | 370 | 371 | // Boostrap Hapiness application 372 | Hapiness.bootstrap(HapinessApplication, [ 373 | HttpServer.setConfig({ 374 | host: '0.0.0.0', 375 | port: 4000 376 | }) 377 | ]); 378 | ``` 379 | 380 | ### Extra Providers: 381 | 382 | Extra Providers can be provided either on engine setup 383 | 384 | ```typescript 385 | NgUniversalModule.setConfig({ 386 | bootstrap: AppServerModuleNgFactory, 387 | lazyModuleMap: LAZY_MODULE_MAP, 388 | staticContent: { 389 | indexFile: 'index.html', 390 | rootPath: BROWSER_FOLDER 391 | }, 392 | providers: [ 393 | ServerService 394 | ] 395 | }) 396 | ``` 397 | 398 | ### Using the Request, Reply and Utils: 399 | 400 | The `Request`, `Reply` and `Utils` objects are injected into the app via injection tokens (`REQUEST`, `REPLY` and `UTILS`). You can access them by `@Inject` 401 | 402 | ```typescript 403 | import { Inject, Injectable } from '@angular/core'; 404 | import { HttpServerRequest, REQUEST } from '@hapiness/ng-universal'; 405 | 406 | @Injectable() 407 | export class RequestService { 408 | constructor(@Inject(REQUEST) private _request: HttpServerRequest) {} 409 | } 410 | ``` 411 | 412 | If your app runs on the `client` side too, you will have to provide your own versions of these in the client app. 413 | 414 | - `REQUEST` token will inject `HttpServerRequest` the current instance of [Fastify Request](https://www.fastify.io/docs/latest/Request/). 415 | - `REPLY` token will inject `HttpServerReply` current instance provides: 416 | - `header(key: string, value: string): HttpServerReply` method to add `new header` in `SSR` response 417 | - `redirect(url: string): HttpServerReply` method to `redirect` the response with a `302` to the given `URL`. 418 | - `UTILS` token will inject `HttpUtils` current instance provides: 419 | - `parseCookie(str: string, options?: any)` method which is the same of original `cookie` [library](https://github.com/jshttp/cookie#cookieparsestr-options). 420 | - `serializeCookie(name: string, value: string, options?: any)` method which is the same of original `cookie` [library](https://github.com/jshttp/cookie#cookieparsestr-options). 421 | 422 | [back to top](#table-of-contents) 423 | 424 |
425 | 426 | ## Step 5: Setup a webpack config to handle this Node server.ts file and serve your application! 427 | 428 | Now that we have our Hapiness application setup, we need to pack it and serve it! 429 | 430 | Create a file named `webpack.server.config.js` at the ROOT of your application. 431 | 432 | > This file basically takes that `server.ts` file, and takes it and compiles it and every dependency it has into `dist/server.js`. 433 | 434 | ### ./webpack.server.config.js (root project level): 435 | 436 | ```javascript 437 | const path = require('path'); 438 | const webpack = require('webpack'); 439 | 440 | module.exports = { 441 | mode: 'none', 442 | entry: { server: './server.ts' }, 443 | target: 'node', 444 | resolve: { 445 | extensions: [ '.ts', '.js' ] 446 | }, 447 | optimization: { 448 | minimize: false 449 | }, 450 | output: { 451 | path: path.join(__dirname, 'dist'), 452 | filename: '[name].js', 453 | libraryTarget: "commonjs" 454 | }, 455 | module: { 456 | noParse: /polyfills-.*\.js/, 457 | rules: [ 458 | { test: /\.ts$/, loader: 'ts-loader' }, 459 | { 460 | // Mark files inside `@angular/core` as using SystemJS style dynamic imports. 461 | // Removing this will cause deprecation warnings to appear. 462 | test: /(\\|\/)@angular(\\|\/)core(\\|\/).+\.js$/, 463 | parser: { system: true }, 464 | } 465 | ] 466 | }, 467 | plugins: [ 468 | // Temporary Fix for issue: https://github.com/angular/angular/issues/11580 469 | // for "WARNING Critical dependency: the request of a dependency is an expression" 470 | new webpack.ContextReplacementPlugin( 471 | /(.+)?angular(\\|\/)core(.+)?/, 472 | path.join(__dirname, 'src'), // location of your src 473 | {} // a map of your routes 474 | ), 475 | new webpack.ContextReplacementPlugin( 476 | /(.+)?hapiness(\\|\/)(.+)?/, 477 | path.join(__dirname, 'src'), 478 | {} 479 | ) 480 | ], 481 | stats: { 482 | warnings: false 483 | } 484 | }; 485 | ``` 486 | 487 | You can add this config if you want to use `@hapiness/config` to have server config in `./config/default.yml` instead of static data: 488 | 489 | ```javascript 490 | externals: [ 491 | { 492 | // This is the only module you have to install with npm in your final packaging 493 | // npm i config 494 | config: { 495 | commonjs: 'config', 496 | root: 'config' 497 | } 498 | } 499 | ] 500 | ``` 501 | 502 | And replace the `bootstrap` in *./server.ts* 503 | 504 | ```typescript 505 | import { Config } from '@hapiness/config'; 506 | 507 | // Boostrap Hapiness application 508 | Hapiness.bootstrap(HapinessApplication, [ 509 | HttpServer.setConfig(Config.get('server')) 510 | ]); 511 | ``` 512 | 513 | Now, you can build your server file: 514 | 515 | ```bash 516 | $ webpack --config webpack.server.config.js --progress --colors 517 | ``` 518 | 519 | #### Almost there: 520 | 521 | Now let's see what our resulting structure should look like, if we open up our `/dist/` folder we should see: 522 | 523 | ``` 524 | /dist/ 525 | /browser/ 526 | /server/ 527 | server.js 528 | ``` 529 | 530 | To fire up the application, in your terminal enter 531 | 532 | ```bash 533 | $ node dist/server.js 534 | ``` 535 | 536 | Now lets create a few handy scripts to help us do all of this in the future. 537 | 538 | ``` 539 | "scripts": { 540 | 541 | // These will be your common scripts 542 | "build:dynamic": "yarn run build:client-and-server-bundles && yarn run webpack:server", 543 | "serve:dynamic": "node dist/server.js", 544 | 545 | // Helpers for the above scripts 546 | "build:client-and-server-bundles": "ng build --prod && ng run your-project-name:server:production", 547 | "webpack:server": "webpack --config webpack.server.config.js --progress --colors" 548 | } 549 | ``` 550 | 551 | In the future when you want to see a Production build of your app with Universal (locally), you can simply run: 552 | 553 | ```bash 554 | $ yarn run build:dynamic && yarn run serve:dynamic 555 | ``` 556 | 557 | Enjoy! 558 | 559 | Once again to see a working version of everything, check out the [universal-starter](https://github.com/hapinessjs/ng-universal-example). 560 | 561 | 562 | [back to top](#table-of-contents) 563 | 564 |
565 | 566 | ## Contributing 567 | 568 | To set up your development environment: 569 | 570 | 1. clone the repo to your workspace, 571 | 2. in the shell `cd` to the main folder, 572 | 3. hit `npm or yarn install`, 573 | 4. run `npm or yarn run test`. 574 | * It will lint the code and execute all tests. 575 | * The test coverage report can be viewed from `./coverage/lcov-report/index.html`. 576 | 577 | [Back to top](#table-of-contents) 578 | 579 | ## Change History 580 | * v8.1.0 (2019-07-04) 581 | * `Angular v8.1.0+` 582 | * Documentation to allow dynamic import syntax directly to load lazy loaded chunks 583 | * v8.0.0 (2019-05-31) 584 | * `Angular v8.0.0+` 585 | * Migrate server to `Hapiness` v2 based on [Fastify](https://www.fastify.io/) 586 | * Code refactoring 587 | * Adapt tests 588 | * Documentation 589 | * v7.0.0 (2018-10-31) 590 | * `Angular v7.0.1+` 591 | * Migrate tests to [jest](https://jestjs.io/en/) and [ts-jest](https://kulshekhar.github.io/ts-jest/) 592 | * Code refactoring 593 | * Documentation 594 | * v6.2.0 (2018-09-24) 595 | * `Angular v6.1.8+` 596 | * Latest packages' versions 597 | * Install automatically `rxj-compat@6.2.2` to be compatible with all `Hapiness` extensions 598 | * Update doc of [webpack.server.config.ts]((#webpackserverconfigjs-root-project-level)) to match with latest version of `Angular Universal` [story](https://github.com/angular/angular-cli/wiki/stories-universal-rendering#webpackserverconfigjs-root-project-level) 599 | * Documentation 600 | * v6.1.0 (2018-07-26) 601 | * `Angular v6.1.0+` 602 | * Documentation 603 | * v6.0.1 (2018-05-25) 604 | * `Angular v6.0.3+` 605 | * `RxJS v6.2.0+` 606 | * Documentation 607 | * v6.0.0 (2018-05-11) 608 | * `Angular v6.0.1+` 609 | * `RxJS v6.1.0+` 610 | * Documentation 611 | 612 | [Back to top](#table-of-contents) 613 | 614 | ## Maintainers 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 |
tadaweb
Julien FauvilleSébastien RitzNicolas JesselMathieu Jeanmougin
633 | 634 | [Back to top](#table-of-contents) 635 | 636 | ## License 637 | 638 | Copyright (c) 2018 **Hapiness** Licensed under the [MIT license](https://github.com/hapinessjs/hapiness/blob/master/LICENSE.md). 639 | 640 | [Back to top](#table-of-contents) 641 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | collectCoverage: true, 5 | coverageDirectory: './coverage', 6 | testMatch: ['**/test/**/*.(test|spec).ts?(x)'], 7 | clearMocks: true, 8 | verbose: true 9 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hapiness/ng-universal", 3 | "version": "8.1.0", 4 | "description": "This is a Hapiness Engine for running Angular Apps on the server for server side rendering.", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "private": false, 8 | "scripts": { 9 | "test": "make test", 10 | "pretest": "make clean && make pretest", 11 | "coveralls": "make coveralls", 12 | "packaging": "make packaging", 13 | "prebuild": "make clean && make pretest && make test", 14 | "build": "make tsc && make ngc-tokens", 15 | "postbuild": "make packaging" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+ssh://git@github.com/hapinessjs/ng-universal-module.git" 20 | }, 21 | "keywords": [ 22 | "Angular", 23 | "Universal", 24 | "Server", 25 | "Rendering", 26 | "Hapiness", 27 | "Framework", 28 | "NodeJS", 29 | "Node", 30 | "HTTP", 31 | "API", 32 | "REST", 33 | "Streams", 34 | "Async", 35 | "Decorator", 36 | "RxJS", 37 | "Rx", 38 | "ReactiveX", 39 | "Observable", 40 | "Observer", 41 | "Module", 42 | "ES2015", 43 | "ES2016", 44 | "ES2017", 45 | "ES6", 46 | "ES7", 47 | "ES8", 48 | "Typescript" 49 | ], 50 | "contributors": [ 51 | { 52 | "name": "Julien Fauville", 53 | "url": "https://github.com/Juneil" 54 | }, 55 | { 56 | "name": "Sébastien Ritz", 57 | "url": "https://github.com/reptilbud" 58 | }, 59 | { 60 | "name": "Nicolas Jessel", 61 | "url": "https://github.com/njl07" 62 | }, 63 | { 64 | "name": "Mathieu Jeanmougin", 65 | "url": "https://github.com/sopretty" 66 | } 67 | ], 68 | "license": "SEE LICENSE IN https://github.com/hapinessjs/ng-universal-module/blob/master/LICENSE.md", 69 | "bugs": { 70 | "url": "https://github.com/hapinessjs/ng-universal-module/issues" 71 | }, 72 | "homepage": "https://github.com/hapinessjs/ng-universal-module#readme", 73 | "dependencies": { 74 | "@hapi/mimos": "^4.1.0", 75 | "cookie": "^0.4.0" 76 | }, 77 | "devDependencies": { 78 | "@angular/animations": "^8.1.0", 79 | "@angular/common": "^8.1.0", 80 | "@angular/compiler": "^8.1.0", 81 | "@angular/compiler-cli": "^8.1.0", 82 | "@angular/core": "^8.1.0", 83 | "@angular/http": "^8.0.0-beta.10", 84 | "@angular/platform-browser": "^8.1.0", 85 | "@angular/platform-browser-dynamic": "^8.1.0", 86 | "@angular/platform-server": "^8.1.0", 87 | "@hapiness/core": "^2.0.0-alpha", 88 | "@nguniversal/module-map-ngfactory-loader": "^8.1.0", 89 | "@types/fs-extra": "^8.0.0", 90 | "@types/jest": "^24.0.15", 91 | "@types/node": "^12.0.12", 92 | "coveralls": "^3.0.4", 93 | "fs-extra": "^8.1.0", 94 | "jest": "^24.8.0", 95 | "rimraf": "^2.6.3", 96 | "rxjs": "^6.5.2", 97 | "ts-jest": "^24.0.2", 98 | "ts-node": "^8.3.0", 99 | "tslint": "^5.18.0", 100 | "typescript": "~3.4.5", 101 | "zone.js": "^0.9.1" 102 | }, 103 | "peerDependencies": { 104 | "@angular/compiler": "^8.1.0", 105 | "@angular/core": "^8.1.0", 106 | "@angular/platform-server": "^8.1.0", 107 | "@hapiness/core": "^2.0.0-alpha", 108 | "@hapiness/ng-universal-transfer-http": "^10.1.0", 109 | "@nguniversal/module-map-ngfactory-loader": "^8.1.0", 110 | "rxjs": "^6.5.2", 111 | "ts-loader": "^6.0.4", 112 | "webpack": "^4.35.2", 113 | "webpack-cli": "^3.3.5" 114 | }, 115 | "engines": { 116 | "node": ">=7.0.0" 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './module'; 2 | export * from './injection'; 3 | 4 | -------------------------------------------------------------------------------- /src/injection/index.ts: -------------------------------------------------------------------------------- 1 | export * from './tokens'; 2 | 3 | -------------------------------------------------------------------------------- /src/injection/tokens.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | 3 | export const REQUEST = new InjectionToken('http_server_request'); 4 | export const REPLY = new InjectionToken('http_server_reply'); 5 | export const UTILS = new InjectionToken('http_utils'); 6 | -------------------------------------------------------------------------------- /src/module/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ng-universal.module'; 2 | export { NgSetupOptions, StaticContent } from './interfaces'; 3 | export { HttpServerReply, HttpUtils } from './services'; 4 | export { HttpServerRequest } from '@hapiness/core/httpserver'; 5 | -------------------------------------------------------------------------------- /src/module/interfaces/config.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@hapiness/core'; 2 | import { NgSetupOptions } from './options'; 3 | 4 | export const NG_UNIVERSAL_MODULE_CONFIG = new InjectionToken('ng_universal_module_config'); 5 | -------------------------------------------------------------------------------- /src/module/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './options'; 2 | export * from './config'; 3 | -------------------------------------------------------------------------------- /src/module/interfaces/options.ts: -------------------------------------------------------------------------------- 1 | import { NgModuleFactory, StaticProvider, Type } from '@angular/core'; 2 | import { 3 | ɵnguniversal_modules_module_map_ngfactory_loader_module_map_ngfactory_loader_a as ModuleMap 4 | } from '@nguniversal/module-map-ngfactory-loader'; 5 | 6 | /** 7 | * These are the allowed options for the module 8 | */ 9 | export interface NgSetupOptions { 10 | bootstrap: Type<{}> | NgModuleFactory<{}>; 11 | lazyModuleMap: ModuleMap; 12 | staticContent: StaticContent; 13 | providers?: StaticProvider[]; 14 | } 15 | 16 | export interface StaticContent { 17 | indexFile: string; 18 | rootPath: string 19 | } 20 | -------------------------------------------------------------------------------- /src/module/ng-universal.module.ts: -------------------------------------------------------------------------------- 1 | import { CoreModuleWithProviders, Module } from '@hapiness/core'; 2 | import { HttpServerReply, HttpUtils, NgEngineService } from './services'; 3 | import { HtmlUniversalRoute } from './routes'; 4 | import { NG_UNIVERSAL_MODULE_CONFIG, NgSetupOptions } from './interfaces'; 5 | 6 | @Module({ 7 | version: '8.0.0-alpha.1', 8 | components: [ 9 | HtmlUniversalRoute 10 | ], 11 | providers: [ 12 | NgEngineService, 13 | HttpUtils, 14 | HttpServerReply 15 | ], 16 | prefix: false 17 | }) 18 | export class NgUniversalModule { 19 | static setConfig(config: NgSetupOptions): CoreModuleWithProviders { 20 | return { 21 | module: NgUniversalModule, 22 | providers: [{ provide: NG_UNIVERSAL_MODULE_CONFIG, useValue: config }] 23 | }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/module/routes/index.ts: -------------------------------------------------------------------------------- 1 | export * from './universal'; 2 | -------------------------------------------------------------------------------- /src/module/routes/universal/html.route.ts: -------------------------------------------------------------------------------- 1 | import { HttpServerReply, NgEngineService } from '../../services'; 2 | import { merge, Observable, of } from 'rxjs'; 3 | import { filter, flatMap, map } from 'rxjs/operators'; 4 | import { Get, HttpResponse, Route } from '@hapiness/core/httpserver'; 5 | 6 | 7 | @Route({ 8 | path: '*' 9 | }) 10 | export class HtmlUniversalRoute { 11 | /** 12 | * Class constructor 13 | * 14 | * @param {NgEngineService} _ngEngineService 15 | * @param {HttpServerReply} _reply 16 | */ 17 | constructor(private _ngEngineService: NgEngineService, 18 | private _reply: HttpServerReply) { 19 | } 20 | 21 | /** 22 | * Get implementation 23 | * 24 | * @returns {Observable>} 25 | */ 26 | @Get() 27 | onGet(): Observable> { 28 | return this._ngEngineService.universal() 29 | .pipe( 30 | flatMap((resp: HttpResponse) => 31 | of( 32 | of( 33 | this._reply.willRedirect 34 | ) 35 | ) 36 | .pipe( 37 | flatMap((obsWillRedirect: Observable) => 38 | merge( 39 | obsWillRedirect 40 | .pipe( 41 | filter((redirect: boolean) => !!redirect), 42 | map(() => this._createResponse()) 43 | ), 44 | obsWillRedirect 45 | .pipe( 46 | filter((redirect: boolean) => !redirect), 47 | map(() => this._formatResponse(resp)), 48 | map((_: HttpResponse) => ({ 49 | ..._, 50 | statusCode: this._isValid(_.value) ? _.statusCode : 204 51 | })), 52 | map((_: HttpResponse) => this._createResponse(_)) 53 | ) 54 | ) 55 | ) 56 | ) 57 | ) 58 | ); 59 | } 60 | 61 | /** 62 | * Format response to HttpResponse object 63 | * 64 | * @param {any} data 65 | * 66 | * @returns HttpResponse 67 | */ 68 | private _formatResponse(data: HttpResponse): HttpResponse { 69 | return { 70 | statusCode: !!data ? data.statusCode || 200 : 204, 71 | headers: !!data ? data.headers || {} : {}, 72 | value: !!data ? data.value : null 73 | }; 74 | } 75 | 76 | /** 77 | * Check if response is not empty 78 | * 79 | * @param {any} response 80 | * 81 | * @returns boolean 82 | */ 83 | private _isValid(response: any): boolean { 84 | return typeof response !== 'undefined' && response !== null; 85 | } 86 | 87 | /** 88 | * Apply new headers or create redirection 89 | * 90 | * @param {HttpResponse} response initial response 91 | * 92 | * @returns {HttpResponse} new response 93 | * 94 | * @private 95 | */ 96 | private _createResponse(response: HttpResponse = { value: null, headers: {} }): HttpResponse { 97 | if (this._reply.willRedirect) { 98 | return { 99 | redirect: this._reply.willRedirect, 100 | value: this._reply.redirectUrl, 101 | headers: this._reply.headers 102 | }; 103 | } 104 | return { 105 | ...response, 106 | headers: { 107 | ...response.headers, ...this._reply.headers 108 | } 109 | }; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/module/routes/universal/index.ts: -------------------------------------------------------------------------------- 1 | export * from './html.route'; 2 | -------------------------------------------------------------------------------- /src/module/services/engine/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ng.service'; 2 | -------------------------------------------------------------------------------- /src/module/services/engine/ng.service.ts: -------------------------------------------------------------------------------- 1 | import { Compiler, CompilerFactory, NgModuleFactory, StaticProvider, Type } from '@angular/core'; 2 | import { INITIAL_CONFIG, platformDynamicServer, renderModuleFactory } from '@angular/platform-server'; 3 | import { ResourceLoader } from '@angular/compiler'; 4 | import { 5 | provideModuleMap, 6 | ɵnguniversal_modules_module_map_ngfactory_loader_module_map_ngfactory_loader_a as ModuleMap 7 | } from '@nguniversal/module-map-ngfactory-loader'; 8 | 9 | import { from, merge, Observable, of, throwError } from 'rxjs'; 10 | import { filter, flatMap, map, tap, toArray } from 'rxjs/operators'; 11 | 12 | import * as fs from 'fs'; 13 | import { join } from 'path'; 14 | import * as Mimos from '@hapi/mimos'; 15 | 16 | import { NG_UNIVERSAL_MODULE_CONFIG, NgSetupOptions, StaticContent } from '../../interfaces'; 17 | import { REPLY, REQUEST, UTILS } from '../../../injection'; 18 | import { Inject, Service } from '@hapiness/core'; 19 | 20 | import { HttpResponse, HttpServerRequest } from '@hapiness/core/httpserver'; 21 | import { HttpServerReply } from '../reply'; 22 | import { HttpUtils } from '../utils'; 23 | 24 | @Service() 25 | export class NgEngineService { 26 | /** 27 | * This holds a cached version of each data used. 28 | */ 29 | private _dataCache: { [key: string]: Buffer }; 30 | /** 31 | * Map of Module Factories 32 | */ 33 | private _factoryCacheMap: Map, NgModuleFactory<{}>>; 34 | /** 35 | * Angular compiler factory 36 | */ 37 | private _compilerFactory: CompilerFactory; 38 | /** 39 | * Angular compiler instance 40 | */ 41 | private _compiler: Compiler; 42 | /** 43 | * Renders a {@link NgModuleFactory} to string. 44 | * 45 | * `document` is the full document HTML of the page to render, as a string. 46 | * `url` is the URL for the current render request. 47 | * `extraProviders` are the platform level providers for the current render request. 48 | * 49 | * store original function to stub it in tests 50 | */ 51 | private readonly _renderModuleFactory: (moduleFactory: NgModuleFactory, options: { 52 | document?: string; 53 | url?: string; 54 | extraProviders?: StaticProvider[]; 55 | }) => Promise; 56 | /** 57 | * Helper function for getting the providers object for the MODULE_MAP 58 | * 59 | * @param {ModuleMap} moduleMap Map to use as a value for MODULE_MAP 60 | * 61 | * store original function to stub it in tests 62 | */ 63 | private readonly _provideModuleMap: (moduleMap: ModuleMap) => StaticProvider; 64 | /** 65 | * Store mimos instance to stub it in tests 66 | */ 67 | private _mimos: Mimos; 68 | 69 | /** 70 | * Service constructor 71 | * 72 | * @param {NgSetupOptions} _config 73 | * @param {HttpServerRequest} _request 74 | * @param {HttpServerReply} _reply helper to modify the response 75 | * @param {HttpUtils} _utils helper to manage data in request/response like cookies 76 | */ 77 | constructor(@Inject(NG_UNIVERSAL_MODULE_CONFIG) private _config: NgSetupOptions, 78 | private _request: HttpServerRequest, 79 | private _reply: HttpServerReply, 80 | private _utils: HttpUtils) { 81 | this._dataCache = {}; 82 | this._factoryCacheMap = new Map, NgModuleFactory<{}>>(); 83 | 84 | this._compilerFactory = platformDynamicServer().injector.get(CompilerFactory); 85 | 86 | this._compiler = this._compilerFactory.createCompiler([ 87 | { 88 | providers: [ 89 | { provide: ResourceLoader, useClass: FileLoader, deps: [] } 90 | ] 91 | } 92 | ]); 93 | 94 | this._renderModuleFactory = renderModuleFactory; 95 | this._provideModuleMap = provideModuleMap; 96 | this._mimos = new Mimos(); 97 | } 98 | 99 | /** 100 | * Returns universal rendering of HTML 101 | * 102 | * @return {Observable>} 103 | */ 104 | universal(): Observable> { 105 | return merge( 106 | this._checkRequest(), 107 | this._checkConfig() 108 | ) 109 | .pipe( 110 | toArray(), 111 | map(_ => 112 | ({ 113 | config: _.pop() 114 | }) 115 | ), 116 | map(_ => Object.assign(_, { mime: this._mimos.path(this._request.raw.url).type })), 117 | flatMap(_ => 118 | merge( 119 | this._getStaticContent(_), 120 | this._getFactoryContent(_) 121 | ) 122 | ) 123 | ); 124 | } 125 | 126 | /** 127 | * Returns HttpResponse from static content 128 | * 129 | * @param _ 130 | * 131 | * @returns {Observable>} 132 | * 133 | * @private 134 | */ 135 | private _getStaticContent(_: any): Observable> { 136 | return of(_) 137 | .pipe( 138 | filter(__ => !!__.mime), 139 | map(__ => 140 | ({ 141 | value: this._getDocument(this._buildFilePath(__.config.staticContent, __.mime, this._request.raw.url)), 142 | headers: { 143 | 'content-type': __.mime 144 | } 145 | }) 146 | ) 147 | ); 148 | } 149 | 150 | /** 151 | * Returns content from NgFactoryModule 152 | * 153 | * @param _ 154 | * 155 | * @returns {Observable>} 156 | * 157 | * @private 158 | */ 159 | private _getFactoryContent(_: any): Observable> { 160 | return of(_) 161 | .pipe( 162 | filter(__ => !__.mime), 163 | map(__ => 164 | ({ 165 | moduleOrFactory: __.config.bootstrap, 166 | extraProviders: this._extraProviders( 167 | __.config.providers, 168 | __.config.lazyModuleMap, 169 | this._buildFilePath(__.config.staticContent) 170 | ) 171 | }) 172 | ), 173 | flatMap(__ => 174 | this._getFactory(__.moduleOrFactory) 175 | .pipe( 176 | flatMap(factory => 177 | from(this._renderModuleFactory(factory, { extraProviders: __.extraProviders })) 178 | ), 179 | map(html => 180 | ({ 181 | value: html, 182 | headers: { 183 | 'content-type': 'text/html' 184 | } 185 | }) 186 | ) 187 | ) 188 | ) 189 | ); 190 | } 191 | 192 | /** 193 | * Function to check request parameter 194 | * 195 | * @returns {Observable} 196 | * 197 | * @private 198 | */ 199 | private _checkRequest(): Observable { 200 | return of(this._request) 201 | .pipe( 202 | flatMap(_ => (!!_ && !!_.raw && _.raw.url !== undefined) ? 203 | of(true) : 204 | throwError(new Error('url is undefined')) 205 | ) 206 | ); 207 | } 208 | 209 | /** 210 | * Function to check module config 211 | * 212 | * @returns {Observable} 213 | * 214 | * @private 215 | */ 216 | private _checkConfig(): Observable { 217 | return of(this._config) 218 | .pipe( 219 | flatMap(_ => (!!_ && !!_.bootstrap) ? 220 | of(_) : 221 | throwError(new Error('You must pass in config a NgModule or NgModuleFactory to be bootstrapped')) 222 | ), 223 | flatMap(_ => (!!_ && !!_.lazyModuleMap) ? 224 | of(_) : 225 | throwError(new Error('You must pass in config lazy module map')) 226 | ), 227 | flatMap(_ => (!!_ && !!_.staticContent) ? 228 | of(_) : 229 | throwError(new Error('You must pass in config the static content object')) 230 | ), 231 | flatMap(_ => (!!_ && !!_.staticContent.indexFile) ? 232 | of(_) : 233 | throwError(new Error('You must pass in config the static content object with index file')) 234 | ), 235 | flatMap(_ => (!!_ && !!_.staticContent.rootPath) ? 236 | of(_) : 237 | throwError(new Error('You must pass in config the static content object with root path')) 238 | ), 239 | flatMap(_ => of({ 240 | bootstrap: _.bootstrap, 241 | lazyModuleMap: _.lazyModuleMap, 242 | staticContent: _.staticContent, 243 | providers: _.providers || [] 244 | }) 245 | ) 246 | ); 247 | } 248 | 249 | /** 250 | * Builds extra providers 251 | * 252 | * @param {StaticProvider[]} providers 253 | * @param {ModuleMap} lazyModuleMap 254 | * @param {string} filePath 255 | * 256 | * @return {Provider[]} 257 | * 258 | * @private 259 | */ 260 | private _extraProviders(providers: StaticProvider[], 261 | lazyModuleMap: ModuleMap, filePath: string): StaticProvider[] { 262 | return providers!.concat( 263 | providers!, 264 | this._provideModuleMap(lazyModuleMap), 265 | this._getAdditionalProviders(), 266 | [ 267 | { 268 | provide: INITIAL_CONFIG, 269 | useValue: { 270 | document: this._getDocument(filePath).toString(), 271 | url: this._request.raw.url 272 | } 273 | } 274 | ] 275 | ); 276 | } 277 | 278 | /** 279 | * Get a factory from a bootstrapped module / module factory 280 | * 281 | * @param {Type<{}> | NgModuleFactory<{}>} moduleOrFactory 282 | * 283 | * @return {Observable>} 284 | * 285 | * @private 286 | */ 287 | private _getFactory(moduleOrFactory: Type<{}> | NgModuleFactory<{}>): Observable> { 288 | return >>of( 289 | of(moduleOrFactory) 290 | ) 291 | .pipe( 292 | flatMap(obs => 293 | merge( 294 | obs 295 | .pipe( 296 | filter(_ => _ instanceof NgModuleFactory) 297 | ), 298 | obs 299 | .pipe( 300 | filter(_ => !(_ instanceof NgModuleFactory)), 301 | map((_: Type<{}>) => this._factoryCacheMap.get(_)), 302 | flatMap(_ => !!_ ? of(_) : this._compile(>moduleOrFactory)) 303 | ) 304 | ) 305 | ) 306 | ); 307 | } 308 | 309 | /** 310 | * Compile the module and cache it 311 | * 312 | * @param {Type<{}>} module to compile and cache 313 | * 314 | * @return {Observable>} 315 | * 316 | * @private 317 | */ 318 | private _compile(module: Type<{}>): Observable> { 319 | return >>from(this._compiler.compileModuleAsync(module)) 320 | .pipe( 321 | tap(_ => this._factoryCacheMap.set(module, _)) 322 | ); 323 | } 324 | 325 | /** 326 | * Get providers of the request and response 327 | * 328 | * @return {StaticProvider[]} 329 | * 330 | * @private 331 | */ 332 | private _getAdditionalProviders(): StaticProvider[] { 333 | return [ 334 | { 335 | provide: REQUEST, 336 | useValue: this._request 337 | }, 338 | { 339 | provide: REPLY, 340 | useValue: this._reply 341 | }, 342 | { 343 | provide: UTILS, 344 | useValue: this._utils 345 | } 346 | ]; 347 | } 348 | 349 | /** 350 | * Returns document path 351 | * 352 | * @param {StaticContent} staticContent 353 | * @param {string} mime 354 | * @param {string} staticFileUrl 355 | * 356 | * @returns {string} 357 | * 358 | * @private 359 | */ 360 | private _buildFilePath(staticContent: StaticContent, mime?: string, staticFileUrl?: string): string { 361 | return (!!mime && !!staticFileUrl) ? 362 | join(staticContent.rootPath, staticFileUrl) : 363 | join(staticContent.rootPath, staticContent.indexFile); 364 | } 365 | 366 | /** 367 | * Returns document from cache or file system 368 | * 369 | * @param {string} filePath path to the file 370 | * 371 | * @return {Buffer} 372 | * 373 | * @private 374 | */ 375 | private _getDocument(filePath: string): Buffer { 376 | return this._dataCache[filePath] = this._dataCache[filePath] || fs.readFileSync(filePath); 377 | } 378 | } 379 | 380 | /** 381 | * FileLoader implementation 382 | */ 383 | class FileLoader implements ResourceLoader { 384 | /* istanbul ignore next */ 385 | get(url: string): Promise { 386 | return new Promise((resolve, reject) => { 387 | fs.readFile(url, (err: NodeJS.ErrnoException, buffer: Buffer) => { 388 | if (err) { 389 | return reject(err); 390 | } 391 | 392 | resolve(buffer.toString()); 393 | }); 394 | }); 395 | } 396 | } 397 | -------------------------------------------------------------------------------- /src/module/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './engine'; 2 | export * from './reply'; 3 | export * from './utils'; 4 | -------------------------------------------------------------------------------- /src/module/services/reply/http-server.reply.ts: -------------------------------------------------------------------------------- 1 | import { Service } from '@hapiness/core'; 2 | 3 | @Service() 4 | export class HttpServerReply { 5 | // private property to store all additional headers 6 | private _headers: { [key: string]: string; }; 7 | // private property to store redirect url 8 | private _redirectUrl: string; 9 | // private property to store redirection flag 10 | private _willRedirect: boolean; 11 | 12 | /** 13 | * Constructor 14 | */ 15 | constructor() { 16 | this._headers = {}; 17 | this._redirectUrl = ''; 18 | this._willRedirect = false; 19 | } 20 | 21 | /** 22 | * Add new header to the original response 23 | * 24 | * @param {string} key the header's key 25 | * @param {string} value the header's value 26 | * 27 | * @returns {HttpServerReply} current instance 28 | */ 29 | header(key: string, value: string): HttpServerReply { 30 | if (!!key && !!value) { 31 | this._headers = { ...this._headers, [key]: value }; 32 | } 33 | return this; 34 | } 35 | 36 | /** 37 | * Returns all additional headers for current response 38 | */ 39 | get headers(): { [key: string]: string; } { 40 | return this._headers; 41 | } 42 | 43 | /** 44 | * Set redirect url 45 | * 46 | * @param {string} url redirection 47 | * 48 | * @returns {HttpServerReply} current instance 49 | */ 50 | redirect(url: string): HttpServerReply { 51 | if (!url || typeof url !== 'string') { 52 | throw new TypeError('argument url must be a string'); 53 | } 54 | this._redirectUrl = url; 55 | this._willRedirect = true; 56 | return this; 57 | } 58 | 59 | /** 60 | * Returns redirect url value 61 | */ 62 | get redirectUrl(): string { 63 | return this._redirectUrl; 64 | } 65 | 66 | /** 67 | * Returns flag to know if response will be a redirection 68 | */ 69 | get willRedirect(): boolean { 70 | return this._willRedirect; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/module/services/reply/index.ts: -------------------------------------------------------------------------------- 1 | export * from './http-server.reply'; 2 | -------------------------------------------------------------------------------- /src/module/services/utils/http.utils.ts: -------------------------------------------------------------------------------- 1 | import { Service } from '@hapiness/core'; 2 | import { parse, serialize } from 'cookie'; 3 | 4 | @Service() 5 | export class HttpUtils { 6 | /** 7 | * Parse a cookie header. 8 | * 9 | * Parse the given cookie header string into an object 10 | * The object has the various cookies as keys(names) => values 11 | * 12 | * @param {string} str 13 | * @param {object} [options] 14 | * 15 | * @return {object} 16 | */ 17 | parseCookie(str: string, options?: any) { 18 | return parse(str, options); 19 | } 20 | 21 | /** 22 | * Serialize data into a cookie header. 23 | * 24 | * Serialize the a name value pair into a cookie string suitable for 25 | * http headers. An optional options object specified cookie parameters. 26 | * 27 | * serialize('foo', 'bar', { httpOnly: true }) 28 | * => "foo=bar; httpOnly" 29 | * 30 | * @param {string} name 31 | * @param {string} value 32 | * @param {object} [options] 33 | * 34 | * @returns {string} 35 | */ 36 | serializeCookie(name: string, value: string, options?: any) { 37 | return serialize(name, value, options); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/module/services/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './http.utils'; 2 | -------------------------------------------------------------------------------- /test/unit/html-universal.route.test.ts: -------------------------------------------------------------------------------- 1 | import { HtmlUniversalRoute } from '../../src/module/routes/universal'; 2 | import { HttpServerReply, NgEngineService } from '../../src/module/services'; 3 | import { of } from 'rxjs'; 4 | import { Buffer } from 'buffer'; 5 | 6 | // mock NgEngineService constructor and all its methods 7 | jest.mock('../../src/module/services/engine/ng.service'); 8 | 9 | describe('- Unit get-html-universal.route.test.ts file', () => { 10 | afterAll(() => { 11 | // restores the original (non-mocked) implementation. 12 | (NgEngineService).mockRestore(); 13 | }); 14 | 15 | /** 16 | * Test if `HtmlUniversalRoute` has `onGet`, `_replyResponse` and `_isValid` functions 17 | */ 18 | test('- `HtmlUniversalRoute` must have `onGet`, `_replyResponse` and `_isValid` functions', (done) => { 19 | // show that mockClear() is working 20 | expect(NgEngineService).not.toHaveBeenCalled(); 21 | 22 | const htmlUniversalRoute = new HtmlUniversalRoute(new NgEngineService(null, null, null, null), new HttpServerReply()); 23 | 24 | expect(typeof htmlUniversalRoute.onGet).toBe('function'); 25 | expect(typeof htmlUniversalRoute['_createResponse']).toBe('function'); 26 | expect(typeof htmlUniversalRoute['_formatResponse']).toBe('function'); 27 | expect(typeof htmlUniversalRoute['_isValid']).toBe('function'); 28 | 29 | // NgEngineService constructor should have been called only 1 time 30 | expect(NgEngineService).toHaveBeenCalledTimes(1); 31 | 32 | done(); 33 | }); 34 | 35 | /** 36 | * Test if `HtmlUniversalRoute.onGet()` function returns an Observable with html data and no header 37 | */ 38 | test( 39 | '- `HtmlUniversalRoute.onGet()` function must return an Observable with html data and no header', 40 | (done) => { 41 | // show that mockClear() is working 42 | expect(NgEngineService).not.toHaveBeenCalled(); 43 | 44 | const htmlUniversalRoute = new HtmlUniversalRoute(new NgEngineService(null, null, null, null), new HttpServerReply()); 45 | 46 | (NgEngineService).mock.instances[0] 47 | .universal.mockReturnValueOnce(of({ value: '

Hello Angular

' })); 48 | 49 | htmlUniversalRoute.onGet().subscribe(res => { 50 | expect(res.value).toBe('

Hello Angular

'); 51 | expect(res.headers).toStrictEqual({}); 52 | 53 | // NgEngineService constructor should have been called only 1 time 54 | expect(NgEngineService).toHaveBeenCalledTimes(1); 55 | 56 | done(); 57 | }); 58 | } 59 | ); 60 | 61 | /** 62 | * Test if `HtmlUniversalRoute.onGet()` function returns an Observable with html buffer and with header 63 | */ 64 | test( 65 | '- `HtmlUniversalRoute.onGet()` function must return an Observable with html buffer and header', 66 | (done) => { 67 | // show that mockClear() is working 68 | expect(NgEngineService).not.toHaveBeenCalled(); 69 | 70 | const htmlUniversalRoute = new HtmlUniversalRoute(new NgEngineService(null, null, null, null), new HttpServerReply()); 71 | 72 | (NgEngineService).mock.instances[0].universal.mockReturnValueOnce( 73 | of({ 74 | value: Buffer.from('

Hello Angular

'), 75 | headers: { 76 | 'content-type': 'text/html' 77 | } 78 | }) 79 | ); 80 | 81 | htmlUniversalRoute.onGet().subscribe(res => { 82 | expect(res.value.toString()).toBe('

Hello Angular

'); 83 | expect(res.headers).toStrictEqual({ 'content-type': 'text/html' }); 84 | 85 | // NgEngineService constructor should have been called only 1 time 86 | expect(NgEngineService).toHaveBeenCalledTimes(1); 87 | 88 | done(); 89 | }); 90 | } 91 | ); 92 | 93 | /** 94 | * Test if `HtmlUniversalRoute.onGet()` function returns an Observable with redirect data and no header 95 | */ 96 | test( 97 | '- `HtmlUniversalRoute.onGet()` function must return an Observable with redirect data and no header', 98 | (done) => { 99 | // show that mockClear() is working 100 | expect(NgEngineService).not.toHaveBeenCalled(); 101 | 102 | const htmlUniversalRoute = new HtmlUniversalRoute(new NgEngineService(null, null, null, null), new HttpServerReply()); 103 | 104 | (NgEngineService).mock.instances[0] 105 | .universal.mockReturnValueOnce(of({ value: '

Hello Angular

' })); 106 | 107 | htmlUniversalRoute['_reply'].redirect('http://universal_redirect'); 108 | 109 | htmlUniversalRoute.onGet().subscribe(res => { 110 | expect(res.value).toBe('http://universal_redirect'); 111 | expect(res.redirect).toBeTruthy(); 112 | 113 | // NgEngineService constructor should have been called only 1 time 114 | expect(NgEngineService).toHaveBeenCalledTimes(1); 115 | 116 | done(); 117 | }); 118 | } 119 | ); 120 | 121 | /** 122 | * Test if `HtmlUniversalRoute.onGet()` function returns an Observable with redirect data and additional header 123 | */ 124 | test( 125 | '- `HtmlUniversalRoute.onGet()` function must return an Observable with redirect data and additional header', 126 | (done) => { 127 | // show that mockClear() is working 128 | expect(NgEngineService).not.toHaveBeenCalled(); 129 | 130 | const htmlUniversalRoute = new HtmlUniversalRoute(new NgEngineService(null, null, null, null), new HttpServerReply()); 131 | 132 | (NgEngineService).mock.instances[0] 133 | .universal.mockReturnValueOnce(of({ value: '

Hello Angular

' })); 134 | 135 | htmlUniversalRoute['_reply'].header('x-redirect', 'universal_redirect').redirect('http://universal_redirect'); 136 | 137 | htmlUniversalRoute.onGet().subscribe(res => { 138 | expect(res.value).toBe('http://universal_redirect'); 139 | expect(res.redirect).toBeTruthy(); 140 | expect(res.headers).toStrictEqual({ 'x-redirect': 'universal_redirect' }); 141 | 142 | // NgEngineService constructor should have been called only 1 time 143 | expect(NgEngineService).toHaveBeenCalledTimes(1); 144 | 145 | done(); 146 | }); 147 | } 148 | ); 149 | 150 | 151 | /** 152 | * Test if `HtmlUniversalRoute.onGet()` function returns an Observable with empty data 153 | */ 154 | test( 155 | '- `HtmlUniversalRoute.onGet()` function must return an Observable with empty data', 156 | (done) => { 157 | // show that mockClear() is working 158 | expect(NgEngineService).not.toHaveBeenCalled(); 159 | 160 | const htmlUniversalRoute = new HtmlUniversalRoute(new NgEngineService(null, null, null, null), new HttpServerReply()); 161 | 162 | (NgEngineService).mock.instances[0] 163 | .universal.mockReturnValueOnce(of(null)); 164 | 165 | htmlUniversalRoute.onGet().subscribe(res => { 166 | expect(res.value).toBeNull(); 167 | expect(res.headers).toStrictEqual({}); 168 | expect(res.statusCode).toBe(204); 169 | 170 | // NgEngineService constructor should have been called only 1 time 171 | expect(NgEngineService).toHaveBeenCalledTimes(1); 172 | 173 | done(); 174 | }); 175 | } 176 | ); 177 | }); 178 | -------------------------------------------------------------------------------- /test/unit/http-server-reply.service.test.ts: -------------------------------------------------------------------------------- 1 | import { HttpServerReply } from '../../src'; 2 | 3 | let httpServerReplyService: HttpServerReply; 4 | 5 | describe('- Unit http-server-reply.service.test.ts file', () => { 6 | /** 7 | * Executed before each tests 8 | */ 9 | beforeEach(() => { 10 | httpServerReplyService = new HttpServerReply(); 11 | }); 12 | 13 | /** 14 | * Executed after each tests 15 | */ 16 | afterEach(() => { 17 | httpServerReplyService = undefined; 18 | }); 19 | 20 | /** 21 | * Test if `HttpServerReply.header` is a function 22 | */ 23 | test('- `HttpServerReply.header` must be a function', (done) => { 24 | expect(typeof httpServerReplyService.header).toBe('function'); 25 | done(); 26 | }); 27 | 28 | /** 29 | * Test if `HttpServerReply.redirect` is a function 30 | */ 31 | test('- `HttpServerReply.redirect` must be a function', (done) => { 32 | expect(typeof httpServerReplyService.redirect).toBe('function'); 33 | done(); 34 | }); 35 | 36 | /** 37 | * Test if `HttpServerReply.headers` returns empty headers object 38 | */ 39 | test('- `HttpServerReply.headers` must return empty headers object', (done) => { 40 | expect(httpServerReplyService.headers).toStrictEqual({}); 41 | done(); 42 | }); 43 | 44 | /** 45 | * Test if `HttpServerReply.redirectUrl` returns empty string 46 | */ 47 | test('- `HttpServerReply.redirectUrl` must return empty string', (done) => { 48 | expect(httpServerReplyService.redirectUrl).toStrictEqual(''); 49 | done(); 50 | }); 51 | 52 | /** 53 | * Test if `HttpServerReply.willRedirect` returns false 54 | */ 55 | test('- `HttpServerReply.willRedirect` must return false', (done) => { 56 | expect(httpServerReplyService.willRedirect).toStrictEqual(false); 57 | done(); 58 | }); 59 | 60 | /** 61 | * Test if `HttpServerReply.headers` returns empty headers object after calling header() method without key/value 62 | */ 63 | test('- `HttpServerReply.headers` must return empty headers object after calling header() method without key/value', (done) => { 64 | expect(httpServerReplyService.header(null, null).headers).toStrictEqual({}); 65 | done(); 66 | }); 67 | 68 | /** 69 | * Test if `HttpServerReply.headers` returns additional headers object after calling header() method with key/value 70 | */ 71 | test('- `HttpServerReply.headers` must return additional headers object after calling header() method with key/value', 72 | (done) => { 73 | expect(httpServerReplyService.header('x-additional-header', 'value').headers).toStrictEqual({ 'x-additional-header': 'value' }); 74 | done(); 75 | } 76 | ); 77 | 78 | /** 79 | * Test if `HttpServerReply.redirect()` returns an error if no url is provided 80 | */ 81 | test('- `HttpServerReply` must return an error if no url is provided', (done) => { 82 | try { 83 | httpServerReplyService.redirect(null); 84 | } catch (e) { 85 | expect(e.message).toBe('argument url must be a string'); 86 | } finally { 87 | done(); 88 | } 89 | }); 90 | 91 | /** 92 | * Test if `HttpServerReply.redirect()` returns new redirect url and flag 93 | */ 94 | test('- `HttpServerReply` must return new redirect url and flag', (done) => { 95 | httpServerReplyService.redirect('http://universal_redirect'); 96 | expect(httpServerReplyService.redirectUrl).toStrictEqual('http://universal_redirect'); 97 | expect(httpServerReplyService.willRedirect).toBeTruthy(); 98 | done(); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /test/unit/http-utils.service.test.ts: -------------------------------------------------------------------------------- 1 | import { HttpUtils } from '../../src'; 2 | 3 | let httpUtilsService: HttpUtils; 4 | 5 | describe('- Unit http-utils.service.test.ts file', () => { 6 | /** 7 | * Executed before each tests 8 | */ 9 | beforeEach(() => { 10 | httpUtilsService = new HttpUtils(); 11 | }); 12 | 13 | /** 14 | * Executed after each tests 15 | */ 16 | afterEach(() => { 17 | httpUtilsService = undefined; 18 | }); 19 | 20 | /** 21 | * Test if `HttpUtils.parseCookie` is a function 22 | */ 23 | test('- `HttpUtils.parseCookie` must be a function', (done) => { 24 | expect(typeof httpUtilsService.parseCookie).toBe('function'); 25 | done(); 26 | }); 27 | 28 | /** 29 | * Test if `HttpUtils.serializeCookie` is a function 30 | */ 31 | test('- `HttpUtils.serializeCookie` must be a function', (done) => { 32 | expect(typeof httpUtilsService.serializeCookie).toBe('function'); 33 | done(); 34 | }); 35 | 36 | /** 37 | * Test if `HttpUtils.parseCookie()` returns an object with each cookie inside 38 | */ 39 | test('- `HttpUtils.parseCookie` must return an object with each cookie inside', (done) => { 40 | expect(httpUtilsService.parseCookie('foo=bar; equation=E%3Dmc%5E2')).toStrictEqual({ foo: 'bar', equation: 'E=mc^2' }); 41 | done(); 42 | }); 43 | 44 | /** 45 | * Test if `HttpUtils.serializeCookie()` returns header string 46 | */ 47 | test('- `HttpUtils.serializeCookie` must return header string', (done) => { 48 | expect(httpUtilsService.serializeCookie('foo', 'bar', { httpOnly: true })).toStrictEqual('foo=bar; HttpOnly'); 49 | done(); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/unit/ng-engine.service.test.ts: -------------------------------------------------------------------------------- 1 | import { NgEngineService } from '../../src/module/services/engine'; 2 | import { Observable } from 'rxjs'; 3 | import * as fs from 'fs'; 4 | import { Buffer } from 'buffer'; 5 | 6 | const request: any = { raw: { url: '/static/content' } }; 7 | const reply: any = {}; 8 | const utils: any = {}; 9 | 10 | describe('- Unit ng-engine.service.test.ts file', () => { 11 | /** 12 | * Test if `NgEngineService` as a `universal` function 13 | */ 14 | test('- `NgEngineService` must have `universal` function', 15 | (done) => { 16 | expect(typeof new NgEngineService(null, null, null, null).universal).toBe('function'); 17 | done(); 18 | }); 19 | 20 | /** 21 | * Test if `NgEngineService.universal()` function returns an Observable 22 | */ 23 | test('- `NgEngineService.universal()` function must return an Observable', (done) => { 24 | expect(new NgEngineService(null, null, null, null).universal()).toBeInstanceOf(Observable); 25 | done(); 26 | }); 27 | 28 | /** 29 | * Test if `NgEngineService.universal()` function returns an Observable Error if parameter is wrong 30 | */ 31 | test('- `NgEngineService.universal()` function must return an Observable Error if parameter is wrong', (done) => { 32 | new NgEngineService(null, null, null, null).universal().subscribe(() => undefined, e => expect(e.message).toBe('url is undefined')); 33 | done(); 34 | }); 35 | 36 | /** 37 | * Test if `NgEngineService.universal()` function returns an Observable Error if missing bootstrap in config 38 | */ 39 | test('- `NgEngineService.universal()` function must return an Observable Error if missing bootstrap in config', (done) => { 40 | new NgEngineService(null, request, reply, utils).universal() 41 | .subscribe(() => undefined, e => expect(e.message) 42 | .toBe('You must pass in config a NgModule or NgModuleFactory to be bootstrapped')); 43 | done(); 44 | }); 45 | 46 | /** 47 | * Test if `NgEngineService.universal()` function returns an Observable Error if missing lazyModuleMap in config 48 | */ 49 | test('- `NgEngineService.universal()` function must return an Observable Error if missing lazyModuleMap in config', (done) => { 50 | const ngE = new NgEngineService({ 51 | bootstrap: {}, 52 | lazyModuleMap: null, 53 | staticContent: null 54 | }, request, reply, utils); 55 | 56 | ngE.universal().subscribe(() => undefined, e => expect(e.message) 57 | .toBe('You must pass in config lazy module map')); 58 | done(); 59 | }); 60 | 61 | /** 62 | * Test if `NgEngineService.universal()` function returns an Observable Error if missing staticContent in config 63 | */ 64 | test('- `NgEngineService.universal()` function must return an Observable Error if missing staticContent in config', (done) => { 65 | const ngE = new NgEngineService({ bootstrap: {}, lazyModuleMap: {}, staticContent: null }, request, reply, utils); 66 | ngE.universal() 67 | .subscribe(() => undefined, e => expect(e.message).toBe('You must pass in config the static content object')); 68 | done(); 69 | }); 70 | 71 | /** 72 | * Test if `NgEngineService.universal()` function returns an Observable Error if missing staticContent indexFile in config 73 | */ 74 | test('- `NgEngineService.universal()` function must return an Observable ' + 75 | 'Error if missing staticContent indexFile in config', (done) => { 76 | const ngE = new NgEngineService({ 77 | bootstrap: {}, 78 | lazyModuleMap: {}, 79 | staticContent: { indexFile: null, rootPath: '' } 80 | }, request, reply, utils); 81 | 82 | ngE.universal() 83 | .subscribe(() => undefined, e => expect(e.message).toBe('You must pass in config the static content object with index file')); 84 | done(); 85 | }); 86 | 87 | /** 88 | * Test if `NgEngineService.universal()` function returns an Observable Error if missing staticContent rootPath in config 89 | */ 90 | test('- `NgEngineService.universal()` function must return an Observable Error if missing staticContent rootPath in config', (done) => { 91 | const ngE = new NgEngineService({ 92 | bootstrap: {}, 93 | lazyModuleMap: {}, 94 | staticContent: { indexFile: '/', rootPath: '' } 95 | }, request, reply, utils); 96 | 97 | ngE.universal() 98 | .subscribe(() => undefined, e => expect(e.message).toBe('You must pass in config the static content object with root path')); 99 | done(); 100 | }); 101 | 102 | /** 103 | * Test if `NgEngineService.universal()` function returns success with compiler 104 | */ 105 | test('- `NgEngineService.universal()` success execution with compiler', (done) => { 106 | const ngE = new NgEngineService({ 107 | bootstrap: {}, lazyModuleMap: {}, staticContent: { 108 | rootPath: './root/path', 109 | indexFile: 'test.html' 110 | } 111 | }, request, reply, utils); 112 | 113 | // create all mocks 114 | const compilerStub = jest.spyOn(ngE['_compiler'], 'compileModuleAsync') 115 | .mockReturnValueOnce(new Promise((resolve) => resolve({} as any))); 116 | const renderModuleFactoryStub = jest.spyOn(ngE, '_renderModuleFactory') 117 | .mockReturnValueOnce(new Promise((resolve) => resolve('

Hello Angular

'))); 118 | const fsStub = jest.spyOn(fs, 'readFileSync').mockReturnValueOnce(Buffer.from('')); 119 | 120 | ngE.universal().subscribe(_ => { 121 | expect(_.value).toBe('

Hello Angular

'); 122 | 123 | // compilerStub should have been called only 1 time 124 | expect(compilerStub).toHaveBeenCalledTimes(1); 125 | 126 | // renderModuleFactoryStub should have been called only 1 time 127 | expect(renderModuleFactoryStub).toHaveBeenCalledTimes(1); 128 | 129 | // fsStub should have been called only 1 time 130 | expect(fsStub).toHaveBeenCalledTimes(1); 131 | 132 | // restore mocks 133 | compilerStub.mockRestore(); 134 | renderModuleFactoryStub.mockRestore(); 135 | fsStub.mockRestore(); 136 | 137 | done(); 138 | }); 139 | }); 140 | 141 | /** 142 | * Test if `NgEngineService.universal()` function returns success with cache 143 | */ 144 | test('- `NgEngineService.universal()` success execution with cache', (done) => { 145 | const ngE = new NgEngineService({ 146 | bootstrap: NgEngineService, lazyModuleMap: {}, staticContent: { 147 | rootPath: './root/path', 148 | indexFile: 'test.html' 149 | } 150 | }, request, reply, utils); 151 | 152 | ngE['_factoryCacheMap'].set(NgEngineService, {}); 153 | 154 | // create all mocks 155 | const renderModuleFactoryStub = jest.spyOn(ngE, '_renderModuleFactory') 156 | .mockReturnValueOnce(new Promise((resolve) => resolve('

Hello Angular

'))); 157 | const fsStub = jest.spyOn(fs, 'readFileSync').mockReturnValueOnce(Buffer.from('')); 158 | 159 | ngE.universal().subscribe(_ => { 160 | expect(_.value).toBe('

Hello Angular

'); 161 | 162 | // renderModuleFactoryStub should have been called only 1 time 163 | expect(renderModuleFactoryStub).toHaveBeenCalledTimes(1); 164 | 165 | // fsStub should have been called only 1 time 166 | expect(fsStub).toHaveBeenCalledTimes(1); 167 | 168 | // restore mocks 169 | renderModuleFactoryStub.mockRestore(); 170 | fsStub.mockRestore(); 171 | 172 | done(); 173 | }); 174 | }); 175 | 176 | /** 177 | * Test if `NgEngineService.universal()` function returns success with static content 178 | */ 179 | test('- `NgEngineService.universal()` success execution with static content', (done) => { 180 | const ngE = new NgEngineService({ 181 | bootstrap: {}, lazyModuleMap: {}, staticContent: { 182 | rootPath: './root/path', 183 | indexFile: 'test.html' 184 | } 185 | }, request, reply, utils); 186 | 187 | const fsStub = jest.spyOn(fs, 'readFileSync').mockReturnValueOnce(Buffer.from('')); 188 | const mimosStub = jest.spyOn(ngE['_mimos'], 'path').mockReturnValue({ type: 'plain/text' }); 189 | 190 | ngE.universal().subscribe(_ => { 191 | expect(_.value.toString()).toBe(''); 192 | 193 | // fsStub should have been called only 1 time 194 | expect(fsStub).toHaveBeenCalledTimes(1); 195 | 196 | // mimosStub should have been called only 1 time 197 | expect(mimosStub).toHaveBeenCalledTimes(1); 198 | 199 | // restore mocks 200 | fsStub.mockRestore(); 201 | mimosStub.mockRestore(); 202 | 203 | done(); 204 | }); 205 | }); 206 | }) 207 | ; 208 | -------------------------------------------------------------------------------- /test/unit/ng-universal.module.test.ts: -------------------------------------------------------------------------------- 1 | import { NgUniversalModule } from '../../src'; 2 | 3 | describe('- Unit ng-universal.module.test.ts file', () => { 4 | /** 5 | * Test if `NgUniversalModule` as a `setConfig` static function 6 | */ 7 | test('- `NgUniversalModule` must have `setConfig` static function', (done) => { 8 | expect(typeof NgUniversalModule.setConfig).toBe('function'); 9 | done(); 10 | }); 11 | 12 | /** 13 | * Test if `NgUniversalModule.universal()` static function returns CoreModuleWithProviders 14 | */ 15 | test('- `NgUniversalModule.setConfig()` static function must return CoreModuleWithProviders', (done) => { 16 | const cwp = NgUniversalModule.setConfig({ bootstrap: {}, lazyModuleMap: {}, staticContent: null }); 17 | expect(cwp).toHaveProperty('module'); 18 | expect(cwp).toHaveProperty('providers'); 19 | expect(cwp.providers).toHaveLength(1); 20 | const provider = cwp.providers.pop(); 21 | expect(provider).toHaveProperty('provide'); 22 | expect(provider).toHaveProperty('useValue'); 23 | done(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /tools/files.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "name":"README.md" }, 3 | { "name":"LICENSE.md" }, 4 | { "name":"package.json" } 5 | ] -------------------------------------------------------------------------------- /tools/packaging.ts: -------------------------------------------------------------------------------- 1 | // import libraries 2 | import { Observable, forkJoin } from 'rxjs'; 3 | import { flatMap } from 'rxjs/operators'; 4 | import * as fs from 'fs-extra'; 5 | 6 | /** 7 | * Interface for file object definition 8 | */ 9 | interface FileObject { 10 | name: string; 11 | } 12 | 13 | /** 14 | * Class declaration 15 | */ 16 | class Packaging { 17 | // private property to store files list 18 | private _files: FileObject[]; 19 | // private property to store src path 20 | private readonly _srcPath: string; 21 | // private property to store dest path 22 | private readonly _destPath: string; 23 | 24 | /** 25 | * Class constructor 26 | * 27 | * @param files {FileObject[]} name of each files to package and flag to know if we need to delete it after 28 | * @param src {string} src base path from current process 29 | * @param dest {string} dest base path from current process 30 | */ 31 | constructor(files: FileObject[], src: string = '', dest: string = '/dist') { 32 | this._files = files; 33 | this._srcPath = `${process.cwd()}${src}/`; 34 | this._destPath = `${process.cwd()}${dest}/`; 35 | } 36 | 37 | /** 38 | * Function to copy one file 39 | * 40 | * @param file {string} 41 | * 42 | * @return {Observable} 43 | */ 44 | private _copy(file: string): Observable { 45 | // copy package.json 46 | if (file.indexOf('package.json') !== -1) { 47 | return this._copyAndCleanupPackageJson(file); 48 | } 49 | 50 | // copy other files 51 | return > Observable.create((observer) => { 52 | fs.stat(`${this._srcPath}${file}`, (error, stats) => { 53 | if (error) { 54 | console.error('doesn\'t exist on copy =>', error.message); 55 | } 56 | if (stats && (stats.isFile() || stats.isDirectory())) { 57 | fs.copy(`${this._srcPath}${file}`, `${this._destPath}${file}`, (err) => { 58 | if (err) { 59 | console.error('copy failed =>', err.message); 60 | } 61 | 62 | observer.next(); 63 | observer.complete(); 64 | }); 65 | } else { 66 | observer.next(); 67 | observer.complete(); 68 | } 69 | }); 70 | }); 71 | } 72 | 73 | /** 74 | * Function to cleanup package.json and _copy it to dist directory 75 | * 76 | * @param file {string} 77 | * 78 | * @return {Observable} 79 | * 80 | * @private 81 | */ 82 | private _copyAndCleanupPackageJson(file: string): Observable { 83 | // function to read JSON 84 | const readJson = (src: string): Observable => Observable.create((observer) => { 85 | fs.readJson(src, (error, packageObj) => { 86 | if (error) { 87 | return observer.error(error); 88 | } 89 | 90 | observer.next(packageObj); 91 | observer.complete(); 92 | }); 93 | }); 94 | 95 | // function to write JSON 96 | const writeJson = (dest: string, data: any): Observable => Observable.create((observer) => { 97 | fs.outputJson(dest, data, (error) => { 98 | if (error) { 99 | return observer.error(error); 100 | } 101 | 102 | observer.next(); 103 | observer.complete(); 104 | }); 105 | }); 106 | 107 | // read package.json 108 | return readJson(`${this._srcPath}${file}`) 109 | .pipe( 110 | flatMap(packageObj => { 111 | // delete obsolete data in package.json 112 | delete packageObj.scripts; 113 | delete packageObj.devDependencies; 114 | 115 | // write new package.json 116 | return writeJson(`${this._destPath}${file}`, packageObj); 117 | }) 118 | ); 119 | } 120 | 121 | /** 122 | * Function that _copy all files in dist directory 123 | */ 124 | process() { 125 | forkJoin( 126 | this._files.map( 127 | (fileObject: FileObject) => this._copy(fileObject.name) 128 | ) 129 | ) 130 | .subscribe(null, error => console.error(error)); 131 | } 132 | } 133 | 134 | // process packaging 135 | new Packaging(require('./files')).process(); 136 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "rootDir": "./src", 6 | "types": [ 7 | "node" 8 | ] 9 | }, 10 | "exclude": [ 11 | "node_modules", 12 | "dist", 13 | "test", 14 | "tools", 15 | "src/injection" 16 | ] 17 | } -------------------------------------------------------------------------------- /tsconfig.build.tokens.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/injection", 5 | "rootDir": "./src/injection" 6 | }, 7 | "angularCompilerOptions": { 8 | "skipTemplateCodegen": true 9 | }, 10 | "exclude": [ 11 | "node_modules", 12 | "dist", 13 | "test", 14 | "tools", 15 | "src/index.ts", 16 | "src/module" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "declaration": true, 7 | "noImplicitAny": false, 8 | "sourceMap": true, 9 | "inlineSources": true, 10 | "noUnusedLocals": true, 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "outDir": "./tmp", 14 | "rootDir": ".", 15 | "skipLibCheck": true, 16 | "typeRoots": ["./node_modules/@types"], 17 | "types": [ 18 | "node", 19 | "fs-extra", 20 | "jest" 21 | ], 22 | "lib": [ 23 | "dom", 24 | "es2015" 25 | ] 26 | }, 27 | "compileOnSave": false, 28 | "buildOnSave": false, 29 | "exclude": [ 30 | "node_modules", 31 | "dist", 32 | "tmp" 33 | ] 34 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "comment-format": [ 5 | true, 6 | "check-space" 7 | ], 8 | "curly": true, 9 | "eofline": true, 10 | "forin": true, 11 | "indent": [ 12 | true, 13 | "spaces" 14 | ], 15 | "label-position": true, 16 | "max-line-length": [ 17 | true, 18 | 140 19 | ], 20 | "member-access": false, 21 | "member-ordering": [ 22 | true, 23 | "static-before-instance", 24 | "variables-before-functions" 25 | ], 26 | "no-arg": true, 27 | "no-bitwise": true, 28 | "no-console": [ 29 | true, 30 | "debug", 31 | "info", 32 | "time", 33 | "timeEnd", 34 | "trace" 35 | ], 36 | "no-construct": true, 37 | "no-debugger": true, 38 | "no-duplicate-variable": true, 39 | "no-empty": false, 40 | "no-eval": true, 41 | "no-inferrable-types": [true, "ignore-params"], 42 | "no-shadowed-variable": true, 43 | "no-string-literal": false, 44 | "no-switch-case-fall-through": true, 45 | "no-trailing-whitespace": true, 46 | "no-unused-expression": true, 47 | "no-use-before-declare": false, 48 | "no-var-keyword": true, 49 | "object-literal-sort-keys": false, 50 | "one-line": [ 51 | true, 52 | "check-open-brace", 53 | "check-catch", 54 | "check-else", 55 | "check-whitespace" 56 | ], 57 | "quotemark": [ 58 | true, 59 | "single" 60 | ], 61 | "radix": true, 62 | "semicolon": [ 63 | "always" 64 | ], 65 | "triple-equals": [ 66 | true, 67 | "allow-null-check" 68 | ], 69 | "typedef-whitespace": [ 70 | true, 71 | { 72 | "call-signature": "nospace", 73 | "index-signature": "nospace", 74 | "parameter": "nospace", 75 | "property-declaration": "nospace", 76 | "variable-declaration": "nospace" 77 | } 78 | ], 79 | "variable-name": false, 80 | "whitespace": [ 81 | true, 82 | "check-branch", 83 | "check-decl", 84 | "check-operator", 85 | "check-separator", 86 | "check-type" 87 | ] 88 | } 89 | } --------------------------------------------------------------------------------