Skip to content

Commit

Permalink
feat: add support for Angular 17
Browse files Browse the repository at this point in the history
  • Loading branch information
michaeljota authored and Markus-Ende committed Jan 31, 2024
1 parent db6fb9c commit 84c35cd
Show file tree
Hide file tree
Showing 18 changed files with 1,176 additions and 7,549 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ 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 that tokens 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

0 comments on commit 84c35cd

Please sign in to comment.