From 204c3db0ef468f842f5a648971cfeb3fbb5b0911 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Tue, 16 Dec 2025 17:13:30 +0000 Subject: [PATCH 1/2] feat(babel): improve filtering capability & performance - drops support for rollup 1.x, which allows simplifying the implementation - implements 'plugin hook filters' for improved performance under rolldown - passes a new `code` argument to custom filter functions, so that consumers can add more advanced filtering logic - allows custom filter functions to be async --- packages/babel/README.md | 2 +- packages/babel/package.json | 2 +- packages/babel/src/index.js | 128 ++++++++++++------------ packages/babel/test/as-input-plugin.mjs | 36 +++++++ packages/babel/types/index.d.ts | 4 +- 5 files changed, 103 insertions(+), 69 deletions(-) diff --git a/packages/babel/README.md b/packages/babel/README.md index 3b573fbec..5c004e70a 100644 --- a/packages/babel/README.md +++ b/packages/babel/README.md @@ -94,7 +94,7 @@ A [picomatch pattern](https://github.com/micromatch/picomatch), or array of patt ### `filter` -Type: (id: string) => boolean
+Type: (id: string, code: string) => boolean
Custom [filter function](https://github.com/rollup/plugins/tree/master/packages/pluginutils#createfilter) can be used to determine whether or not certain modules should be operated upon. diff --git a/packages/babel/package.json b/packages/babel/package.json index 45bdda3b7..aeda64e99 100644 --- a/packages/babel/package.json +++ b/packages/babel/package.json @@ -55,7 +55,7 @@ "peerDependencies": { "@babel/core": "^7.0.0", "@types/babel__core": "^7.1.9", - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + "rollup": "^2.0.0||^3.0.0||^4.0.0" }, "peerDependenciesMeta": { "rollup": { diff --git a/packages/babel/src/index.js b/packages/babel/src/index.js index 125e6305a..322f8ada0 100644 --- a/packages/babel/src/index.js +++ b/packages/babel/src/index.js @@ -5,7 +5,7 @@ import { BUNDLED, HELPERS } from './constants.js'; import bundledHelpersPlugin from './bundledHelpersPlugin.js'; import preflightCheck from './preflightCheck.js'; import transformCode from './transformCode.js'; -import { addBabelPlugin, escapeRegExpCharacters, warnOnce, stripQuery } from './utils.js'; +import { addBabelPlugin, escapeRegExpCharacters, warnOnce } from './utils.js'; const unpackOptions = ({ extensions = babel.DEFAULT_EXTENSIONS, @@ -37,7 +37,7 @@ const warnAboutDeprecatedHelpersOption = ({ deprecatedOption, suggestion }) => { ); }; -const unpackInputPluginOptions = ({ skipPreflightCheck = false, ...rest }, rollupVersion) => { +const unpackInputPluginOptions = ({ skipPreflightCheck = false, ...rest }) => { if ('runtimeHelpers' in rest) { warnAboutDeprecatedHelpersOption({ deprecatedOption: 'runtimeHelpers', @@ -63,8 +63,7 @@ const unpackInputPluginOptions = ({ skipPreflightCheck = false, ...rest }, rollu supportsStaticESM: true, supportsDynamicImport: true, supportsTopLevelAwait: true, - // todo: remove version checks for 1.20 - 1.25 when we bump peer deps - supportsExportNamespaceFrom: !rollupVersion.match(/^1\.2[0-5]\./), + supportsExportNamespaceFrom: true, ...rest.caller } }); @@ -110,77 +109,76 @@ function createBabelInputPluginFactory(customCallback = returnObject) { overrides ); - let babelHelpers; - let babelOptions; - let filter; - let skipPreflightCheck; - return { - name: 'babel', - - options() { - // todo: remove options hook and hoist declarations when version checks are removed - let exclude; - let include; - let extensions; - let customFilter; - - ({ - exclude, - extensions, - babelHelpers, - include, - filter: customFilter, - skipPreflightCheck, - ...babelOptions - } = unpackInputPluginOptions(pluginOptionsWithOverrides, this.meta.rollupVersion)); + const { + exclude, + extensions, + babelHelpers, + include, + filter: customFilter, + skipPreflightCheck, + ...babelOptions + } = unpackInputPluginOptions(pluginOptionsWithOverrides); + + const extensionRegExp = new RegExp( + `(${extensions.map(escapeRegExpCharacters).join('|')})(\\?.*)?(#.*)?$` + ); + if (customFilter && (include || exclude)) { + throw new Error('Could not handle include or exclude with custom filter together'); + } + const userDefinedFilter = + typeof customFilter === 'function' ? customFilter : createFilter(include, exclude); + const filter = (id, code) => extensionRegExp.test(id) && userDefinedFilter(id, code); - const extensionRegExp = new RegExp( - `(${extensions.map(escapeRegExpCharacters).join('|')})$` - ); - if (customFilter && (include || exclude)) { - throw new Error('Could not handle include or exclude with custom filter together'); - } - const userDefinedFilter = - typeof customFilter === 'function' ? customFilter : createFilter(include, exclude); - filter = (id) => extensionRegExp.test(stripQuery(id).bareId) && userDefinedFilter(id); + const helpersFilter = { id: new RegExp(`^${escapeRegExpCharacters(HELPERS)}$`) }; - return null; - }, + return { + name: 'babel', - resolveId(id) { - if (id !== HELPERS) { - return null; + resolveId: { + filter: helpersFilter, + handler(id) { + if (id !== HELPERS) { + return null; + } + return id; } - return id; }, - load(id) { - if (id !== HELPERS) { - return null; + load: { + filter: helpersFilter, + handler(id) { + if (id !== HELPERS) { + return null; + } + return babel.buildExternalHelpers(null, 'module'); } - return babel.buildExternalHelpers(null, 'module'); }, - transform(code, filename) { - if (!filter(filename)) return null; - if (filename === HELPERS) return null; - - return transformCode( - code, - { ...babelOptions, filename }, - overrides, - customOptions, - this, - async (transformOptions) => { - if (!skipPreflightCheck) { - await preflightCheck(this, babelHelpers, transformOptions); + transform: { + filter: { + id: extensionRegExp + }, + async handler(code, filename) { + if (!(await filter(filename, code))) return null; + if (filename === HELPERS) return null; + + return transformCode( + code, + { ...babelOptions, filename }, + overrides, + customOptions, + this, + async (transformOptions) => { + if (!skipPreflightCheck) { + await preflightCheck(this, babelHelpers, transformOptions); + } + + return babelHelpers === BUNDLED + ? addBabelPlugin(transformOptions, bundledHelpersPlugin) + : transformOptions; } - - return babelHelpers === BUNDLED - ? addBabelPlugin(transformOptions, bundledHelpersPlugin) - : transformOptions; - } - ); + ); + } } }; }; diff --git a/packages/babel/test/as-input-plugin.mjs b/packages/babel/test/as-input-plugin.mjs index c7d145558..4b8d9495f 100644 --- a/packages/babel/test/as-input-plugin.mjs +++ b/packages/babel/test/as-input-plugin.mjs @@ -122,6 +122,23 @@ console.log("the answer is ".concat(foo())); ); }); +test('does not babelify excluded code with code-based filter', async (t) => { + const filter = (id, code) => code.includes('the answer is'); + const code = await generate('exclusions/main.js', { filter }); + // eslint-disable-next-line no-template-curly-in-string + t.false(code.includes('${foo()}')); + t.true(code.includes('=> 42')); + t.is( + code, + `'use strict'; + +const foo = () => 42; + +console.log("the answer is ".concat(foo())); +` + ); +}); + test('does babelify included code with custom filter', async (t) => { const filter = createFilter('**/foo.js', [], { resolve: DIRNAME @@ -143,6 +160,25 @@ console.log(\`the answer is \${foo()}\`); ); }); +test('does babelify excluded code with code-based filter', async (t) => { + const filter = (id, code) => !code.includes('the answer is'); + const code = await generate('exclusions/main.js', { filter }); + // eslint-disable-next-line no-template-curly-in-string + t.true(code.includes('${foo()}')); + t.false(code.includes('=> 42')); + t.is( + code, + `'use strict'; + +var foo = function foo() { + return 42; +}; + +console.log(\`the answer is \${foo()}\`); +` + ); +}); + test('can not pass include or exclude when custom filter specified', async (t) => { const filter = createFilter('**/foo.js', [], { resolve: DIRNAME diff --git a/packages/babel/types/index.d.ts b/packages/babel/types/index.d.ts index 20288d8a0..76cf6af8a 100644 --- a/packages/babel/types/index.d.ts +++ b/packages/babel/types/index.d.ts @@ -1,5 +1,5 @@ import type { Plugin, PluginContext, TransformPluginContext } from 'rollup'; -import type { FilterPattern, CreateFilter } from '@rollup/pluginutils'; +import type { FilterPattern } from '@rollup/pluginutils'; import type * as babelCore from '@babel/core'; export interface RollupBabelInputPluginOptions @@ -23,7 +23,7 @@ export interface RollupBabelInputPluginOptions * const filter = createFilter(include, exclude, {}); * @default undefined; */ - filter?: ReturnType; + filter?: (id: string, code: string) => Promise; /** * An array of file extensions that Babel should transpile. If you want to transpile TypeScript files with this plugin it's essential to include .ts and .tsx in this option. * @default ['.js', '.jsx', '.es6', '.es', '.mjs'] From 1a508bf83e656e7d2077906d7eb70d8560bbf014 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Mon, 12 Jan 2026 18:09:33 +0000 Subject: [PATCH 2/2] feat(babel): add parallel processing via worker threads Add a `parallel` option that processes files concurrently using Node.js worker threads. This reduces build times for large projects where Babel transformation is a bottleneck. This is similar to the existing parallel behavior of `@rollup/plugin-terser`. This required some fairly significant refactoring, because we can only pass serializable objects between the main thread and the worker threads. It also required changes to the plugin's own build config, so that we can generate a dedicated worker entrypoint. Validations are added to ensure that unserializable config (e.g. inline babel plugins) cannot be used alongside the new parallel mode. For people using dedicated babel config files, this isn't a problem, because they are loaded directly by babel in the worker thread itself. The worker threads do have a setup cost, so this only makes sense for large projects. In Discourse, enabling this parallel mode cuts our overall vite (rolldown) build time by about 45% (from ~11s to ~6s) on my machine. --- packages/babel/README.md | 9 ++ packages/babel/rollup.config.mjs | 27 +++++- packages/babel/src/index.js | 101 ++++++++++++++++---- packages/babel/src/preflightCheck.js | 14 +-- packages/babel/src/transformCode.js | 31 ++++-- packages/babel/src/worker.js | 25 +++++ packages/babel/src/workerPool.js | 119 ++++++++++++++++++++++++ packages/babel/test/as-input-plugin.mjs | 65 +++++++++++++ packages/babel/types/index.d.ts | 8 ++ 9 files changed, 361 insertions(+), 38 deletions(-) create mode 100644 packages/babel/src/worker.js create mode 100644 packages/babel/src/workerPool.js diff --git a/packages/babel/README.md b/packages/babel/README.md index 5c004e70a..33d2a3bdf 100644 --- a/packages/babel/README.md +++ b/packages/babel/README.md @@ -135,6 +135,15 @@ Default: `false` Before transpiling your input files this plugin also transpile a short piece of code **for each** input file. This is used to validate some misconfiguration errors, but for sufficiently big projects it can slow your build times so if you are confident about your configuration then you might disable those checks with this option. +### `parallel` + +Type: `Boolean | number` +Default: `false` + +Enable parallel processing of files in worker threads. This has a setup cost, so is best suited for larger projects. Pass an integer to set the number of workers. Set `true` for the default number of workers (4). + +This option cannot be used alongside custom overrides or non-serializable Babel options. + ### External dependencies Ideally, you should only be transforming your source code, rather than running all of your external dependencies through Babel (to ignore external dependencies from being handled by this plugin you might use `exclude: 'node_modules/**'` option). If you have a dependency that exposes untranspiled ES6 source code that doesn't run in your target environment, then you may need to break this rule, but it often causes problems with unusual `.babelrc` files or mismatched versions of Babel. diff --git a/packages/babel/rollup.config.mjs b/packages/babel/rollup.config.mjs index 686d97f22..4124e6fc1 100644 --- a/packages/babel/rollup.config.mjs +++ b/packages/babel/rollup.config.mjs @@ -1,6 +1,6 @@ import { readFileSync } from 'fs'; -import { createConfig } from '../../shared/rollup.config.mjs'; +import { createConfig, emitModulePackageFile } from '../../shared/rollup.config.mjs'; import { babel } from './src/index.js'; @@ -8,7 +8,30 @@ const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), export default { ...createConfig({ pkg }), - input: './src/index.js', + input: { + index: './src/index.js', + worker: './src/worker.js' + }, + output: [ + { + format: 'cjs', + dir: 'dist/cjs', + exports: 'named', + footer(chunkInfo) { + if (chunkInfo.name === 'index') { + return 'module.exports = Object.assign(exports.default, exports);'; + } + return null; + }, + sourcemap: true + }, + { + format: 'es', + dir: 'dist/es', + plugins: [emitModulePackageFile()], + sourcemap: true + } + ], plugins: [ babel({ presets: [['@babel/preset-env', { targets: { node: 14 } }]], diff --git a/packages/babel/src/index.js b/packages/babel/src/index.js index 322f8ada0..58d0cb38b 100644 --- a/packages/babel/src/index.js +++ b/packages/babel/src/index.js @@ -2,10 +2,9 @@ import * as babel from '@babel/core'; import { createFilter } from '@rollup/pluginutils'; import { BUNDLED, HELPERS } from './constants.js'; -import bundledHelpersPlugin from './bundledHelpersPlugin.js'; -import preflightCheck from './preflightCheck.js'; import transformCode from './transformCode.js'; -import { addBabelPlugin, escapeRegExpCharacters, warnOnce } from './utils.js'; +import { escapeRegExpCharacters, warnOnce } from './utils.js'; +import WorkerPool from './workerPool.js'; const unpackOptions = ({ extensions = babel.DEFAULT_EXTENSIONS, @@ -100,6 +99,24 @@ const returnObject = () => { return {}; }; +function isSerializable(value) { + if (value === null) { + return true; + } else if (Array.isArray(value)) { + return value.every(isSerializable); + } + switch (typeof value) { + case 'string': + case 'number': + case 'boolean': + return true; + case 'object': + return Object.keys(value).every((key) => isSerializable(value[key])); + default: + return false; + } +} + function createBabelInputPluginFactory(customCallback = returnObject) { const overrides = customCallback(babel); @@ -109,6 +126,8 @@ function createBabelInputPluginFactory(customCallback = returnObject) { overrides ); + let workerPool; + const { exclude, extensions, @@ -116,6 +135,7 @@ function createBabelInputPluginFactory(customCallback = returnObject) { include, filter: customFilter, skipPreflightCheck, + parallel, ...babelOptions } = unpackInputPluginOptions(pluginOptionsWithOverrides); @@ -129,6 +149,23 @@ function createBabelInputPluginFactory(customCallback = returnObject) { typeof customFilter === 'function' ? customFilter : createFilter(include, exclude); const filter = (id, code) => extensionRegExp.test(id) && userDefinedFilter(id, code); + if (parallel) { + const parallelAllowed = + isSerializable(babelOptions) && !overrides?.config && !overrides?.result; + + if (!parallelAllowed) { + throw new Error( + 'Cannot use "parallel" mode alongside custom overrides or non-serializable Babel options.' + ); + } + + const parallelWorkerCount = typeof parallel === 'number' ? parallel : 4; + workerPool = new WorkerPool( + new URL('./worker.js', import.meta.url).pathname, + parallelWorkerCount + ); + } + const helpersFilter = { id: new RegExp(`^${escapeRegExpCharacters(HELPERS)}$`) }; return { @@ -162,22 +199,39 @@ function createBabelInputPluginFactory(customCallback = returnObject) { if (!(await filter(filename, code))) return null; if (filename === HELPERS) return null; - return transformCode( - code, - { ...babelOptions, filename }, - overrides, + if (parallel) { + return workerPool.runTask({ + inputCode: code, + babelOptions: { ...babelOptions, filename }, + runPreflightCheck: !skipPreflightCheck, + babelHelpers + }); + } + + return transformCode({ + inputCode: code, + babelOptions: { ...babelOptions, filename }, + overrides: { + config: overrides.config?.bind(this), + result: overrides.result?.bind(this) + }, customOptions, - this, - async (transformOptions) => { - if (!skipPreflightCheck) { - await preflightCheck(this, babelHelpers, transformOptions); - } - - return babelHelpers === BUNDLED - ? addBabelPlugin(transformOptions, bundledHelpersPlugin) - : transformOptions; - } - ); + error: this.error.bind(this), + runPreflightCheck: !skipPreflightCheck, + babelHelpers + }); + } + }, + + async closeBundle() { + if (parallel && !this.meta.watchMode) { + await workerPool.terminate(); + } + }, + + async closeWatcher() { + if (parallel) { + await workerPool.terminate(); } } }; @@ -257,7 +311,16 @@ function createBabelOutputPluginFactory(customCallback = returnObject) { } } - return transformCode(code, babelOptions, overrides, customOptions, this); + return transformCode({ + inputCode: code, + babelOptions, + overrides: { + config: overrides.config?.bind(this), + result: overrides.result?.bind(this) + }, + customOptions, + error: this.error.bind(this) + }); } }; }; diff --git a/packages/babel/src/preflightCheck.js b/packages/babel/src/preflightCheck.js index 1ca8c1c1c..ae1e43407 100644 --- a/packages/babel/src/preflightCheck.js +++ b/packages/babel/src/preflightCheck.js @@ -37,27 +37,27 @@ const mismatchError = (actual, expected, filename) => // Revert to /\/helpers\/(esm\/)?inherits/ when Babel 8 gets released, this was fixed in https://github.com/babel/babel/issues/14185 const inheritsHelperRe = /[\\/]+helpers[\\/]+(esm[\\/]+)?inherits/; -export default async function preflightCheck(ctx, babelHelpers, transformOptions) { +export default async function preflightCheck(error, babelHelpers, transformOptions) { const finalOptions = addBabelPlugin(transformOptions, helpersTestTransform); const check = (await babel.transformAsync(PREFLIGHT_INPUT, finalOptions)).code; // Babel sometimes splits ExportDefaultDeclaration into 2 statements, so we also check for ExportNamedDeclaration if (!/export (d|{)/.test(check)) { - ctx.error(MODULE_ERROR); + error(MODULE_ERROR); } if (inheritsHelperRe.test(check)) { if (babelHelpers === RUNTIME) { return; } - ctx.error(mismatchError(RUNTIME, babelHelpers, transformOptions.filename)); + error(mismatchError(RUNTIME, babelHelpers, transformOptions.filename)); } if (check.includes('babelHelpers.inherits')) { if (babelHelpers === EXTERNAL) { return; } - ctx.error(mismatchError(EXTERNAL, babelHelpers, transformOptions.filename)); + error(mismatchError(EXTERNAL, babelHelpers, transformOptions.filename)); } // test unminifiable string content @@ -66,12 +66,12 @@ export default async function preflightCheck(ctx, babelHelpers, transformOptions return; } if (babelHelpers === RUNTIME && !transformOptions.plugins.length) { - ctx.error( + error( `You must use the \`@babel/plugin-transform-runtime\` plugin when \`babelHelpers\` is "${RUNTIME}".\n` ); } - ctx.error(mismatchError(INLINE, babelHelpers, transformOptions.filename)); + error(mismatchError(INLINE, babelHelpers, transformOptions.filename)); } - ctx.error(UNEXPECTED_ERROR); + error(UNEXPECTED_ERROR); } diff --git a/packages/babel/src/transformCode.js b/packages/babel/src/transformCode.js index f677ea09c..b75feb03a 100644 --- a/packages/babel/src/transformCode.js +++ b/packages/babel/src/transformCode.js @@ -1,13 +1,19 @@ import * as babel from '@babel/core'; -export default async function transformCode( +import bundledHelpersPlugin from './bundledHelpersPlugin.js'; +import preflightCheck from './preflightCheck.js'; +import { BUNDLED } from './constants.js'; +import { addBabelPlugin } from './utils.js'; + +export default async function transformCode({ inputCode, babelOptions, overrides, customOptions, - ctx, - finalizeOptions -) { + error, + runPreflightCheck, + babelHelpers +}) { // loadPartialConfigAsync has become available in @babel/core@7.8.0 const config = await (babel.loadPartialConfigAsync || babel.loadPartialConfig)(babelOptions); @@ -16,18 +22,23 @@ export default async function transformCode( return null; } - let transformOptions = !overrides.config + let transformOptions = !overrides?.config ? config.options - : await overrides.config.call(ctx, config, { + : await overrides.config(config, { code: inputCode, customOptions }); - if (finalizeOptions) { - transformOptions = await finalizeOptions(transformOptions); + if (runPreflightCheck) { + await preflightCheck(error, babelHelpers, transformOptions); } - if (!overrides.result) { + transformOptions = + babelHelpers === BUNDLED + ? addBabelPlugin(transformOptions, bundledHelpersPlugin) + : transformOptions; + + if (!overrides?.result) { const { code, map } = await babel.transformAsync(inputCode, transformOptions); return { code, @@ -36,7 +47,7 @@ export default async function transformCode( } const result = await babel.transformAsync(inputCode, transformOptions); - const { code, map } = await overrides.result.call(ctx, result, { + const { code, map } = await overrides.result(result, { code: inputCode, customOptions, config, diff --git a/packages/babel/src/worker.js b/packages/babel/src/worker.js new file mode 100644 index 000000000..c1dd78d23 --- /dev/null +++ b/packages/babel/src/worker.js @@ -0,0 +1,25 @@ +import { parentPort } from 'worker_threads'; + +import transformCode from './transformCode.js'; + +parentPort.on('message', async (opts) => { + try { + const result = await transformCode({ + ...opts, + error: (msg) => { + throw new Error(msg); + } + }); + parentPort.postMessage({ + result + }); + } catch (error) { + parentPort.postMessage({ + error: { + message: error.message, + stack: error.stack, + name: error.name + } + }); + } +}); diff --git a/packages/babel/src/workerPool.js b/packages/babel/src/workerPool.js new file mode 100644 index 000000000..5b95ce4a2 --- /dev/null +++ b/packages/babel/src/workerPool.js @@ -0,0 +1,119 @@ +import { Worker } from 'worker_threads'; + +export default class WorkerPool { + workers = []; + availableWorkers = []; + + pendingTasks = []; + runningTasks = new Map(); + + constructor(workerScript, poolSize) { + this.workerScript = workerScript; + this.poolSize = poolSize; + } + + createWorker() { + const worker = new Worker(this.workerScript); + + worker.on('message', (message) => { + const { result, error } = message; + const runningTask = this.runningTasks.get(worker); + + if (runningTask) { + this.runningTasks.delete(worker); + + if (error) { + const err = new Error(error.message); + err.name = error.name; + err.stack = error.stack; + runningTask.reject(err); + } else { + runningTask.resolve(result); + } + + this.availableWorkers.push(worker); + this.processQueue(); + } + }); + + worker.on('error', (error) => { + // Unexpected worker error, remove from pool + + const workerIdx = this.workers.indexOf(worker); + if (workerIdx === -1) return; + this.workers.splice(workerIdx, 1); + + const availableIdx = this.availableWorkers.indexOf(worker); + if (availableIdx !== -1) { + this.availableWorkers.splice(availableIdx, 1); + } + + const runningTask = this.runningTasks.get(worker); + if (runningTask) { + this.runningTasks.delete(worker); + runningTask.reject(error); + } + + this.processQueue(); + }); + + this.workers.push(worker); + return worker; + } + + getAvailableWorker() { + if (this.availableWorkers.length > 0) { + return this.availableWorkers.shift(); + } + if (this.workers.length < this.poolSize) { + return this.createWorker(); + } + return null; + } + + processQueue() { + while (this.pendingTasks.length > 0) { + const worker = this.getAvailableWorker(); + if (!worker) break; + + const task = this.pendingTasks.shift(); + + this.runningTasks.set(worker, task); + + worker.postMessage(task.opts); + } + } + + async runTask(opts) { + const taskPromise = new Promise((resolve, reject) => { + this.pendingTasks.push({ + resolve, + reject, + opts + }); + }); + + this.processQueue(); + + return taskPromise; + } + + async terminate() { + for (const [, { reject }] of this.runningTasks.entries()) { + reject(new Error('Worker pool is terminating')); + } + this.runningTasks.clear(); + this.pendingTasks.length = 0; + + const terminatePromises = this.workers.map((worker) => + worker.terminate().catch((err) => { + console.error('Error terminating worker:', err); + }) + ); + + await Promise.all(terminatePromises); + + this.workers.length = 0; + this.availableWorkers.length = 0; + } +} diff --git a/packages/babel/test/as-input-plugin.mjs b/packages/babel/test/as-input-plugin.mjs index 4b8d9495f..0ca8b71a8 100644 --- a/packages/babel/test/as-input-plugin.mjs +++ b/packages/babel/test/as-input-plugin.mjs @@ -600,3 +600,68 @@ test('works as a CJS plugin', async (t) => { t.false(code.includes('const')); }); + +test('works in parallel', async (t) => { + const bundle = await rollup({ + input: `${FIXTURES}proposal-decorators/main.js`, + plugins: [babelPlugin({ parallel: true })] + }); + const code = await getCode(bundle); + + t.true(code.includes('_createClass'), 'decorator was applied'); +}); + +test('works in parallel with specified worker count', async (t) => { + const code = await generate('basic/main.js', { parallel: 2 }); + t.false(code.includes('const')); + t.true(code.includes('var answer = 42')); +}); + +test('throws when using parallel with non-serializable babel options', async (t) => { + await t.throwsAsync( + () => + generate('basic/main.js', { + parallel: true, + plugins: [ + // Functions are not serializable + function customPlugin() { + return { visitor: {} }; + } + ] + }), + { + message: + /Cannot use "parallel" mode alongside custom overrides or non-serializable Babel options/ + } + ); +}); + +test('throws when using parallel with config override', (t) => { + const customBabelPlugin = createBabelInputPluginFactory(() => { + return { + config(cfg) { + return cfg.options; + } + }; + }); + + t.throws(() => customBabelPlugin({ babelHelpers: 'bundled', parallel: true }), { + message: + /Cannot use "parallel" mode alongside custom overrides or non-serializable Babel options/ + }); +}); + +test('throws when using parallel with result override', (t) => { + const customBabelPlugin = createBabelInputPluginFactory(() => { + return { + result(result) { + return result; + } + }; + }); + + t.throws(() => customBabelPlugin({ babelHelpers: 'bundled', parallel: true }), { + message: + /Cannot use "parallel" mode alongside custom overrides or non-serializable Babel options/ + }); +}); diff --git a/packages/babel/types/index.d.ts b/packages/babel/types/index.d.ts index 76cf6af8a..7880f6d7a 100644 --- a/packages/babel/types/index.d.ts +++ b/packages/babel/types/index.d.ts @@ -39,6 +39,14 @@ export interface RollupBabelInputPluginOptions * @default false */ skipPreflightCheck?: boolean; + /** + * Enable parallel processing of files in worker threads. This has a setup cost, so is best suited for larger projects. + * Pass an integer to set the number of workers. Set `true` for the default number of workers (4). + * + * This option cannot be used alongside custom overrides or non-serializable Babel options. + * @default false + */ + parallel?: boolean | number; } export interface RollupBabelOutputPluginOptions