Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/angular 17 #2

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
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
46 changes: 0 additions & 46 deletions .circleci/config.yml

This file was deleted.

39 changes: 39 additions & 0 deletions .github/workflows/master.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Build and Test

on:
push:
branches:
- master
pull_request:
branches:
- master

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20

- name: Restore cache
uses: actions/cache@v4
with:
path: ~/.npm
key: dependency-cache-${{ hashFiles('package-lock.json') }}
restore-keys: |
dependency-cache-

- name: Install dependencies
run: npm ci

- name: Build
run: npm run build

- name: Run tests for schematics
run: npm run test:schematics
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
Forked from @nestjs/ng-universal with support for Angular 17

<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo_text.svg" width="320" alt="Nest Logo" /></a>
</p>
Expand Down Expand Up @@ -114,7 +116,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 Expand Up @@ -152,6 +154,7 @@ In some situations, it may be required to customize the `webpack` build while us
To add a customizable `webpack` config to your project, it is recommended to install [@angular-builders/custom-webpack](https://www.npmjs.com/package/@angular-builders/custom-webpack) in the project and to set your builders appropriately.

### Example Custom Webpack

```typescript
// webpack.config.ts
import { Configuration, IgnorePlugin } from 'webpack'
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';
22 changes: 12 additions & 10 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,13 +19,15 @@ 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
) {}

static forRoot(options: AngularUniversalOptions): DynamicModule {
const indexHtml = existsSync(join(options.viewsPath, 'index.original.html'))
? 'index.original.html'
: 'index';
? join(options.viewsPath, 'index.original.html')
: join(options.viewsPath, 'index.html');

options = {
templatePath: indexHtml,
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