├── .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 |
2 |
3 |
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 |
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 | }
--------------------------------------------------------------------------------