diff --git a/apps/docs/docs/isr/cache-handlers.md b/apps/docs/docs/isr/cache-handlers.md index 699964782a..0219459f72 100644 --- a/apps/docs/docs/isr/cache-handlers.md +++ b/apps/docs/docs/isr/cache-handlers.md @@ -71,85 +71,63 @@ export class RedisCacheHandler extends CacheHandler { this.redis = new Redis(this.options.connectionString); console.log('RedisCacheHandler initialized šŸš€'); + options.keyPrefix = options.keyPrefix || 'isr'; } - add( - url: string, - html: string, - options: ISROptions = { revalidate: null } - ): Promise { - const htmlWithMsg = html + cacheMsg(options.revalidate); - - return new Promise((resolve, reject) => { - const cacheData: CacheData = { - html: htmlWithMsg, - options, - createdAt: Date.now(), - }; - const key = this.createKey(url); - this.redis.set(key, JSON.stringify(cacheData)).then(() => { - resolve(); - }); + add(cacheKey: string, html: string | Buffer, options: ISROptions = { revalidate: null }): Promise { + const key = this.createKey(cacheKey); + const createdAt = Date.now().toString(); + await this.redis.hmset(key, { + html, + revalidate: config.revalidate ? config.revalidate.toString() : '', + buildId: config.buildId || '', + createdAt, }); } - get(url: string): Promise { - return new Promise((resolve, reject) => { - const key = this.createKey(url); - this.redis.get(key, (err, result) => { - if (err || result === null || result === undefined) { - reject('This url does not exist in cache!'); - } else { - resolve(JSON.parse(result)); - } - }); - }); + // in this example, it is assumed that the html is stored as a buffer, use hgetall if it is stored as a string + async get(cacheKey: string): Promise { + const key = this.createKey(cacheKey); + const data = await this.redis.hgetallBuffer(key); + if (Object.keys(data).length > 0) { + const revalidate = data['revalidate'] ? parseInt(data['revalidate'].toString(), 10) : null; + return { + html: data['html'], + options: { + revalidate, + buildId: data['buildId'].toString() || null, + }, + createdAt: parseInt(data['createdAt'].toString(), 10), + } as CacheData; + } else { + this.logger.info(`Cache with key ${cacheKey} not found`); + throw new Error(`Cache with key ${cacheKey} not found`); + } } - getAll(): Promise { - console.log('getAll() is not implemented for RedisCacheHandler'); - return Promise.resolve([]); + async getAll(): Promise { + return await this.redis.keys(`${this.redisCacheOptions.keyPrefix}:*`); } - has(url: string): Promise { - return new Promise((resolve, reject) => { - const key = this.createKey(url); - resolve(this.redis.exists(key).then((exists) => exists === 1)); - }); + async has(cacheKey: string): Promise { + const key = this.createKey(cacheKey); + return (await this.redis.exists(key)) === 1; } - delete(url: string): Promise { - return new Promise((resolve, reject) => { - const key = this.createKey(url); - resolve(this.redis.del(key).then((deleted) => deleted === 1)); - }); + async delete(cacheKey: string): Promise { + const key = this.createKey(cacheKey); + return (await this.redis.del(key)) === 1; } - clearCache?(): Promise { - throw new Error('Method not implemented.'); + async clearCache(): Promise { + await this.redis.flushdb(); + return true; } - private createKey(url: string): string { - const prefix = this.options.keyPrefix || 'isr'; - return `${prefix}:${url}`; + private createKey(cacheKey: string): string { + return `${this.redisCacheOptions.keyPrefix}:${cacheKey}`; } } - -const cacheMsg = (revalidateTime?: number | null): string => { - const time = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, ''); - - let msg = ''; - - return msg; -}; ``` And then, to register the cache handler, you need to pass it to the `cache` field in ISRHandler: @@ -203,15 +181,22 @@ The `CacheHandler` abstract class has the following API: ```typescript export abstract class CacheHandler { - abstract add(url: string, html: string, options: ISROptions): Promise; + // html could be a string or a buffer, it is depending on if `compressHtml` is set in `ISRHandler` config. + // if `compressHtml` is set, the html will be a buffer, otherwise it will be a string + abstract add( + cacheKey: string, + // it will be buffer when we use compressHtml + html: string | Buffer, + config?: CacheISRConfig, + ): Promise; - abstract get(url: string): Promise; + abstract get(cacheKey: string): Promise; - abstract getAll(): Promise; + abstract has(cacheKey: string): Promise; - abstract has(url: string): Promise; + abstract delete(cacheKey: string): Promise; - abstract delete(url: string): Promise; + abstract getAll(): Promise; abstract clearCache?(): Promise; } @@ -223,8 +208,11 @@ The `CacheData` interface is used to store the cached pages in the cache handler ```typescript export interface CacheData { - html: string; + html: string | Buffer; options: ISROptions; createdAt: number; } ``` + +**note**: The `html` field can be a string or a buffer. It depends on if you set `compressHtml` function in the `ISRHandler` options. +If it is set, the html will be compressed and stored as a buffer. If it is not set, the html will be stored as a string. diff --git a/apps/docs/docs/isr/cache-hooks.md b/apps/docs/docs/isr/cache-hooks.md index 132712e545..a489cf41b4 100644 --- a/apps/docs/docs/isr/cache-hooks.md +++ b/apps/docs/docs/isr/cache-hooks.md @@ -14,13 +14,13 @@ To do that, you can use the `modifyCachedHtml` and `modifyGeneratedHtml` callbac ### modifyCachedHtml -The `modifyCachedHtml` callback is called when the html is served from cache (on every user request). It receives the request and the cached html as parameters. It should return the modified html. +The `modifyCachedHtml` callback is called when the html is served from cache (on every user request). It receives the request and the cached html as parameters. It should return the modified html. However, if compressHtml is set, this callback will not be called, since the cached html is compressed and cannot be modified. ### modifyGeneratedHtml The `modifyGeneratedHtml` callback is called when the html is generated on the fly (before the cache is stored). It receives the request and the generated html as parameters. It should return the modified html. -### Example +#### Example ```ts server.get( @@ -55,3 +55,7 @@ server.get( :::caution **Important:** Use these methods with caution as the logic written can increase the processing time. ::: + +### compressHtml (> v18.1.0) + +A compression callback can be provided to compress the HTML before storing it in the cache. If not provided, the HTML will be stored without compression. When provided, the HTML will be compressed and stored as Buffer | string in the cache (depending on how cache handler is implemented. Default examples use Buffer). Note that this will disable the modifyCachedHtml callback, as compressed HTML cannot be modified. diff --git a/apps/docs/docs/isr/compression.md b/apps/docs/docs/isr/compression.md new file mode 100644 index 0000000000..3c3491b23a --- /dev/null +++ b/apps/docs/docs/isr/compression.md @@ -0,0 +1,54 @@ +--- +sidebar_label: Compression +sidebar_position: 12 +title: Compression +--- + +## Why Compression? + +Caching pages on the server can lead to high memory usage, especially when caching a large number of pages. Even when caching pages on disk, reading them from disk and sending them to the client can result in high disk I/O usage. + +Typically, reverse proxies like Nginx are used to compress responses before sending them to clients. However, if we compress cached pages and serve them as compressed responses, we eliminate the need to compress them every time in Nginx, reducing server load and improving performance. + +## How to Use Compression + +You can enable compression by setting the `compressHtml` property in `ISRHandlerConfig` to a compression callback function. This function will be called with the HTML content of the page and should return the compressed HTML content. The signature of the function is: + +```typescript +export type CompressHtmlFn = (html: string) => Promise; +``` + +### Example + +```typescript +import { ISRHandler } from '@rx-angular/isr'; +import { CompressHtmlFn } from '@rx-angular/isr/models'; +import * as zlib from 'zlib'; + +// Example compressHtml function +const compressHtml: CompressHtmlFn = (html: string): Promise => { + return new Promise((resolve, reject) => { + zlib.gzip(html, (err, buffer) => { + if (err) { + reject(err); + } else { + resolve(buffer); + } + }); + }); +}; + +// ISRHandler configuration +const isr = new ISRHandler({ + indexHtml, + // other options omitted for brevity + compressHtml: compressHtml, // compress the HTML before storing in cache + htmlCompressionMethod: 'gzip', // specify the compression method, default is 'gzip' +}); +``` + +## Important Notes + +- **HTML Parameter Type**: With compression enabled, the type of the `html` parameter in `CacheHandler` will be `Buffer` instead of `string`. +- **Content-Encoding Header**: The `htmlCompressionMethod` property is used for the `Content-Encoding` header and should match the compression method used in the `compressHtml` function. +- **Modify Cached HTML**: The `modifyCachedHtml` function will be ignored when `compressHtml` is set, since it is not possible to modify the compressed cached HTML content without decompressing it first. diff --git a/apps/ssr-isr/server.ts b/apps/ssr-isr/server.ts index a3db3e3b81..eda7dd1310 100644 --- a/apps/ssr-isr/server.ts +++ b/apps/ssr-isr/server.ts @@ -1,6 +1,11 @@ import { CommonEngine } from '@angular/ssr'; import { ModifyHtmlCallbackFn } from '@rx-angular/isr/models'; -import { ISRHandler } from '@rx-angular/isr/server'; +import { + compressHtml, + CompressStaticFilter, + ISRHandler, +} from '@rx-angular/isr/server'; +import compression from 'compression'; import express, { Request } from 'express'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -34,9 +39,12 @@ export function app(): express.Express { backgroundRevalidation: true, // will revalidate in background and serve the cache page first nonBlockingRender: true, // will serve page first and store in cache in background modifyGeneratedHtml: customModifyGeneratedHtml, + compressHtml: compressHtml, // compress the html before storing in cache + // cacheHtmlCompressionMethod: 'gzip', // compression method for cache // cache: fsCacheHandler, }); - + // compress js|css files + server.use(compression({ filter: CompressStaticFilter })); server.use(express.json()); server.post( diff --git a/libs/isr/models/src/cache-handler.ts b/libs/isr/models/src/cache-handler.ts index b87f34a9c7..5b46d77fef 100644 --- a/libs/isr/models/src/cache-handler.ts +++ b/libs/isr/models/src/cache-handler.ts @@ -10,8 +10,10 @@ export interface CacheISRConfig { errors?: string[]; } +// html could be a string or a buffer, it is depending on if `compressHtml` is set in `ISRHandler` config. +// if `compressHtml` is set, the html will be a buffer, otherwise it will be a string export interface CacheData { - html: string; + html: string | Buffer; options: CacheISRConfig; createdAt: number; } @@ -29,9 +31,12 @@ export interface VariantRebuildItem { } export abstract class CacheHandler { + // html could be a string or a buffer, it is depending on if `compressHtml` is set in `ISRHandler` config. + // if `compressHtml` is set, the html will be a buffer, otherwise it will be a string abstract add( cacheKey: string, - html: string, + // it will be buffer when we use compressHtml + html: string | Buffer, config?: CacheISRConfig, ): Promise; diff --git a/libs/isr/models/src/index.ts b/libs/isr/models/src/index.ts index b849bf4abe..f1cc2a1637 100644 --- a/libs/isr/models/src/index.ts +++ b/libs/isr/models/src/index.ts @@ -8,6 +8,7 @@ export { } from './cache-handler'; export { CacheKeyGeneratorFn, + CompressHtmlFn, InvalidateConfig, ISRHandlerConfig, ModifyHtmlCallbackFn, diff --git a/libs/isr/models/src/isr-handler-config.ts b/libs/isr/models/src/isr-handler-config.ts index bf0f518e18..d35e91d156 100644 --- a/libs/isr/models/src/isr-handler-config.ts +++ b/libs/isr/models/src/isr-handler-config.ts @@ -125,6 +125,19 @@ export interface ISRHandlerConfig { */ backgroundRevalidation?: boolean; + /** + * A compression callback can be provided to compress the HTML before storing it in the cache. + * If not provided, the HTML will be stored without compression. + * When provided, the HTML will be compressed and stored as Buffer | string in the cache + * (depending on how cache handler is implemented. Default examples use Buffer) + * Note that this will disable the modifyCachedHtml callback, as compressed HTML cannot be modified. + **/ + compressHtml?: CompressHtmlFn; + + /** + * Cached Html compression method, it will use gzip by default if not provided. + */ + htmlCompressionMethod?: string; /** * This callback lets you use custom cache key generation logic. If not provided, it will use the default cache key generation logic. */ @@ -192,3 +205,5 @@ export interface RenderConfig { export interface RouteISRConfig { revalidate?: number | null; } + +export type CompressHtmlFn = (html: string) => Promise; diff --git a/libs/isr/server/src/cache-generation.ts b/libs/isr/server/src/cache-generation.ts index 2d06e4d72b..68ac9b98b9 100644 --- a/libs/isr/server/src/cache-generation.ts +++ b/libs/isr/server/src/cache-generation.ts @@ -13,7 +13,7 @@ import { getRouteISRDataFromHTML } from './utils/get-isr-options'; import { renderUrl, RenderUrlConfig } from './utils/render-url'; export interface IGeneratedResult { - html?: string; + html?: string | Buffer; errors?: string[]; } @@ -56,7 +56,6 @@ export class CacheGeneration { return this.generateWithCacheKey(req, res, cacheKey, providers, mode); } - async generateWithCacheKey( req: Request, res: Response, @@ -77,7 +76,6 @@ export class CacheGeneration { this.urlsOnHold.push(cacheKey); } - const renderUrlConfig: RenderUrlConfig = { req, res, @@ -89,17 +87,19 @@ export class CacheGeneration { browserDistFolder: this.isrConfig.browserDistFolder, inlineCriticalCss: this.isrConfig.inlineCriticalCss, }; - try { const html = await renderUrl(renderUrlConfig); const { revalidate, errors } = getRouteISRDataFromHTML(html); // Apply the modify generation callback // If undefined, use the default modifyGeneratedHtml function - const finalHtml = this.isrConfig.modifyGeneratedHtml + let finalHtml: string | Buffer = this.isrConfig.modifyGeneratedHtml ? this.isrConfig.modifyGeneratedHtml(req, html, revalidate) : defaultModifyGeneratedHtml(req, html, revalidate); - + // Apply the compressHtml callback + if (this.isrConfig.compressHtml) { + finalHtml = await this.isrConfig.compressHtml(finalHtml); + } // if there are errors, don't add the page to cache if (errors?.length && this.isrConfig.skipCachingOnHttpError) { // remove url from urlsOnHold because we want to try to regenerate it again diff --git a/libs/isr/server/src/cache-handlers/filesystem-cache-handler.ts b/libs/isr/server/src/cache-handlers/filesystem-cache-handler.ts index 2299c5d9e7..1bff25da8e 100644 --- a/libs/isr/server/src/cache-handlers/filesystem-cache-handler.ts +++ b/libs/isr/server/src/cache-handlers/filesystem-cache-handler.ts @@ -16,6 +16,7 @@ export interface FileSystemCacheOptions { interface FileSystemCacheData { htmlFilePath: string; // full path to file options: CacheISRConfig; + isBuffer: boolean; createdAt: number; } @@ -43,7 +44,7 @@ export class FileSystemCacheHandler extends CacheHandler { async add( cacheKey: string, - html: string, + html: string | Buffer, config?: CacheISRConfig, ): Promise { return new Promise((resolve, reject) => { @@ -61,6 +62,7 @@ export class FileSystemCacheHandler extends CacheHandler { htmlFilePath: filePath, options: config || { revalidate: null }, createdAt: Date.now(), + isBuffer: Buffer.isBuffer(html), }); resolve(); @@ -76,7 +78,7 @@ export class FileSystemCacheHandler extends CacheHandler { if (cachedMeta) { // on html field we have saved path to file - this.readFromFile(cachedMeta.htmlFilePath) + this.readFromFile(cachedMeta.htmlFilePath, cachedMeta.isBuffer) .then((html) => { const cacheData: CacheData = { html, @@ -87,7 +89,7 @@ export class FileSystemCacheHandler extends CacheHandler { }) .catch((err) => { reject( - `Error: šŸ’„ Cannot read cache file for route ${cacheKey}: ${cachedMeta.htmlFilePath}, ${err}`, + `Error: šŸ’„ Cannot read cache file for cacheKey ${cacheKey}: ${cachedMeta.htmlFilePath}, ${err}`, ); }); } else { @@ -102,15 +104,15 @@ export class FileSystemCacheHandler extends CacheHandler { delete(cacheKey: string): Promise { return new Promise((resolve, reject) => { - const cacheMeta = this.cache.get(cacheKey); + const cachedMeta = this.cache.get(cacheKey); - if (cacheMeta) { - fs.unlink(cacheMeta.htmlFilePath, (err) => { + if (cachedMeta) { + fs.unlink(cachedMeta.htmlFilePath, (err) => { if (err) { reject( - 'Error: šŸ’„ Cannot delete cache file for route ' + + 'Error: šŸ’„ Cannot delete cache file for cacheKey ' + cacheKey + - `: ${cacheMeta.htmlFilePath}`, + `: ${cachedMeta.htmlFilePath}`, ); } else { this.cache.delete(cacheKey); @@ -118,7 +120,7 @@ export class FileSystemCacheHandler extends CacheHandler { } }); } else { - reject(`Error: šŸ’„ CacheKey: ${cacheKey} is not cached.`); + reject(`Error: šŸ’„ cacheKey: ${cacheKey} is not cached.`); } }); } @@ -179,9 +181,10 @@ export class FileSystemCacheHandler extends CacheHandler { htmlFilePath: filePath, // full path to file options: { revalidate, errors }, createdAt: Date.now(), + isBuffer: false, }); - console.log('The request was stored in cache! Route: ', cacheKey); + console.log('The request was stored in cache! cacheKey: ', cacheKey); } } @@ -219,7 +222,9 @@ export class FileSystemCacheHandler extends CacheHandler { } } } catch (err) { - console.error('ERROR! šŸ’„ ! Cannot read folder: ' + folderPath); + console.error( + `ERROR! šŸ’„ ! Cannot read folder: ${folderPath}, err: ${err instanceof Error ? err.message : ''}`, + ); } for (const { path } of pathsToCache) { @@ -265,9 +270,13 @@ export class FileSystemCacheHandler extends CacheHandler { ); } - private async readFromFile(filePath: string): Promise { + private async readFromFile( + filePath: string, + isBuffer: boolean, + ): Promise { + const options = isBuffer ? undefined : 'utf-8'; return new Promise((resolve, reject) => { - fs.readFile(filePath, 'utf-8', (err, data) => { + fs.readFile(filePath, options, (err, data) => { if (err) { console.error('ERROR! šŸ’„ ! Cannot read file: ' + filePath); reject(err); @@ -309,7 +318,9 @@ function findIndexHtmlFilesRecursively( }); } catch (err) { // If an error occurs, log an error message and return an empty array - console.error('ERROR! šŸ’„ ! Cannot read folder: ' + path); + console.error( + `ERROR! šŸ’„ ! Cannot read folder: ${path}, err: ${err instanceof Error ? err.message : ''}`, + ); return []; } @@ -338,14 +349,14 @@ function getFileFullPath(fileName: string, cacheFolderPath: string): string { } /** - * This function takes a string parameter 'route' and replaces all '/' characters in it with '__' and returns the modified string. + * This function takes a string parameter 'cacheKey' and replaces all '/' characters in it with '__' and returns the modified string. * * @internal - * @param {string} cacheKey - The string representing the route to be converted into a file name. + * @param {string} cacheKey - The string representing the cacheKey to be converted into a file name. * @returns {string} The modified string representing the file name obtained by replacing '/' characters with '__'. */ export function convertCacheKeyToFileName(cacheKey: string): string { - // replace all occurrences of '/' character in the 'route' string with '__' using regular expression + // replace all occurrences of '/' character in the 'cacheKey' string with '__' using regular expression return cacheKey .replace(new RegExp('/', 'g'), '__') .replace(new RegExp('\\?', 'g'), '++'); @@ -353,7 +364,7 @@ export function convertCacheKeyToFileName(cacheKey: string): string { /** * This function takes a string parameter 'fileName' and replaces all '__' strings in it with '/' and returns the modified string. - * @param fileName - The string representing the file name to be converted into a route. + * @param fileName - The string representing the file name to be converted into a cacheKey. */ export function convertFileNameToCacheKey(fileName: string): string { // replace all occurrences of '__' string in the 'fileName' string with '/' using regular expression diff --git a/libs/isr/server/src/cache-handlers/in-memory-cache-handler.ts b/libs/isr/server/src/cache-handlers/in-memory-cache-handler.ts index c00f6757d9..3f66732387 100644 --- a/libs/isr/server/src/cache-handlers/in-memory-cache-handler.ts +++ b/libs/isr/server/src/cache-handlers/in-memory-cache-handler.ts @@ -17,7 +17,7 @@ export class InMemoryCacheHandler extends CacheHandler { add( cacheKey: string, - html: string, + html: string | Buffer, config: CacheISRConfig = defaultCacheISRConfig, ): Promise { return new Promise((resolve) => { diff --git a/libs/isr/server/src/compress-html.ts b/libs/isr/server/src/compress-html.ts new file mode 100644 index 0000000000..faec9dcfdc --- /dev/null +++ b/libs/isr/server/src/compress-html.ts @@ -0,0 +1,14 @@ +import { CompressHtmlFn } from '@rx-angular/isr/models'; +import * as zlib from 'zlib'; + +export const compressHtml: CompressHtmlFn = (html: string): Promise => { + return new Promise((resolve, reject) => { + zlib.gzip(html, (err, buffer) => { + if (err) { + reject(err); + } else { + resolve(buffer); + } + }); + }); +}; diff --git a/libs/isr/server/src/index.ts b/libs/isr/server/src/index.ts index 23a6c623b6..a109f0eb55 100644 --- a/libs/isr/server/src/index.ts +++ b/libs/isr/server/src/index.ts @@ -3,7 +3,9 @@ export { FileSystemCacheOptions, } from './cache-handlers/filesystem-cache-handler'; export { InMemoryCacheHandler } from './cache-handlers/in-memory-cache-handler'; +export { compressHtml } from './compress-html'; export { IsrModule } from './isr.module'; export { ISRHandler } from './isr-handler'; export { IsrServerService } from './isr-server.service'; export { isrHttpInterceptors, provideISR } from './provide-isr'; +export { CompressStaticFilter } from './utils/compression-utils'; diff --git a/libs/isr/server/src/isr-handler.ts b/libs/isr/server/src/isr-handler.ts index dd45fd9f62..011bff61bf 100644 --- a/libs/isr/server/src/isr-handler.ts +++ b/libs/isr/server/src/isr-handler.ts @@ -12,6 +12,7 @@ import { CacheGeneration } from './cache-generation'; import { InMemoryCacheHandler } from './cache-handlers/in-memory-cache-handler'; import { ISRLogger } from './isr-logger'; import { getVariant } from './utils/cache-utils'; +import { setCompressHeader } from './utils/compression-utils'; export class ISRHandler { protected cache!: CacheHandler; @@ -190,7 +191,7 @@ export class ISRHandler { // Cache exists. Send it. this.logger.log(`Page was retrieved from cache: `, cacheKey); - let finalHtml = html; + let finalHtml: string | Buffer = html; // if the cache is expired, we will regenerate it if (cacheConfig.revalidate && cacheConfig.revalidate > 0) { @@ -224,6 +225,20 @@ export class ISRHandler { } } + if (!this.isrConfig.compressHtml) { + // Apply the callback if given + // It doesn't work with compressed html + if (config?.modifyCachedHtml) { + const timeStart = performance.now(); + finalHtml = config.modifyCachedHtml(req, finalHtml as string); + const totalTime = (performance.now() - timeStart).toFixed(2); + finalHtml += ``; + } + } else { + setCompressHeader(res, this.isrConfig.htmlCompressionMethod); + } + return res.send(finalHtml); } catch (error) { // Cache does not exist. Serve user using SSR @@ -260,9 +275,12 @@ export class ISRHandler { config?.providers, 'generate', ); - if (!result) { + if (!result?.html) { throw new Error('Error while generating the page!'); } else { + if (this.isrConfig.compressHtml) { + setCompressHeader(res, this.isrConfig.htmlCompressionMethod); + } return res.send(result.html); } } catch (error) { diff --git a/libs/isr/server/src/utils/compression-utils.ts b/libs/isr/server/src/utils/compression-utils.ts new file mode 100644 index 0000000000..d229af33bc --- /dev/null +++ b/libs/isr/server/src/utils/compression-utils.ts @@ -0,0 +1,24 @@ +import * as compression from 'compression'; +import * as express from 'express'; + +export function setCompressHeader( + response: express.Response, + method?: string, +): void { + response.setHeader('Content-Encoding', method || 'gzip'); + response.setHeader('Content-type', 'text/html; charset=utf-8'); + response.setHeader('Vary', 'Accept-Encoding'); +} + +export function CompressStaticFilter( + req: express.Request, + res: express.Response, +): boolean { + const isStatic = new RegExp('.(?:js|css)$'); + if (!isStatic.test(req.url)) { + // don't compress responses with this request header + return false; + } + // fallback to standard filter function + return compression.filter(req, res); +} diff --git a/package.json b/package.json index 91a20f8030..7464b02fc3 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@angular/ssr": "18.2.9", "@typescript-eslint/utils": "7.18.0", "bootstrap": "^5.2.3", + "compression": "^1.7.4", "eslint-plugin-unused-imports": "^4.1.4", "ngx-skeleton-loader": "^7.0.0", "normalize-css": "^2.3.1", @@ -62,6 +63,7 @@ "rxjs": "7.8.0", "rxjs-zone-less": "^1.0.0", "tslib": "^2.4.1", + "zlib": "^1.0.5", "zone.js": "0.14.10" }, "devDependencies": { @@ -91,7 +93,8 @@ "@swc-node/register": "1.9.2", "@swc/core": "1.5.7", "@types/benchmark": "^2.1.0", - "@types/express": "4.17.14", + "@types/compression": "^1.7.5", + "@types/express": "4.17.21", "@types/jest": "29.5.14", "@types/klaw-sync": "^6.0.0", "@types/lodash": "^4.14.196", diff --git a/yarn.lock b/yarn.lock index de904b851b..0bf9f3b99e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10110,6 +10110,15 @@ __metadata: languageName: node linkType: hard +"@types/compression@npm:^1.7.5": + version: 1.7.5 + resolution: "@types/compression@npm:1.7.5" + dependencies: + "@types/express": "npm:*" + checksum: 10c0/3818f3d10cede38a835b40b80c341eae162aef1691f2e8f81178a77dbc109f04234cf760b6066eaa06ecbb1da143433c00db2fd9999198b76cd5a193e1d09675 + languageName: node + linkType: hard + "@types/connect-history-api-fallback@npm:^1.3.5": version: 1.3.5 resolution: "@types/connect-history-api-fallback@npm:1.3.5" @@ -10233,18 +10242,6 @@ __metadata: languageName: node linkType: hard -"@types/express-serve-static-core@npm:^4.17.18": - version: 4.17.41 - resolution: "@types/express-serve-static-core@npm:4.17.41" - dependencies: - "@types/node": "npm:*" - "@types/qs": "npm:*" - "@types/range-parser": "npm:*" - "@types/send": "npm:*" - checksum: 10c0/dc166cbf4475c00a81fbcab120bf7477c527184be11ae149df7f26d9c1082114c68f8d387a2926fe80291b06477c8bbd9231ff4f5775de328e887695aefce269 - languageName: node - linkType: hard - "@types/express@npm:*, @types/express@npm:^4.17.13": version: 4.17.17 resolution: "@types/express@npm:4.17.17" @@ -10257,19 +10254,7 @@ __metadata: languageName: node linkType: hard -"@types/express@npm:4.17.14": - version: 4.17.14 - resolution: "@types/express@npm:4.17.14" - dependencies: - "@types/body-parser": "npm:*" - "@types/express-serve-static-core": "npm:^4.17.18" - "@types/qs": "npm:*" - "@types/serve-static": "npm:*" - checksum: 10c0/616e3618dfcbafe387bf2213e1e40f77f101685f3e9efff47c66fd2da611b7578ed5f4e61e1cdb1f2a32c8f01eff4ee74f93c52ad56d45e69b7154da66b3443a - languageName: node - linkType: hard - -"@types/express@npm:^4.17.21": +"@types/express@npm:4.17.21, @types/express@npm:^4.17.21": version: 4.17.21 resolution: "@types/express@npm:4.17.21" dependencies: @@ -27377,7 +27362,8 @@ __metadata: "@swc-node/register": "npm:1.9.2" "@swc/core": "npm:1.5.7" "@types/benchmark": "npm:^2.1.0" - "@types/express": "npm:4.17.14" + "@types/compression": "npm:^1.7.5" + "@types/express": "npm:4.17.21" "@types/jest": "npm:29.5.14" "@types/klaw-sync": "npm:^6.0.0" "@types/lodash": "npm:^4.14.196" @@ -27389,6 +27375,7 @@ __metadata: benchmark: "npm:^2.1.4" bootstrap: "npm:^5.2.3" browser-sync: "npm:^3.0.0" + compression: "npm:^1.7.4" cpx: "npm:^1.5.0" cypress: "npm:13.15.2" eslint: "npm:^8.57.1" @@ -27424,6 +27411,7 @@ __metadata: ts-node: "npm:10.9.1" tslib: "npm:^2.4.1" typescript: "npm:5.5.4" + zlib: "npm:^1.0.5" zone.js: "npm:0.14.10" languageName: unknown linkType: soft @@ -31446,6 +31434,13 @@ __metadata: languageName: node linkType: hard +"zlib@npm:^1.0.5": + version: 1.0.5 + resolution: "zlib@npm:1.0.5" + checksum: 10c0/34bd33f4fdcda34f57a1ab628ceb423bdf8ef07290f46cd944eedd9a66458cc24ccf3c770da73dc0f8d28016607d861290ac5c53d49113177f7c321838df2913 + languageName: node + linkType: hard + "zone.js@npm:0.14.10": version: 0.14.10 resolution: "zone.js@npm:0.14.10"