diff --git a/common/changes/@rushstack/module-minifier/module-minifier-no-cache_2025-10-23-21-27.json b/common/changes/@rushstack/module-minifier/module-minifier-no-cache_2025-10-23-21-27.json new file mode 100644 index 00000000000..7d697035db1 --- /dev/null +++ b/common/changes/@rushstack/module-minifier/module-minifier-no-cache_2025-10-23-21-27.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/module-minifier", + "comment": "Add the ability to disable the minifier result cache to \"LocalMinifier\" and \"WorkerPoolMinifier\".", + "type": "minor" + } + ], + "packageName": "@rushstack/module-minifier" +} \ No newline at end of file diff --git a/common/reviews/api/module-minifier.api.md b/common/reviews/api/module-minifier.api.md index e4aa1c13a60..d513a65fb2e 100644 --- a/common/reviews/api/module-minifier.api.md +++ b/common/reviews/api/module-minifier.api.md @@ -16,6 +16,7 @@ export function getIdentifier(ordinal: number): string; // @public export interface ILocalMinifierOptions { + cache?: boolean; // (undocumented) terserOptions?: MinifyOptions; } @@ -77,6 +78,7 @@ export interface IModuleMinifierFunction { // @public export interface IWorkerPoolMinifierOptions { + cache?: boolean; maxThreads?: number; terserOptions?: MinifyOptions; verbose?: boolean; diff --git a/libraries/module-minifier/src/LocalMinifier.ts b/libraries/module-minifier/src/LocalMinifier.ts index 9f64006cc0f..9365dcb68d2 100644 --- a/libraries/module-minifier/src/LocalMinifier.ts +++ b/libraries/module-minifier/src/LocalMinifier.ts @@ -20,6 +20,10 @@ import { minifySingleFileAsync } from './MinifySingleFile'; * @public */ export interface ILocalMinifierOptions { + /** + * Indicates whether to cache minification results in memory. + */ + cache?: boolean; terserOptions?: MinifyOptions; } @@ -30,11 +34,11 @@ export interface ILocalMinifierOptions { export class LocalMinifier implements IModuleMinifier { private readonly _terserOptions: MinifyOptions; - private readonly _resultCache: Map; + private readonly _resultCache: Map | undefined; private readonly _configHash: string; public constructor(options: ILocalMinifierOptions) { - const { terserOptions = {} } = options || {}; + const { terserOptions = {}, cache = true } = options || {}; this._terserOptions = { ...terserOptions, @@ -53,7 +57,7 @@ export class LocalMinifier implements IModuleMinifier { .update(serialize(terserOptions)) .digest('base64'); - this._resultCache = new Map(); + this._resultCache = cache ? new Map() : undefined; } /** @@ -64,14 +68,14 @@ export class LocalMinifier implements IModuleMinifier { public minify(request: IModuleMinificationRequest, callback: IModuleMinificationCallback): void { const { hash } = request; - const cached: IModuleMinificationResult | undefined = this._resultCache.get(hash); + const cached: IModuleMinificationResult | undefined = this._resultCache?.get(hash); if (cached) { return callback(cached); } minifySingleFileAsync(request, this._terserOptions) .then((result: IModuleMinificationResult) => { - this._resultCache.set(hash, result); + this._resultCache?.set(hash, result); callback(result); }) .catch((error) => { diff --git a/libraries/module-minifier/src/WorkerPoolMinifier.ts b/libraries/module-minifier/src/WorkerPoolMinifier.ts index 4a81fbc75a0..62fe6df0de1 100644 --- a/libraries/module-minifier/src/WorkerPoolMinifier.ts +++ b/libraries/module-minifier/src/WorkerPoolMinifier.ts @@ -34,6 +34,11 @@ export interface IWorkerPoolMinifierOptions { */ terserOptions?: MinifyOptions; + /** + * Indicates whether to cache minification results in memory. + */ + cache?: boolean; + /** * If true, log to the console about the minification results. */ @@ -58,11 +63,12 @@ export class WorkerPoolMinifier implements IModuleMinifier { private _deduped: number; private _minified: number; - private readonly _resultCache: Map; + private readonly _resultCache: Map | undefined; private readonly _activeRequests: Map; public constructor(options: IWorkerPoolMinifierOptions) { const { + cache = true, maxThreads = os.availableParallelism?.() ?? os.cpus().length, terserOptions = {}, verbose = false, @@ -70,7 +76,7 @@ export class WorkerPoolMinifier implements IModuleMinifier { } = options || {}; const activeRequests: Map = new Map(); - const resultCache: Map = new Map(); + const resultCache: Map | undefined = cache ? new Map() : undefined; const terserPool: WorkerPool = new WorkerPool({ id: 'Minifier', maxWorkers: maxThreads, @@ -113,7 +119,7 @@ export class WorkerPoolMinifier implements IModuleMinifier { public minify(request: IModuleMinificationRequest, callback: IModuleMinificationCallback): void { const { hash } = request; - const cached: IModuleMinificationResult | undefined = this._resultCache.get(hash); + const cached: IModuleMinificationResult | undefined = this._resultCache?.get(hash); if (cached) { ++this._deduped; return callback(cached); @@ -141,7 +147,7 @@ export class WorkerPoolMinifier implements IModuleMinifier { message.hash )!; activeRequests.delete(message.hash); - this._resultCache.set(message.hash, message); + this._resultCache?.set(message.hash, message); for (const workerCallback of workerCallbacks) { workerCallback(message); } @@ -180,7 +186,7 @@ export class WorkerPoolMinifier implements IModuleMinifier { console.log(`Shutting down minifier worker pool`); } await this._pool.finishAsync(); - this._resultCache.clear(); + this._resultCache?.clear(); this._activeRequests.clear(); if (this._verbose) { // eslint-disable-next-line no-console diff --git a/libraries/module-minifier/src/test/LocalMinifier.test.ts b/libraries/module-minifier/src/test/LocalMinifier.test.ts index 1be9cfa8be8..e8eea93978c 100644 --- a/libraries/module-minifier/src/test/LocalMinifier.test.ts +++ b/libraries/module-minifier/src/test/LocalMinifier.test.ts @@ -2,6 +2,7 @@ // See LICENSE in the project root for license information. import type { IMinifierConnection } from '../types'; +import type { minifySingleFileAsync } from '../MinifySingleFile'; let terserVersion: string = '1.0.0'; jest.mock('terser/package.json', () => { @@ -12,7 +13,25 @@ jest.mock('terser/package.json', () => { }; }); +const mockMinifySingleFileAsync: jest.MockedFunction = jest.fn(); +jest.mock('../MinifySingleFile', () => { + return { + minifySingleFileAsync: mockMinifySingleFileAsync + }; +}); + describe('LocalMinifier', () => { + beforeEach(() => { + mockMinifySingleFileAsync.mockReset().mockImplementation(async (req) => { + return { + code: `minified(${req.code})`, + map: undefined, + hash: req.hash, + error: undefined + }; + }); + }); + it('Includes terserOptions in config hash', async () => { const { LocalMinifier } = await import('../LocalMinifier'); // eslint-disable-next-line @typescript-eslint/no-redeclare @@ -58,4 +77,58 @@ describe('LocalMinifier', () => { expect(connection2.configHash).toMatchSnapshot('terser-5.16.1'); expect(connection1.configHash !== connection2.configHash); }); + + it('Deduplicates when cache is enabled', async () => { + const { LocalMinifier } = await import('../LocalMinifier'); + // eslint-disable-next-line @typescript-eslint/no-redeclare + type LocalMinifier = typeof LocalMinifier.prototype; + + const minifier1: LocalMinifier = new LocalMinifier({}); + + let completedRequests: number = 0; + function onRequestComplete(): void { + completedRequests++; + } + + const connection1: IMinifierConnection = await minifier1.connectAsync(); + await minifier1.minify( + { hash: 'hash1', code: 'code1', nameForMap: undefined, externals: undefined }, + onRequestComplete + ); + await minifier1.minify( + { hash: 'hash1', code: 'code1', nameForMap: undefined, externals: undefined }, + onRequestComplete + ); + await connection1.disconnectAsync(); + + expect(completedRequests).toBe(2); + expect(mockMinifySingleFileAsync).toHaveBeenCalledTimes(1); + }); + + it('Does not deduplicate when cache is disabled', async () => { + const { LocalMinifier } = await import('../LocalMinifier'); + // eslint-disable-next-line @typescript-eslint/no-redeclare + type LocalMinifier = typeof LocalMinifier.prototype; + + const minifier1: LocalMinifier = new LocalMinifier({ cache: false }); + + let completedRequests: number = 0; + function onRequestComplete(): void { + completedRequests++; + } + + const connection1: IMinifierConnection = await minifier1.connectAsync(); + await minifier1.minify( + { hash: 'hash1', code: 'code1', nameForMap: undefined, externals: undefined }, + onRequestComplete + ); + await minifier1.minify( + { hash: 'hash1', code: 'code1', nameForMap: undefined, externals: undefined }, + onRequestComplete + ); + await connection1.disconnectAsync(); + + expect(completedRequests).toBe(2); + expect(mockMinifySingleFileAsync).toHaveBeenCalledTimes(2); + }); });