diff --git a/packages/commonjs/CHANGELOG.md b/packages/commonjs/CHANGELOG.md index 0f0981bd3..99c7632e4 100644 --- a/packages/commonjs/CHANGELOG.md +++ b/packages/commonjs/CHANGELOG.md @@ -1,5 +1,13 @@ # @rollup/plugin-commonjs ChangeLog +## v23.0.4 + +_2022-12-07_ + +### Bugfixes + +- fix: declaration tag @default for ignoreTryCatch + fix some typos [#1370](https://github.com/rollup/plugins/pull/1370) + ## v23.0.3 _2022-11-27_ diff --git a/packages/commonjs/package.json b/packages/commonjs/package.json index 212bbadfa..05efb2ae1 100644 --- a/packages/commonjs/package.json +++ b/packages/commonjs/package.json @@ -1,6 +1,6 @@ { "name": "@rollup/plugin-commonjs", - "version": "23.0.3", + "version": "23.0.4", "publishConfig": { "access": "public" }, diff --git a/packages/commonjs/types/index.d.ts b/packages/commonjs/types/index.d.ts index 0b79f9ebf..37453bd92 100644 --- a/packages/commonjs/types/index.d.ts +++ b/packages/commonjs/types/index.d.ts @@ -109,17 +109,17 @@ interface RollupCommonJSOptions { * they should be left unconverted as it requires an optional dependency * that may or may not be installed beside the rolled up package. * Due to the conversion of `require` to a static `import` - the call is - * hoisted to the top of the file, outside of the `try-catch` clause. + * hoisted to the top of the file, outside the `try-catch` clause. * - * - `true`: All `require` calls inside a `try` will be left unconverted. + * - `true`: Default. All `require` calls inside a `try` will be left unconverted. * - `false`: All `require` calls inside a `try` will be converted as if the * `try-catch` clause is not there. * - `remove`: Remove all `require` calls from inside any `try` block. * - `string[]`: Pass an array containing the IDs to left unconverted. - * - `((id: string) => boolean|'remove')`: Pass a function that control + * - `((id: string) => boolean|'remove')`: Pass a function that controls * individual IDs. * - * @default false + * @default true */ ignoreTryCatch?: | boolean @@ -133,14 +133,14 @@ interface RollupCommonJSOptions { * NodeJS where ES modules can only import a default export from a CommonJS * dependency. * - * If you set `esmExternals` to `true`, this plugins assumes that all + * If you set `esmExternals` to `true`, this plugin assumes that all * external dependencies are ES modules and respect the * `requireReturnsDefault` option. If that option is not set, they will be * rendered as namespace imports. * * You can also supply an array of ids to be treated as ES modules, or a - * function that will be passed each external id to determine if it is an ES - * module. + * function that will be passed each external id to determine whether it is + * an ES module. * @default false */ esmExternals?: boolean | ReadonlyArray | ((id: string) => boolean); @@ -158,7 +158,7 @@ interface RollupCommonJSOptions { * import * as foo from 'foo'; * ``` * - * However there are some situations where this may not be desired. + * However, there are some situations where this may not be desired. * For these situations, you can change Rollup's behaviour either globally or * per module. To change it globally, set the `requireReturnsDefault` option * to one of the following values: @@ -208,7 +208,7 @@ interface RollupCommonJSOptions { * Some modules contain dynamic `require` calls, or require modules that * contain circular dependencies, which are not handled well by static * imports. Including those modules as `dynamicRequireTargets` will simulate a - * CommonJS (NodeJS-like) environment for them with support for dynamic + * CommonJS (NodeJS-like) environment for them with support for dynamic * dependencies. It also enables `strictRequires` for those modules. * * Note: In extreme cases, this feature may result in some paths being @@ -222,7 +222,7 @@ interface RollupCommonJSOptions { * To avoid long paths when using the `dynamicRequireTargets` option, you can use this option to specify a directory * that is a common parent for all files that use dynamic require statements. Using a directory higher up such as `/` * may lead to unnecessarily long paths in the generated code and may expose directory names on your machine like your - * home directory name. By default it uses the current working directory. + * home directory name. By default, it uses the current working directory. */ dynamicRequireRoot?: string; } diff --git a/packages/terser/CHANGELOG.md b/packages/terser/CHANGELOG.md index 8811529a8..d2ed5077f 100644 --- a/packages/terser/CHANGELOG.md +++ b/packages/terser/CHANGELOG.md @@ -1,5 +1,13 @@ # @rollup/plugin-terser ChangeLog +## v0.2.0 + +_2022-12-05_ + +### Features + +- feat: parallel execution [#1341](https://github.com/rollup/plugins/pull/1341) + ## v0.1.0 _2022-10-27_ 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..f55046b0c 100644 --- a/packages/terser/package.json +++ b/packages/terser/package.json @@ -1,6 +1,6 @@ { "name": "@rollup/plugin-terser", - "version": "0.1.0", + "version": "0.2.0", "publishConfig": { "access": "public" }, @@ -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'}