Skip to content
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
Original file line number Diff line number Diff line change
@@ -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"
}
2 changes: 2 additions & 0 deletions common/reviews/api/module-minifier.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export function getIdentifier(ordinal: number): string;

// @public
export interface ILocalMinifierOptions {
cache?: boolean;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
cache?: boolean;
disableCache?: boolean;

true-as-default always feels unintiuitive.

// (undocumented)
terserOptions?: MinifyOptions;
}
Expand Down Expand Up @@ -77,6 +78,7 @@ export interface IModuleMinifierFunction {

// @public
export interface IWorkerPoolMinifierOptions {
cache?: boolean;
maxThreads?: number;
terserOptions?: MinifyOptions;
verbose?: boolean;
Expand Down
14 changes: 9 additions & 5 deletions libraries/module-minifier/src/LocalMinifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ import { minifySingleFileAsync } from './MinifySingleFile';
* @public
*/
export interface ILocalMinifierOptions {
/**
* Indicates whether to cache minification results in memory.
*/
cache?: boolean;
terserOptions?: MinifyOptions;
}

Expand All @@ -30,11 +34,11 @@ export interface ILocalMinifierOptions {
export class LocalMinifier implements IModuleMinifier {
private readonly _terserOptions: MinifyOptions;

private readonly _resultCache: Map<string, IModuleMinificationResult>;
private readonly _resultCache: Map<string, IModuleMinificationResult> | undefined;
private readonly _configHash: string;

public constructor(options: ILocalMinifierOptions) {
const { terserOptions = {} } = options || {};
const { terserOptions = {}, cache = true } = options || {};

this._terserOptions = {
...terserOptions,
Expand All @@ -53,7 +57,7 @@ export class LocalMinifier implements IModuleMinifier {
.update(serialize(terserOptions))
.digest('base64');

this._resultCache = new Map();
this._resultCache = cache ? new Map() : undefined;
}

/**
Expand All @@ -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) => {
Expand Down
16 changes: 11 additions & 5 deletions libraries/module-minifier/src/WorkerPoolMinifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -58,19 +63,20 @@ export class WorkerPoolMinifier implements IModuleMinifier {
private _deduped: number;
private _minified: number;

private readonly _resultCache: Map<string, IModuleMinificationResult>;
private readonly _resultCache: Map<string, IModuleMinificationResult> | undefined;
private readonly _activeRequests: Map<string, IModuleMinificationCallback[]>;

public constructor(options: IWorkerPoolMinifierOptions) {
const {
cache = true,
maxThreads = os.availableParallelism?.() ?? os.cpus().length,
terserOptions = {},
verbose = false,
workerResourceLimits
} = options || {};

const activeRequests: Map<string, IModuleMinificationCallback[]> = new Map();
const resultCache: Map<string, IModuleMinificationResult> = new Map();
const resultCache: Map<string, IModuleMinificationResult> | undefined = cache ? new Map() : undefined;
const terserPool: WorkerPool = new WorkerPool({
id: 'Minifier',
maxWorkers: maxThreads,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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
Expand Down
73 changes: 73 additions & 0 deletions libraries/module-minifier/src/test/LocalMinifier.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -12,7 +13,25 @@ jest.mock('terser/package.json', () => {
};
});

const mockMinifySingleFileAsync: jest.MockedFunction<typeof minifySingleFileAsync> = 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
Expand Down Expand Up @@ -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);
});
});