Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit bf36e07

Browse files
committed
feat: update isr to be used with application builder
1 parent 7857591 commit bf36e07

File tree

12 files changed

+254
-70
lines changed

12 files changed

+254
-70
lines changed

apps/docs/docs/isr/getting-started.md

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,18 +58,99 @@ export class AppServerModule {}
5858
If you are in a standalone application, you can also register the provider in the **serverConfig**.
5959

6060
```typescript title="main.server.ts"
61-
import { provideISR } from '@rx-angular/isr/server';
61+
import { provideISR, isrHttpInterceptors } from '@rx-angular/isr/server';
6262

6363
const serverConfig: ApplicationConfig = {
6464
providers: [
6565
provideServerRendering(),
6666
// highlight-next-line
6767
provideISR(), // 👈 Use it in config providers
68+
69+
// register ISR Http Interceptors
70+
provideHttpClient(withInterceptors(isrHttpInterceptors)),
6871
],
6972
};
7073
```
7174

72-
## Configure server handling
75+
## Configure server handling (Common Engine)
76+
77+
Now you need to configure the ISR handler in your **server.ts** file.
78+
79+
1. Import the **ISRHandler ** class from the **@rx-angular/isr** package.
80+
2. Create a new instance of the **ISRHandler** class.
81+
3. Use the ISRHandler instance to handle the requests.
82+
4. Comment out default handler, because it's will be handled in ISR render method.
83+
84+
```typescript title="server.ts"
85+
import { CommonEngine } from '@angular/ssr';
86+
import express from 'express';
87+
import { fileURLToPath } from 'node:url';
88+
import { dirname, join, resolve } from 'node:path';
89+
import bootstrap from './src/main.server';
90+
91+
// 1. 👇 Import the ISRHandler class
92+
// highlight-next-line
93+
import { ISRHandler } from '@rx-angular/isr/server';
94+
95+
// The Express app is exported so that it can be used by serverless Functions.
96+
export function app(): express.Express {
97+
const server = express();
98+
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
99+
const browserDistFolder = resolve(serverDistFolder, '../browser');
100+
const indexHtml = join(serverDistFolder, 'index.server.html');
101+
102+
const commonEngine = new CommonEngine();
103+
104+
// 2. 👇 Instantiate the ISRHandler class with the index.html file
105+
// highlight-start
106+
const isr = new ISRHandler({
107+
indexHtml,
108+
invalidateSecretToken: 'MY_TOKEN', // replace with env secret key ex. process.env.REVALIDATE_SECRET_TOKEN
109+
enableLogging: true,
110+
serverDistFolder,
111+
browserDistFolder,
112+
bootstrap,
113+
commonEngine,
114+
});
115+
// highlight-end
116+
117+
server.use(express.json());
118+
server.post(
119+
'/api/invalidate',
120+
async (req, res) => await isr.invalidate(req, res)
121+
);
122+
123+
server.set('view engine', 'html');
124+
server.set('views', browserDistFolder);
125+
126+
// Example Express Rest API endpoints
127+
// server.get('/api/**', (req, res) => { });
128+
// Serve static files from /browser
129+
server.get(
130+
'*.*',
131+
express.static(browserDistFolder, {
132+
maxAge: '1y',
133+
})
134+
);
135+
136+
// 3. 👇 Use the ISRHandler to handle the requests
137+
// highlight-start
138+
server.get(
139+
'*',
140+
// Serve page if it exists in cache
141+
async (req, res, next) => await isr.serveFromCache(req, res, next),
142+
// Server side render the page and add to cache if needed
143+
async (req, res, next) => await isr.render(req, res, next)
144+
);
145+
// highlight-end
146+
147+
return server;
148+
}
149+
```
150+
151+
## [Optional] Configure server handling (Express Engine)
152+
153+
> ⚠️ This was the old way of configuring the server handling. It's still supported, but it's recommended to use the Common Engine way.
73154
74155
Now you need to configure the ISR handler in your **server.ts** file.
75156

@@ -166,3 +247,16 @@ The **revalidate** key is the number of seconds after which the page will be rev
166247

167248
If you don't want a specific route to be handled by the ISR handler, you just shouldn't add
168249
the **revalidate** key in the route **data** object.
250+
251+
## Start Development Server
252+
253+
In v17 the application builder won't use the `server.ts` file in development mode.
254+
So, it's recommended to build the application independently and start the server with node independently.
255+
256+
```shell
257+
ng build -c=development --watch
258+
```
259+
260+
```shell
261+
node dist/your-app/server/server.mjs --watch
262+
```

apps/ssr-isr/src/assets/.gitkeep

Whitespace-only changes.

libs/isr/.eslintrc.json

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,10 @@
44
"overrides": [
55
{
66
"files": ["*.ts"],
7-
"extends": [
8-
"plugin:@nx/angular",
9-
"plugin:@angular-eslint/template/process-inline-templates"
10-
],
7+
"extends": ["plugin:@nx/angular"],
118
"rules": {
12-
"@angular-eslint/directive-selector": [
13-
"error",
14-
{
15-
"type": "attribute",
16-
"prefix": "rx",
17-
"style": "camelCase"
18-
}
19-
],
20-
"@angular-eslint/component-selector": [
21-
"error",
22-
{
23-
"type": "element",
24-
"prefix": "rx",
25-
"style": "kebab-case"
26-
}
27-
],
289
"@typescript-eslint/no-non-null-assertion": "warn"
2910
}
30-
},
31-
{
32-
"files": ["*.html"],
33-
"extends": ["plugin:@nx/angular-template"],
34-
"rules": {
35-
"@angular-eslint/template/eqeqeq": "warn"
36-
}
3711
}
3812
]
3913
}

libs/isr/models/src/isr-handler-config.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Provider } from '@angular/core';
22
import { CacheHandler, RenderVariant } from './cache-handler';
33
import { Request } from 'express';
4+
import { CommonEngine, CommonEngineRenderOptions } from '@angular/ssr';
45

56
export interface ISRHandlerConfig {
67
/**
@@ -25,6 +26,30 @@ export interface ISRHandlerConfig {
2526
*/
2627
cache?: CacheHandler;
2728

29+
/**
30+
* An instance of a common engine. This engine is responsible for
31+
* rendering the HTML of the application.
32+
*/
33+
commonEngine?: CommonEngine;
34+
35+
/**
36+
* The bootstrap function of the application. This function is responsible for
37+
* bootstrapping the application. If not provided, defaults to null.
38+
*/
39+
bootstrap?: CommonEngineRenderOptions['bootstrap'];
40+
41+
/**
42+
* The path to the server dist folder. This folder contains the server bundle
43+
* of the application. If not provided, defaults to null.
44+
*/
45+
serverDistFolder?: string;
46+
47+
/**
48+
* The path to the browser dist folder. This folder contains the browser bundle
49+
* of the application. If not provided, defaults to null.
50+
*/
51+
browserDistFolder?: string;
52+
2853
/**
2954
* The build ID of the application. This value is used to generate unique cache keys.
3055
* If not provided, defaults to null.

libs/isr/server/src/cache-handlers/filesystem-cache-handler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import * as fs from 'fs';
2-
import { join } from 'path';
1+
import * as fs from 'node:fs';
2+
import { join } from 'node:path';
33

44
import {
55
CacheData,

libs/isr/server/src/cache-regeneration.ts

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { renderUrl } from './utils/render-url';
88
import { getRouteISRDataFromHTML } from './utils/get-isr-options';
99
import { Request, Response } from 'express';
1010
import { ISRLogger } from './isr-logger';
11+
import { CommonEngine, CommonEngineRenderOptions } from '@angular/ssr';
1112

1213
export class CacheRegeneration {
1314
// TODO: make this pluggable because on serverless environments we can't share memory between functions
@@ -17,7 +18,10 @@ export class CacheRegeneration {
1718
constructor(
1819
public isrConfig: ISRHandlerConfig,
1920
public cache: CacheHandler,
20-
public indexHtml: string
21+
public indexHtml: string,
22+
public commonEngine?: CommonEngine,
23+
public bootstrap?: CommonEngineRenderOptions['bootstrap'],
24+
public browserDistFolder?: string
2125
) {}
2226

2327
async regenerate(
@@ -40,27 +44,34 @@ export class CacheRegeneration {
4044

4145
this.urlsOnHold.push(url);
4246

43-
renderUrl({ req, res, url, indexHtml: this.indexHtml, providers }).then(
44-
(html) => {
45-
const { errors } = getRouteISRDataFromHTML(html);
47+
renderUrl({
48+
req,
49+
res,
50+
url,
51+
indexHtml: this.indexHtml,
52+
providers,
53+
commonEngine: this.commonEngine,
54+
bootstrap: this.bootstrap,
55+
browserDistFolder: this.browserDistFolder,
56+
}).then((html) => {
57+
const { errors } = getRouteISRDataFromHTML(html);
4658

47-
// if there are errors, don't add the page to cache
48-
if (errors?.length) {
49-
// remove url from urlsOnHold because we want to try to regenerate it again
50-
this.urlsOnHold = this.urlsOnHold.filter((x) => x !== url);
51-
logger.log('💥 ERROR: Url: ' + url + ' was not regenerated!', errors);
52-
return;
53-
}
54-
55-
// add the regenerated page to cache
56-
this.cache
57-
.add(req.url, html, { revalidate, buildId: this.isrConfig.buildId })
58-
.then(() => {
59-
// remove from urlsOnHold because we are done
60-
this.urlsOnHold = this.urlsOnHold.filter((x) => x !== url);
61-
logger.log('Url: ' + url + ' was regenerated!');
62-
});
59+
// if there are errors, don't add the page to cache
60+
if (errors?.length) {
61+
// remove url from urlsOnHold because we want to try to regenerate it again
62+
this.urlsOnHold = this.urlsOnHold.filter((x) => x !== url);
63+
logger.log('💥 ERROR: Url: ' + url + ' was not regenerated!', errors);
64+
return;
6365
}
64-
);
66+
67+
// add the regenerated page to cache
68+
this.cache
69+
.add(req.url, html, { revalidate, buildId: this.isrConfig.buildId })
70+
.then(() => {
71+
// remove from urlsOnHold because we are done
72+
this.urlsOnHold = this.urlsOnHold.filter((x) => x !== url);
73+
logger.log('Url: ' + url + ' was regenerated!');
74+
});
75+
});
6576
}
6677
}

libs/isr/server/src/http-errors.interceptor.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,33 @@
1-
import { Injectable, Provider } from '@angular/core';
1+
import { Injectable, Provider, inject } from '@angular/core';
22
import {
33
HTTP_INTERCEPTORS,
44
HttpEvent,
55
HttpHandler,
6+
HttpHandlerFn,
67
HttpInterceptor,
8+
HttpInterceptorFn,
79
HttpRequest,
810
} from '@angular/common/http';
911
import { catchError, Observable, throwError } from 'rxjs';
10-
import { IsrService } from '@rx-angular/isr/browser';
12+
import { IsrServerService } from '@rx-angular/isr/server';
13+
14+
export const httpErrorInterceptorISR: HttpInterceptorFn = (
15+
req: HttpRequest<unknown>,
16+
next: HttpHandlerFn
17+
) => {
18+
const isrService = inject(IsrServerService);
19+
20+
return next(req).pipe(
21+
catchError((err) => {
22+
isrService.addError(err);
23+
return throwError(() => err);
24+
})
25+
);
26+
};
1127

1228
@Injectable()
1329
export class HttpErrorsInterceptor implements HttpInterceptor {
14-
constructor(private isrService: IsrService) {}
30+
constructor(private isrService: IsrServerService) {}
1531

1632
intercept(
1733
request: HttpRequest<unknown>,

libs/isr/server/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@ export { IsrServerService } from './isr-server.service';
99

1010
export { ISRHandler } from './isr-handler';
1111

12-
export { provideISR } from './provide-isr';
12+
export { provideISR, isrHttpInterceptors } from './provide-isr';
1313

1414
export { IsrModule } from './isr.module';

libs/isr/server/src/isr-handler.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ export class ISRHandler {
4242
this.cacheRegeneration = new CacheRegeneration(
4343
this.config,
4444
this.cache,
45-
config.indexHtml
45+
config.indexHtml,
46+
config.commonEngine,
47+
config.bootstrap,
48+
config.browserDistFolder
4649
);
4750
}
4851

@@ -95,6 +98,9 @@ export class ISRHandler {
9598
url,
9699
indexHtml,
97100
providers: config?.providers,
101+
bootstrap: this.config.bootstrap,
102+
commonEngine: this.config.commonEngine,
103+
browserDistFolder: this.config.browserDistFolder,
98104
});
99105

100106
// get revalidate data in order to set it to cache data
@@ -241,6 +247,9 @@ export class ISRHandler {
241247
url: req.url,
242248
indexHtml: this.config.indexHtml,
243249
providers: config?.providers,
250+
bootstrap: this.config.bootstrap,
251+
commonEngine: this.config.commonEngine,
252+
browserDistFolder: this.config.browserDistFolder,
244253
};
245254

246255
renderUrl(renderUrlConfig).then(async (html) => {

libs/isr/server/src/provide-isr.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';
22
import { IsrServerService } from './isr-server.service';
3-
import { HTTP_ERROR_PROVIDER_ISR } from './http-errors.interceptor';
3+
import {
4+
HTTP_ERROR_PROVIDER_ISR,
5+
httpErrorInterceptorISR,
6+
} from './http-errors.interceptor';
47
import { BEFORE_APP_SERIALIZED } from '@angular/platform-server';
58
import { DOCUMENT, isPlatformServer } from '@angular/common';
69
import { addIsrDataBeforeSerialized } from './utils/add-isr-data-before-serialized';
@@ -62,3 +65,20 @@ export const provideISR = (): EnvironmentProviders => {
6265
},
6366
]);
6467
};
68+
69+
/**
70+
* @description
71+
* This function registers the providers needed for ISR to work.
72+
*
73+
* @usage
74+
* ```ts
75+
* import { isrHttpInterceptors } from '@rx-angular/isr/server';
76+
*
77+
* providers: [
78+
* provideHttpClient(
79+
* withInterceptors(isrHttpInterceptors)
80+
* )
81+
* ]
82+
* ```
83+
*/
84+
export const isrHttpInterceptors = [httpErrorInterceptorISR];

0 commit comments

Comments
 (0)