diff --git a/packages/babel/README.md b/packages/babel/README.md
index 3b573fbec..33d2a3bdf 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.
@@ -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/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/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 125e6305a..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, stripQuery } from './utils.js';
+import { escapeRegExpCharacters, warnOnce } from './utils.js';
+import WorkerPool from './workerPool.js';
const unpackOptions = ({
extensions = babel.DEFAULT_EXTENSIONS,
@@ -37,7 +36,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 +62,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
}
});
@@ -101,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);
@@ -110,77 +126,113 @@ 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;
+ let workerPool;
+
+ const {
+ exclude,
+ extensions,
+ babelHelpers,
+ include,
+ filter: customFilter,
+ skipPreflightCheck,
+ parallel,
+ ...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);
- ({
- exclude,
- extensions,
- babelHelpers,
- include,
- filter: customFilter,
- skipPreflightCheck,
- ...babelOptions
- } = unpackInputPluginOptions(pluginOptionsWithOverrides, this.meta.rollupVersion));
+ if (parallel) {
+ const parallelAllowed =
+ isSerializable(babelOptions) && !overrides?.config && !overrides?.result;
- const extensionRegExp = new RegExp(
- `(${extensions.map(escapeRegExpCharacters).join('|')})$`
+ if (!parallelAllowed) {
+ throw new Error(
+ 'Cannot use "parallel" mode alongside custom overrides or non-serializable Babel options.'
);
- 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 parallelWorkerCount = typeof parallel === 'number' ? parallel : 4;
+ workerPool = new WorkerPool(
+ new URL('./worker.js', import.meta.url).pathname,
+ parallelWorkerCount
+ );
+ }
- return null;
+ const helpersFilter = { id: new RegExp(`^${escapeRegExpCharacters(HELPERS)}$`) };
+
+ return {
+ name: 'babel',
+
+ resolveId: {
+ filter: helpersFilter,
+ handler(id) {
+ if (id !== HELPERS) {
+ return null;
+ }
+ return id;
+ }
},
- resolveId(id) {
- if (id !== HELPERS) {
- return null;
+ load: {
+ filter: helpersFilter,
+ handler(id) {
+ if (id !== HELPERS) {
+ return null;
+ }
+ return babel.buildExternalHelpers(null, 'module');
}
- return id;
},
- load(id) {
- if (id !== HELPERS) {
- return null;
+ transform: {
+ filter: {
+ id: extensionRegExp
+ },
+ async handler(code, filename) {
+ if (!(await filter(filename, code))) return null;
+ if (filename === HELPERS) return null;
+
+ 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,
+ error: this.error.bind(this),
+ runPreflightCheck: !skipPreflightCheck,
+ babelHelpers
+ });
}
- return babel.buildExternalHelpers(null, 'module');
},
- transform(code, filename) {
- if (!filter(filename)) return null;
- if (filename === HELPERS) return null;
+ async closeBundle() {
+ if (parallel && !this.meta.watchMode) {
+ await workerPool.terminate();
+ }
+ },
- return transformCode(
- code,
- { ...babelOptions, filename },
- overrides,
- customOptions,
- this,
- async (transformOptions) => {
- if (!skipPreflightCheck) {
- await preflightCheck(this, babelHelpers, transformOptions);
- }
-
- return babelHelpers === BUNDLED
- ? addBabelPlugin(transformOptions, bundledHelpersPlugin)
- : transformOptions;
- }
- );
+ async closeWatcher() {
+ if (parallel) {
+ await workerPool.terminate();
+ }
}
};
};
@@ -259,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 c7d145558..0ca8b71a8 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
@@ -564,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 20288d8a0..7880f6d7a 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']
@@ -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