Skip to content

Commit 7108c19

Browse files
authored
Merge branch 'master' into bump-picomatch
2 parents 729f6e2 + 0abd95b commit 7108c19

12 files changed

Lines changed: 409 additions & 40 deletions

packages/babel/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# @rollup/plugin-babel ChangeLog
22

3+
## v7.1.0
4+
5+
_2026-05-29_
6+
7+
### Features
8+
9+
- babel: add parallel processing via worker threads ([#1956](https://github.com/rollup/plugins/issues/1956))
10+
311
## v7.0.0
412

513
_2026-03-05_

packages/babel/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,17 @@ Default: `false`
135135

136136
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.
137137

138+
### `parallel`
139+
140+
Type: `Boolean | number`<br>
141+
Default: `false`
142+
143+
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 (based on CPU cores, capped at 4).
144+
145+
This option is available for both the input plugin (`babel()`) and the output plugin (`getBabelOutputPlugin()`).
146+
147+
This option cannot be used alongside custom overrides or non-serializable Babel options.
148+
138149
### External dependencies
139150

140151
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.

packages/babel/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@rollup/plugin-babel",
3-
"version": "7.0.0",
3+
"version": "7.1.0",
44
"publishConfig": {
55
"access": "public"
66
},
@@ -67,7 +67,8 @@
6767
},
6868
"dependencies": {
6969
"@babel/helper-module-imports": "^7.18.6",
70-
"@rollup/pluginutils": "^5.0.1"
70+
"@rollup/pluginutils": "^5.0.1",
71+
"workerpool": "^9.0.0"
7172
},
7273
"devDependencies": {
7374
"@babel/core": "^7.19.1",

packages/babel/rollup.config.mjs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,37 @@
11
import { readFileSync } from 'fs';
22

3-
import { createConfig } from '../../shared/rollup.config.mjs';
3+
import { createConfig, emitModulePackageFile } from '../../shared/rollup.config.mjs';
44

55
import { babel } from './src/index.js';
66

77
const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
88

99
export default {
1010
...createConfig({ pkg }),
11-
input: './src/index.js',
11+
input: {
12+
index: './src/index.js',
13+
worker: './src/worker.js'
14+
},
15+
output: [
16+
{
17+
format: 'cjs',
18+
dir: 'dist/cjs',
19+
exports: 'named',
20+
footer(chunkInfo) {
21+
if (chunkInfo.name === 'index') {
22+
return 'module.exports = Object.assign(exports.default, exports);';
23+
}
24+
return null;
25+
},
26+
sourcemap: true
27+
},
28+
{
29+
format: 'es',
30+
dir: 'dist/es',
31+
plugins: [emitModulePackageFile()],
32+
sourcemap: true
33+
}
34+
],
1235
plugins: [
1336
babel({
1437
presets: [['@babel/preset-env', { targets: { node: 14 } }]],

packages/babel/src/index.js

Lines changed: 140 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
import { cpus } from 'os';
2+
import { fileURLToPath } from 'url';
3+
14
import * as babel from '@babel/core';
25
import { createFilter } from '@rollup/pluginutils';
6+
import workerpool from 'workerpool';
37

48
import { BUNDLED, HELPERS } from './constants.js';
5-
import bundledHelpersPlugin from './bundledHelpersPlugin.js';
6-
import preflightCheck from './preflightCheck.js';
79
import transformCode from './transformCode.js';
8-
import { addBabelPlugin, escapeRegExpCharacters, warnOnce } from './utils.js';
10+
import { escapeRegExpCharacters, warnOnce } from './utils.js';
911

1012
const unpackOptions = ({
1113
extensions = babel.DEFAULT_EXTENSIONS,
@@ -100,6 +102,68 @@ const returnObject = () => {
100102
return {};
101103
};
102104

105+
function findNonSerializableOption(obj) {
106+
const isSerializable = (value) => {
107+
if (value === null) return true;
108+
if (Array.isArray(value)) return value.every(isSerializable);
109+
switch (typeof value) {
110+
case 'undefined':
111+
case 'string':
112+
case 'number':
113+
case 'boolean':
114+
return true;
115+
case 'object':
116+
return Object.values(value).every(isSerializable);
117+
default:
118+
return false;
119+
}
120+
};
121+
122+
for (const key of Object.keys(obj)) {
123+
if (!isSerializable(obj[key])) return key;
124+
}
125+
return null;
126+
}
127+
128+
const WORKER_PATH = fileURLToPath(new URL('./worker.js', import.meta.url));
129+
130+
function createParallelWorkerPool(parallel, overrides) {
131+
if (typeof parallel === 'number' && (!Number.isInteger(parallel) || parallel < 1)) {
132+
throw new Error(
133+
'The "parallel" option must be true or a positive integer specifying the number of workers.'
134+
);
135+
}
136+
137+
if (!parallel) return null;
138+
139+
if (overrides?.config) {
140+
throw new Error('Cannot use "parallel" mode with a custom "config" override.');
141+
}
142+
if (overrides?.result) {
143+
throw new Error('Cannot use "parallel" mode with a custom "result" override.');
144+
}
145+
146+
// Default limits to 4 workers. Benefits diminish after this point, because of the setup cost.
147+
const workerCount = typeof parallel === 'number' ? parallel : Math.min(cpus().length, 4);
148+
return workerpool.pool(WORKER_PATH, {
149+
maxWorkers: workerCount,
150+
workerType: 'thread'
151+
});
152+
}
153+
154+
function transformWithWorkerPool(workerPool, context, transformOpts, babelOptions) {
155+
const nonSerializableKey = findNonSerializableOption(babelOptions);
156+
if (nonSerializableKey) {
157+
return Promise.reject(
158+
new Error(
159+
`Cannot use "parallel" mode because the "${nonSerializableKey}" option is not serializable.`
160+
)
161+
);
162+
}
163+
164+
return workerPool.exec('transform', [transformOpts]).catch((err) => context.error(err.message));
165+
}
166+
103167
function createBabelInputPluginFactory(customCallback = returnObject) {
104168
const overrides = customCallback(babel);
105169

@@ -116,9 +180,12 @@ function createBabelInputPluginFactory(customCallback = returnObject) {
116180
include,
117181
filter: customFilter,
118182
skipPreflightCheck,
183+
parallel,
119184
...babelOptions
120185
} = unpackInputPluginOptions(pluginOptionsWithOverrides);
121186

187+
const workerPool = createParallelWorkerPool(parallel, overrides);
188+
122189
const extensionRegExp = new RegExp(
123190
`(${extensions.map(escapeRegExpCharacters).join('|')})(\\?.*)?(#.*)?$`
124191
);
@@ -162,23 +229,45 @@ function createBabelInputPluginFactory(customCallback = returnObject) {
162229
if (!(await filter(filename, code))) return null;
163230
if (filename === HELPERS) return null;
164231

165-
return transformCode(
166-
code,
167-
{ ...babelOptions, filename },
168-
overrides,
232+
const resolvedBabelOptions = { ...babelOptions, filename };
233+
234+
if (workerPool) {
235+
return transformWithWorkerPool(
236+
workerPool,
237+
this,
238+
{
239+
inputCode: code,
240+
babelOptions: resolvedBabelOptions,
241+
skipPreflightCheck,
242+
babelHelpers
243+
},
244+
resolvedBabelOptions
245+
);
246+
}
247+
248+
return transformCode({
249+
inputCode: code,
250+
babelOptions: resolvedBabelOptions,
251+
overrides: {
252+
config: overrides.config?.bind(this),
253+
result: overrides.result?.bind(this)
254+
},
169255
customOptions,
170-
this,
171-
async (transformOptions) => {
172-
if (!skipPreflightCheck) {
173-
await preflightCheck(this, babelHelpers, transformOptions);
174-
}
175-
176-
return babelHelpers === BUNDLED
177-
? addBabelPlugin(transformOptions, bundledHelpersPlugin)
178-
: transformOptions;
179-
}
180-
);
256+
error: this.error.bind(this),
257+
skipPreflightCheck,
258+
babelHelpers
259+
});
260+
}
261+
},
262+
263+
async closeBundle() {
264+
if (!this.meta.watchMode) {
265+
await workerPool?.terminate();
181266
}
267+
},
268+
269+
async closeWatcher() {
270+
await workerPool?.terminate();
182271
}
183272
};
184273
};
@@ -207,6 +296,8 @@ function createBabelOutputPluginFactory(customCallback = returnObject) {
207296
overrides
208297
);
209298

299+
const workerPool = createParallelWorkerPool(pluginOptionsWithOverrides.parallel, overrides);
300+
210301
// cache for chunk name filter (includeChunks/excludeChunks)
211302
let chunkNameFilter;
212303

@@ -242,6 +333,7 @@ function createBabelOutputPluginFactory(customCallback = returnObject) {
242333
externalHelpers,
243334
externalHelpersWhitelist,
244335
include,
336+
parallel,
245337
runtimeHelpers,
246338
...babelOptions
247339
} = unpackOutputPluginOptions(pluginOptionsWithOverrides, outputOptions);
@@ -257,7 +349,36 @@ function createBabelOutputPluginFactory(customCallback = returnObject) {
257349
}
258350
}
259351

260-
return transformCode(code, babelOptions, overrides, customOptions, this);
352+
if (workerPool) {
353+
return transformWithWorkerPool(
354+
workerPool,
355+
this,
356+
{
357+
inputCode: code,
358+
babelOptions,
359+
skipPreflightCheck: true
360+
},
361+
babelOptions
362+
);
363+
}
364+
365+
return transformCode({
366+
inputCode: code,
367+
babelOptions,
368+
overrides: {
369+
config: overrides.config?.bind(this),
370+
result: overrides.result?.bind(this)
371+
},
372+
customOptions,
373+
error: this.error.bind(this),
374+
skipPreflightCheck: true
375+
});
376+
},
377+
378+
async generateBundle() {
379+
if (!this.meta.watchMode) {
380+
await workerPool?.terminate();
381+
}
261382
}
262383
};
263384
};

packages/babel/src/preflightCheck.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,27 +37,27 @@ const mismatchError = (actual, expected, filename) =>
3737
// Revert to /\/helpers\/(esm\/)?inherits/ when Babel 8 gets released, this was fixed in https://github.com/babel/babel/issues/14185
3838
const inheritsHelperRe = /[\\/]+helpers[\\/]+(esm[\\/]+)?inherits/;
3939

40-
export default async function preflightCheck(ctx, babelHelpers, transformOptions) {
40+
export default async function preflightCheck(error, babelHelpers, transformOptions) {
4141
const finalOptions = addBabelPlugin(transformOptions, helpersTestTransform);
4242
const check = (await babel.transformAsync(PREFLIGHT_INPUT, finalOptions)).code;
4343

4444
// Babel sometimes splits ExportDefaultDeclaration into 2 statements, so we also check for ExportNamedDeclaration
4545
if (!/export (d|{)/.test(check)) {
46-
ctx.error(MODULE_ERROR);
46+
error(MODULE_ERROR);
4747
}
4848

4949
if (inheritsHelperRe.test(check)) {
5050
if (babelHelpers === RUNTIME) {
5151
return;
5252
}
53-
ctx.error(mismatchError(RUNTIME, babelHelpers, transformOptions.filename));
53+
error(mismatchError(RUNTIME, babelHelpers, transformOptions.filename));
5454
}
5555

5656
if (check.includes('babelHelpers.inherits')) {
5757
if (babelHelpers === EXTERNAL) {
5858
return;
5959
}
60-
ctx.error(mismatchError(EXTERNAL, babelHelpers, transformOptions.filename));
60+
error(mismatchError(EXTERNAL, babelHelpers, transformOptions.filename));
6161
}
6262

6363
// test unminifiable string content
@@ -66,12 +66,12 @@ export default async function preflightCheck(ctx, babelHelpers, transformOptions
6666
return;
6767
}
6868
if (babelHelpers === RUNTIME && !transformOptions.plugins.length) {
69-
ctx.error(
69+
error(
7070
`You must use the \`@babel/plugin-transform-runtime\` plugin when \`babelHelpers\` is "${RUNTIME}".\n`
7171
);
7272
}
73-
ctx.error(mismatchError(INLINE, babelHelpers, transformOptions.filename));
73+
error(mismatchError(INLINE, babelHelpers, transformOptions.filename));
7474
}
7575

76-
ctx.error(UNEXPECTED_ERROR);
76+
error(UNEXPECTED_ERROR);
7777
}

0 commit comments

Comments
 (0)