diff --git a/packages/terser/README.md b/packages/terser/README.md
index e6f3e6dcc..bcc387ed8 100644
--- a/packages/terser/README.md
+++ b/packages/terser/README.md
@@ -9,11 +9,11 @@
# @rollup/plugin-terser
-🍣 A Rollup plugin to generate a minified output bundle.
+🍣 A Rollup plugin to generate a minified bundle with terser.
## Requirements
-This plugin requires an [LTS](https://github.com/nodejs/Release) Node version (v14.0.0+) and Rollup v1.20.0+.
+This plugin requires an [LTS](https://github.com/nodejs/Release) Node version (v14.0.0+) and Rollup v2.0+.
## Install
@@ -27,7 +27,7 @@ npm install @rollup/plugin-terser --save-dev
Create a `rollup.config.js` [configuration file](https://www.rollupjs.org/guide/en/#configuration-files) and import the plugin:
-```js
+```typescript
import terser from '@rollup/plugin-terser';
export default {
@@ -47,13 +47,34 @@ Then call `rollup` either via the [CLI](https://www.rollupjs.org/guide/en/#comma
The plugin accepts a terser [Options](https://github.com/terser/terser#minify-options) object as input parameter,
to modify the default behaviour.
+In addition to the `terser` options, it is also possible to provide the following options:
+
+### `maxWorkers`
+
+Type: `Number`
+Default: `undefined`
+
+Instructs the plugin to use a specific amount of cpu threads.
+
+```typescript
+import terser from '@rollup/plugin-terser';
+
+export default {
+ input: 'src/index.js',
+ output: {
+ dir: 'output',
+ format: 'cjs'
+ },
+ plugins: [
+ terser({
+ maxWorkers: 4
+ })
+ ]
+};
+```
+
## Meta
[CONTRIBUTING](/.github/CONTRIBUTING.md)
[LICENSE (MIT)](/LICENSE)
-
-## Credits
-
-This package was originally developed by [https://github.com/TrySound](TrySound) but is not
-maintained anymore.
diff --git a/packages/terser/package.json b/packages/terser/package.json
index 586c76201..d77e1a6dd 100644
--- a/packages/terser/package.json
+++ b/packages/terser/package.json
@@ -61,9 +61,12 @@
}
},
"dependencies": {
+ "serialize-javascript": "^6.0.0",
+ "smob": "^0.0.6",
"terser": "^5.15.1"
},
"devDependencies": {
+ "@types/serialize-javascript": "^5.0.2",
"rollup": "^3.0.0-7",
"typescript": "^4.8.3"
},
diff --git a/packages/terser/src/index.ts b/packages/terser/src/index.ts
index aca132ae7..76a880f5a 100644
--- a/packages/terser/src/index.ts
+++ b/packages/terser/src/index.ts
@@ -1,25 +1,8 @@
-import type { NormalizedOutputOptions, RenderedChunk } from 'rollup';
-import type { MinifyOptions } from 'terser';
-import { minify } from 'terser';
+import { runWorker } from './worker';
+import terser from './module';
-export default function terser(options?: MinifyOptions) {
- return {
- name: 'terser',
+runWorker();
- async renderChunk(code: string, chunk: RenderedChunk, outputOptions: NormalizedOutputOptions) {
- const defaultOptions: MinifyOptions = {
- sourceMap: outputOptions.sourcemap === true || typeof outputOptions.sourcemap === 'string'
- };
+export * from './type';
- if (outputOptions.format === 'es') {
- defaultOptions.module = true;
- }
-
- if (outputOptions.format === 'cjs') {
- defaultOptions.toplevel = true;
- }
-
- return minify(code, { ...defaultOptions, ...(options || {}) });
- }
- };
-}
+export default terser;
diff --git a/packages/terser/src/module.ts b/packages/terser/src/module.ts
new file mode 100644
index 000000000..c6264c5a0
--- /dev/null
+++ b/packages/terser/src/module.ts
@@ -0,0 +1,72 @@
+import type { NormalizedOutputOptions, RenderedChunk } from 'rollup';
+import { hasOwnProperty, isObject, merge } from 'smob';
+
+import type { Options } from './type';
+import { WorkerPool } from './worker-pool';
+
+export default function terser(options: Options = {}) {
+ const workerPool = new WorkerPool({
+ filePath: __filename,
+ maxWorkers: options.maxWorkers
+ });
+
+ return {
+ name: 'terser',
+
+ async renderChunk(code: string, chunk: RenderedChunk, outputOptions: NormalizedOutputOptions) {
+ const defaultOptions: Options = {
+ sourceMap: outputOptions.sourcemap === true || typeof outputOptions.sourcemap === 'string'
+ };
+
+ if (outputOptions.format === 'es') {
+ defaultOptions.module = true;
+ }
+
+ if (outputOptions.format === 'cjs') {
+ defaultOptions.toplevel = true;
+ }
+
+ try {
+ const { code: result, nameCache } = await workerPool.addAsync({
+ code,
+ options: merge({}, options || {}, defaultOptions)
+ });
+
+ if (options.nameCache && nameCache) {
+ let vars: Record = {
+ props: {}
+ };
+
+ if (hasOwnProperty(options.nameCache, 'vars') && isObject(options.nameCache.vars)) {
+ vars = merge({}, options.nameCache.vars || {}, vars);
+ }
+
+ if (hasOwnProperty(nameCache, 'vars') && isObject(nameCache.vars)) {
+ vars = merge({}, nameCache.vars, vars);
+ }
+
+ // eslint-disable-next-line no-param-reassign
+ options.nameCache.vars = vars;
+
+ let props: Record = {};
+
+ if (hasOwnProperty(options.nameCache, 'props') && isObject(options.nameCache.props)) {
+ // eslint-disable-next-line prefer-destructuring
+ props = options.nameCache.props;
+ }
+
+ if (hasOwnProperty(nameCache, 'props') && isObject(nameCache.props)) {
+ props = merge({}, nameCache.props, props);
+ }
+
+ // eslint-disable-next-line no-param-reassign
+ options.nameCache.props = props;
+ }
+
+ return result;
+ } catch (e) {
+ return Promise.reject(e);
+ }
+ }
+ };
+}
diff --git a/packages/terser/src/type.ts b/packages/terser/src/type.ts
new file mode 100644
index 000000000..5e01b0620
--- /dev/null
+++ b/packages/terser/src/type.ts
@@ -0,0 +1,33 @@
+import type { MinifyOptions } from 'terser';
+
+export interface Options extends MinifyOptions {
+ nameCache?: Record;
+ maxWorkers?: number;
+}
+
+export interface WorkerContext {
+ code: string;
+ options: Options;
+}
+
+export type WorkerCallback = (err: Error | null, output?: WorkerOutput) => void;
+
+export interface WorkerContextSerialized {
+ code: string;
+ options: string;
+}
+
+export interface WorkerOutput {
+ code: string;
+ nameCache?: Options['nameCache'];
+}
+
+export interface WorkerPoolOptions {
+ filePath: string;
+ maxWorkers?: number;
+}
+
+export interface WorkerPoolTask {
+ context: WorkerContext;
+ cb: WorkerCallback;
+}
diff --git a/packages/terser/src/worker-pool.ts b/packages/terser/src/worker-pool.ts
new file mode 100644
index 000000000..5154d4510
--- /dev/null
+++ b/packages/terser/src/worker-pool.ts
@@ -0,0 +1,117 @@
+import { Worker } from 'worker_threads';
+import { cpus } from 'os';
+import { EventEmitter } from 'events';
+
+import serializeJavascript from 'serialize-javascript';
+
+import type {
+ WorkerCallback,
+ WorkerContext,
+ WorkerOutput,
+ WorkerPoolOptions,
+ WorkerPoolTask
+} from './type';
+
+const symbol = Symbol.for('FreeWoker');
+
+export class WorkerPool extends EventEmitter {
+ protected maxInstances: number;
+
+ protected filePath: string;
+
+ protected tasks: WorkerPoolTask[] = [];
+
+ protected workers = 0;
+
+ constructor(options: WorkerPoolOptions) {
+ super();
+
+ this.maxInstances = options.maxWorkers || cpus().length;
+ this.filePath = options.filePath;
+
+ this.on(symbol, () => {
+ if (this.tasks.length > 0) {
+ this.run();
+ }
+ });
+ }
+
+ add(context: WorkerContext, cb: WorkerCallback) {
+ this.tasks.push({
+ context,
+ cb
+ });
+
+ if (this.workers >= this.maxInstances) {
+ return;
+ }
+
+ this.run();
+ }
+
+ async addAsync(context: WorkerContext): Promise {
+ return new Promise((resolve, reject) => {
+ this.add(context, (err, output) => {
+ if (err) {
+ reject(err);
+ return;
+ }
+
+ if (!output) {
+ reject(new Error('The output is empty'));
+ return;
+ }
+
+ resolve(output);
+ });
+ });
+ }
+
+ private run() {
+ if (this.tasks.length === 0) {
+ return;
+ }
+
+ const task = this.tasks.shift();
+
+ if (typeof task === 'undefined') {
+ return;
+ }
+
+ this.workers += 1;
+
+ let called = false;
+ const callCallback = (err: Error | null, output?: WorkerOutput) => {
+ if (called) {
+ return;
+ }
+ called = true;
+
+ this.workers -= 1;
+
+ task.cb(err, output);
+ this.emit(symbol);
+ };
+
+ const worker = new Worker(this.filePath, {
+ workerData: {
+ code: task.context.code,
+ options: serializeJavascript(task.context.options)
+ }
+ });
+
+ worker.on('message', (data) => {
+ callCallback(null, data);
+ });
+
+ worker.on('error', (err) => {
+ callCallback(err);
+ });
+
+ worker.on('exit', (code) => {
+ if (code !== 0) {
+ callCallback(new Error(`Minify worker stopped with exit code ${code}`));
+ }
+ });
+ }
+}
diff --git a/packages/terser/src/worker.ts b/packages/terser/src/worker.ts
new file mode 100644
index 000000000..7b56842c0
--- /dev/null
+++ b/packages/terser/src/worker.ts
@@ -0,0 +1,47 @@
+import process from 'process';
+import { isMainThread, parentPort, workerData } from 'worker_threads';
+
+import { hasOwnProperty, isObject } from 'smob';
+
+import { minify } from 'terser';
+
+import type { WorkerContextSerialized, WorkerOutput } from './type';
+
+/**
+ * Duck typing worker context.
+ *
+ * @param input
+ */
+function isWorkerContextSerialized(input: unknown): input is WorkerContextSerialized {
+ return (
+ isObject(input) &&
+ hasOwnProperty(input, 'code') &&
+ typeof input.code === 'string' &&
+ hasOwnProperty(input, 'options') &&
+ typeof input.options === 'string'
+ );
+}
+
+export async function runWorker() {
+ if (isMainThread || !parentPort || !isWorkerContextSerialized(workerData)) {
+ return;
+ }
+
+ try {
+ // eslint-disable-next-line no-eval
+ const eval2 = eval;
+
+ const options = eval2(`(${workerData.options})`);
+
+ const result = await minify(workerData.code, options);
+
+ const output: WorkerOutput = {
+ code: result.code || workerData.code,
+ nameCache: options.nameCache
+ };
+
+ parentPort.postMessage(output);
+ } catch (e) {
+ process.exit(1);
+ }
+}
diff --git a/packages/terser/test/test.js b/packages/terser/test/test.js
index 812461851..faf3da233 100644
--- a/packages/terser/test/test.js
+++ b/packages/terser/test/test.js
@@ -107,7 +107,7 @@ test.serial('throw error on terser fail', async (t) => {
await bundle.generate({ format: 'esm' });
t.falsy(true);
} catch (error) {
- t.is(error.toString(), 'SyntaxError: Name expected');
+ t.is(error.toString(), 'Error: Minify worker stopped with exit code 1');
}
});
@@ -127,7 +127,7 @@ test.serial('throw error on terser fail with multiple outputs', async (t) => {
await Promise.all([bundle.generate({ format: 'cjs' }), bundle.generate({ format: 'esm' })]);
t.falsy(true);
} catch (error) {
- t.is(error.toString(), 'SyntaxError: Name expected');
+ t.is(error.toString(), 'Error: Minify worker stopped with exit code 1');
}
});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 32b89b195..ba5729584 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -498,12 +498,18 @@ importers:
packages/terser:
specifiers:
+ '@types/serialize-javascript': ^5.0.2
rollup: ^3.0.0-7
+ serialize-javascript: ^6.0.0
+ smob: ^0.0.6
terser: ^5.15.1
typescript: ^4.8.3
dependencies:
+ serialize-javascript: 6.0.0
+ smob: 0.0.6
terser: 5.15.1
devDependencies:
+ '@types/serialize-javascript': 5.0.2
rollup: 3.0.0-7
typescript: 4.8.4
@@ -2368,6 +2374,10 @@ packages:
resolution: {integrity: sha512-WwA1MW0++RfXmCr12xeYOOC5baSC9mSb0ZqCquFzKhcoF4TvHu5MKOuXsncgZcpVFhB1pXd5hZmM0ryAoCp12A==}
dev: true
+ /@types/serialize-javascript/5.0.2:
+ resolution: {integrity: sha512-BRLlwZzRoZukGaBtcUxkLsZsQfWZpvog6MZk3PWQO9Q6pXmXFzjU5iGzZ+943evp6tkkbN98N1Z31KT0UG1yRw==}
+ dev: true
+
/@types/source-map-support/0.5.6:
resolution: {integrity: sha512-b2nJ9YyXmkhGaa2b8VLM0kJ04xxwNyijcq12/kDoomCt43qbHBeK2SLNJ9iJmETaAj+bKUT05PQUu3Q66GvLhQ==}
dependencies:
@@ -6389,6 +6399,12 @@ packages:
engines: {node: '>=10'}
dev: true
+ /randombytes/2.1.0:
+ resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
+ dependencies:
+ safe-buffer: 5.2.1
+ dev: false
+
/read-pkg-up/7.0.1:
resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==}
engines: {node: '>=8'}
@@ -6689,7 +6705,6 @@ packages:
/safe-buffer/5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
- dev: true
/safe-identifier/0.4.2:
resolution: {integrity: sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==}
@@ -6735,6 +6750,12 @@ packages:
type-fest: 0.13.1
dev: true
+ /serialize-javascript/6.0.0:
+ resolution: {integrity: sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==}
+ dependencies:
+ randombytes: 2.1.0
+ dev: false
+
/set-blocking/2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
dev: true
@@ -6825,6 +6846,10 @@ packages:
is-fullwidth-code-point: 4.0.0
dev: true
+ /smob/0.0.6:
+ resolution: {integrity: sha512-V21+XeNni+tTyiST1MHsa84AQhT1aFZipzPpOFAVB8DkHzwJyjjAmt9bgwnuZiZWnIbMo2duE29wybxv/7HWUw==}
+ dev: false
+
/sort-keys/2.0.0:
resolution: {integrity: sha512-/dPCrG1s3ePpWm6yBbxZq5Be1dXGLyLn9Z791chDC3NFrpkVbWGzkBwPN1knaciexFXgRJ7hzdnwZ4stHSDmjg==}
engines: {node: '>=4'}