Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ export class CustomCacheKeyGenerator implements CacheKeyGenerator {

## Request and Response Providers

This tool uses `@nguniversal/express-engine` and will properly provide access to the Express Request and Response objects in you Angular components. Note that tokens must be imported from the `@nestjs/ng-universal/tokens`, not `@nguniversal/express-engine/tokens`.
This tool uses `@angular/ssr` and will properly provide access to the Express Request and Response objects in your Angular components.
Note since Angular 17, tokens are no longer provided by Angular, and must be imported from the `@nestjs/ng-universal/tokens`.

This is useful for things like setting the response code to 404 when your Angular router can't find a page (i.e. `path: '**'` in routing):

Expand Down
3 changes: 3 additions & 0 deletions lib/angular-universal.constants.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
export const ANGULAR_UNIVERSAL_OPTIONS = 'ANGULAR_UNIVERSAL_OPTIONS';

export const ANGULAR_UNIVERSAL_CACHE_OPTIONS =
'ANGULAR_UNIVERSAL_CACHE_OPTIONS';
18 changes: 10 additions & 8 deletions lib/angular-universal.module.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { APP_BASE_HREF } from '@angular/common';
import { DynamicModule, Inject, Module, OnModuleInit } from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';
import { existsSync } from 'fs';
import { join } from 'path';
import 'reflect-metadata';
import { ANGULAR_UNIVERSAL_OPTIONS } from './angular-universal.constants';
import {
ANGULAR_UNIVERSAL_CACHE_OPTIONS,
ANGULAR_UNIVERSAL_OPTIONS
} from './angular-universal.constants';
import { angularUniversalProviders } from './angular-universal.providers';
import { AngularUniversalOptions } from './interfaces/angular-universal-options.interface';
import { renderUniversal } from './utils/setup-universal.utils';
import { AngularUniversalCacheOptions } from './interfaces/angular-universal-cache-options.interface';

@Module({
providers: [...angularUniversalProviders]
Expand All @@ -15,6 +19,8 @@ export class AngularUniversalModule implements OnModuleInit {
constructor(
@Inject(ANGULAR_UNIVERSAL_OPTIONS)
private readonly ngOptions: AngularUniversalOptions,
@Inject(ANGULAR_UNIVERSAL_CACHE_OPTIONS)
private readonly cacheOptions: AngularUniversalCacheOptions,
private readonly httpAdapterHost: HttpAdapterHost
) {}

Expand Down Expand Up @@ -50,12 +56,8 @@ export class AngularUniversalModule implements OnModuleInit {
return;
}
const app = httpAdapter.getInstance();
app.get(this.ngOptions.renderPath, (req, res) =>
res.render(this.ngOptions.templatePath, {
req,
res,
providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }]
})
app.get(this.ngOptions.renderPath, (req, res, next) =>
renderUniversal(req, res, next, this.ngOptions, this.cacheOptions)
);
}
}
11 changes: 10 additions & 1 deletion lib/angular-universal.providers.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { Provider } from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';
import { ANGULAR_UNIVERSAL_OPTIONS } from './angular-universal.constants';
import {
ANGULAR_UNIVERSAL_CACHE_OPTIONS,
ANGULAR_UNIVERSAL_OPTIONS
} from './angular-universal.constants';
import { AngularUniversalOptions } from './interfaces/angular-universal-options.interface';
import { setupUniversal } from './utils/setup-universal.utils';
import { angularUniversalCacheOptionsFactory } from './providers/universal-cache.service';

export const angularUniversalProviders: Provider[] = [
{
Expand All @@ -15,5 +19,10 @@ export const angularUniversalProviders: Provider[] = [
host.httpAdapter &&
setupUniversal(host.httpAdapter.getInstance(), options),
inject: [HttpAdapterHost, ANGULAR_UNIVERSAL_OPTIONS]
},
{
provide: ANGULAR_UNIVERSAL_CACHE_OPTIONS,
useFactory: angularUniversalCacheOptionsFactory,
inject: [ANGULAR_UNIVERSAL_OPTIONS]
}
];
17 changes: 17 additions & 0 deletions lib/interfaces/angular-universal-cache-options.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { CacheKeyGenerator } from './cache-key-generator.interface';
import { CacheStorage } from './cache-storage.interface';

interface CacheOptionsEnabled {
isEnabled: true;
storage: CacheStorage;
expiresIn: number;
keyGenerator: CacheKeyGenerator;
}

interface CacheOptionsDisabled {
isEnabled: false;
}

export type AngularUniversalCacheOptions =
| CacheOptionsEnabled
| CacheOptionsDisabled;
31 changes: 31 additions & 0 deletions lib/providers/universal-cache.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { CacheKeyByOriginalUrlGenerator } from '../cache/cache-key-by-original-url.generator';
import { InMemoryCacheStorage } from '../cache/in-memory-cache.storage';
import { AngularUniversalCacheOptions } from '../interfaces/angular-universal-cache-options.interface';
import { AngularUniversalOptions } from '../interfaces/angular-universal-options.interface';

const DEFAULT_CACHE_EXPIRATION_TIME = 60000; // 60 seconds

export function angularUniversalCacheOptionsFactory(
ngOptions: AngularUniversalOptions
): AngularUniversalCacheOptions {
if (!ngOptions.cache) {
return {
isEnabled: false
};
}
if (typeof ngOptions.cache !== 'object') {
return {
isEnabled: true,
storage: new InMemoryCacheStorage(),
expiresIn: DEFAULT_CACHE_EXPIRATION_TIME,
keyGenerator: new CacheKeyByOriginalUrlGenerator()
};
}
return {
isEnabled: true,
storage: ngOptions.cache.storage ?? new InMemoryCacheStorage(),
expiresIn: ngOptions.cache.expiresIn ?? DEFAULT_CACHE_EXPIRATION_TIME,
keyGenerator:
ngOptions.cache.keyGenerator ?? new CacheKeyByOriginalUrlGenerator()
};
}
7 changes: 6 additions & 1 deletion lib/tokens.ts
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
export { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
import { InjectionToken } from '@angular/core';
import { Request, Response } from 'express';

export const REQUEST = new InjectionToken<Request>('REQUEST');
export const RESPONSE = new InjectionToken<Response>('RESPONSE');

114 changes: 49 additions & 65 deletions lib/utils/setup-universal.utils.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,15 @@
import { Logger } from '@nestjs/common';
import { ngExpressEngine } from '@nguniversal/express-engine';
import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine } from '@angular/ssr';
import * as express from 'express';
import { CacheKeyByOriginalUrlGenerator } from '../cache/cache-key-by-original-url.generator';
import { InMemoryCacheStorage } from '../cache/in-memory-cache.storage';
import { AngularUniversalOptions } from '../interfaces/angular-universal-options.interface';

const DEFAULT_CACHE_EXPIRATION_TIME = 60000; // 60 seconds
import { AngularUniversalOptions } from '../interfaces/angular-universal-options.interface';
import { AngularUniversalCacheOptions } from '../interfaces/angular-universal-cache-options.interface';

const logger = new Logger('AngularUniversalModule');
const commonEngine = new CommonEngine();

export function setupUniversal(app: any, ngOptions: AngularUniversalOptions) {
const cacheOptions = getCacheOptions(ngOptions);

app.engine('html', (_, options, callback) => {
let cacheKey;
if (cacheOptions.isEnabled) {
const cacheKeyGenerator = cacheOptions.keyGenerator;
cacheKey = cacheKeyGenerator.generateCacheKey(options.req);
const cacheHtml = cacheOptions.storage.get(cacheKey);
if (cacheHtml) {
return callback(null, cacheHtml);
}
}

ngExpressEngine({
bootstrap: ngOptions.bootstrap,
inlineCriticalCss: ngOptions.inlineCriticalCss,
providers: [
{
provide: 'serverUrl',
useValue: `${options.req.protocol}://${options.req.get('host')}`
},
...(ngOptions.extraProviders || [])
]
})(_, options, (err, html) => {
if (err && ngOptions.errorHandler) {
return ngOptions.errorHandler({ err, html, renderCallback: callback });
}

if (err) {
logger.error(err);
return callback(err);
}

if (cacheOptions.isEnabled && cacheKey) {
cacheOptions.storage.set(cacheKey, html, cacheOptions.expiresIn);
}
callback(null, html);
});
});

app.set('view engine', 'html');
app.set('views', ngOptions.viewsPath);

Expand All @@ -62,25 +22,49 @@ export function setupUniversal(app: any, ngOptions: AngularUniversalOptions) {
);
}

export function getCacheOptions(ngOptions: AngularUniversalOptions) {
if (!ngOptions.cache) {
return {
isEnabled: false
};
}
if (typeof ngOptions.cache !== 'object') {
return {
isEnabled: true,
storage: new InMemoryCacheStorage(),
expiresIn: DEFAULT_CACHE_EXPIRATION_TIME,
keyGenerator: new CacheKeyByOriginalUrlGenerator()
};
export function renderUniversal(
req: express.Request,
res: express.Response,
next: express.Next,
ngOptions: AngularUniversalOptions,
cacheOptions: AngularUniversalCacheOptions
) {
const { protocol, originalUrl, baseUrl, headers } = req;

const cacheKey =
cacheOptions.isEnabled && cacheOptions.keyGenerator.generateCacheKey(req);

if (cacheKey) {
const cacheHtml = cacheOptions.storage.get(cacheKey);
if (cacheHtml) {
res.send(cacheHtml);
}
}
return {
isEnabled: true,
storage: ngOptions.cache.storage || new InMemoryCacheStorage(),
expiresIn: ngOptions.cache.expiresIn || DEFAULT_CACHE_EXPIRATION_TIME,
keyGenerator:
ngOptions.cache.keyGenerator || new CacheKeyByOriginalUrlGenerator()
};

commonEngine
.render({
bootstrap: ngOptions.bootstrap,
documentFilePath: ngOptions.templatePath,
inlineCriticalCss: ngOptions.inlineCriticalCss,
url: `${protocol}://${headers.host}${originalUrl}`,
publicPath: ngOptions.viewsPath,
providers: [
{ provide: APP_BASE_HREF, useValue: baseUrl },
...(ngOptions.extraProviders ?? [])
]
})
.then((html) => {
if (cacheKey) {
cacheOptions.storage.set(cacheKey, html, cacheOptions.expiresIn);
}
res.send(html);
})
.catch((err) => {
if (ngOptions.errorHandler) {
return ngOptions.errorHandler({ err, html: '', renderCallback: next });
}

logger.error(err);
return next(err);
});
}
Loading