broccoli-babel-transpiler
Version:
A Broccoli plugin which transpile ES6 to readable ES5 by using babel.
287 lines (240 loc) • 7.79 kB
JavaScript
;
const transpiler = require('@babel/core');
const path = require('path');
const workerpool = require('workerpool');
const Promise = require('rsvp').Promise;
const debugGenerator = require('heimdalljs-logger');
const JOBS = Number(process.env.JOBS) || require('os').cpus().length;
const loggerName = 'broccoli-persistent-filter:babel:parallel-api';
const _logger = debugGenerator(loggerName);
const babelCoreVersion = getBabelVersion();
// return the version of Babel that will be used by this plugin
function getBabelVersion() {
return require('@babel/core/package.json').version;
}
function getWorkerPool() {
let pool;
let globalPoolID = 'v2/broccoli-babel-transpiler/async-workerpool/babel-core-' + babelCoreVersion;
let existingPool = process[globalPoolID];
if (existingPool) {
pool = existingPool;
} else {
pool = workerpool.pool(path.join(__dirname, 'worker.js'), { workerType: 'auto', maxWorkers: JOBS });
process[globalPoolID] = pool;
}
return pool;
}
function implementsParallelAPI(object) {
const type = typeof object;
const hasProperties = type === 'function' || (type === 'object' && object !== null) || Array.isArray(object);
return hasProperties &&
object._parallelBabel !== null &&
typeof object._parallelBabel === 'object' &&
typeof object._parallelBabel.requireFile === 'string';
}
function isSerializable(value) {
if (value === null) {
return true;
} else if (implementsParallelAPI(value)) {
return true;
} else if (Array.isArray(value)) {
return value.every(isSerializable);
} else {
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 pluginsAreParallelizable(plugins) {
const errors = [];
let isParallelizable = true;
if (Array.isArray(plugins)) {
for (let i = 0; i < plugins.length; i++) {
let plugin = plugins[i];
if (isSerializable(plugin) === false) {
isParallelizable = false;
errors.push(humanizePlugin(plugin));
}
}
}
return {
isParallelizable,
errors
};
}
function callbacksAreParallelizable(babelOptions) {
const callbacks = Object.keys(babelOptions).filter(key => typeof babelOptions[key] === 'function');
let isParallelizable = true;
const errors = [];
for (let i = 0; i < callbacks.length; i++) {
let callback = babelOptions[callbacks[i]];
if (implementsParallelAPI(callback) === false) {
isParallelizable = false;
errors.push(humanizePlugin(callback))
}
}
return { isParallelizable, errors };
}
function transformIsParallelizable(babelOptions) {
const plugins = pluginsAreParallelizable(babelOptions.plugins);
const callbacks = callbacksAreParallelizable(babelOptions);
return {
isParallelizable: isSerializable(babelOptions),
errors: [].concat(plugins.errors, callbacks.errors)
};
}
function humanizePlugin(plugin) {
let name, baseDir;
if (Array.isArray(plugin) && typeof plugin[0] === 'function') {
plugin = plugin[0];
}
if (typeof plugin === 'function' || typeof plugin === 'object' && plugin !== null) {
if (typeof plugin.name === 'string') {
name = plugin.name;
}
if (typeof plugin.baseDir === 'function') {
baseDir = plugin.baseDir();
}
}
const output = `name: ${name || 'unknown'}, location: ${baseDir || 'unknown'}`;
if (!name && typeof plugin === 'function') {
return output + `,\n↓ function source ↓ \n${plugin.toString().substr(0, 200)}\n \n`;
} else {
return output;
}
}
function buildFromParallelApiInfo(parallelApiInfo, cacheKey) {
let requiredStuff = require(parallelApiInfo.requireFile);
if (parallelApiInfo.useMethod) {
if (requiredStuff[parallelApiInfo.useMethod] === undefined) {
throw new Error("method '" + parallelApiInfo.useMethod + "' does not exist in file " + parallelApiInfo.requireFile);
}
return requiredStuff[parallelApiInfo.useMethod];
}
if (parallelApiInfo.buildUsing) {
if (typeof requiredStuff[parallelApiInfo.buildUsing] !== 'function') {
throw new Error("'" + parallelApiInfo.buildUsing + "' is not a function in file " + parallelApiInfo.requireFile);
}
return requiredStuff[parallelApiInfo.buildUsing](parallelApiInfo.params, cacheKey);
}
return requiredStuff;
}
function deserialize(data, cacheKey) {
if (data.cacheKey) {
cacheKey = data.cacheKey;
}
if (data.babel) {
data = data.babel;
}
if (Array.isArray(data)) {
const result = [];
for (let i =0; i < data.length; i++) {
let member = data[i];
if (implementsParallelAPI(member)) {
result[i] = buildFromParallelApiInfo(member._parallelBabel, cacheKey);
} else {
result[i] = deserialize(member, cacheKey);
}
}
return result;
} else if (typeof data === 'object' && data !== null) {
const result = {};
Object.keys(data).forEach(key => {
let value = data[key];
if (value === undefined || value == null) {
} else if (implementsParallelAPI(value)) {
value = buildFromParallelApiInfo(value._parallelBabel, cacheKey);
} else {
value = deserialize(value, cacheKey);
}
result[key] = value;
});
return result;
} else {
return data;
}
}
const visited = new WeakSet();
// replace callback functions with objects so they can be transferred to the worker processes
function serialize(options) {
let optionsType = typeof options;
if ((optionsType === 'object' && options !== null) ||
optionsType === 'function' ||
Array.isArray(options)) {
if (visited.has(options)) {
// a cycle, so simply reuse
return options;
} else {
if (implementsParallelAPI(options)) {
options = {
_parallelBabel: options._parallelBabel
};
}
visited.add(options);
}
} else {
return options;
}
const serialized = Array.isArray(options) ? [] : {};
Object.keys(options).forEach(key => {
let value = options[key];
if (value === options) {
throw new TypeError('Babel Plugins contain a cycle');
}
if (isSerializable(value)) {
value = serialize(value);
} else {
// leave as is
}
serialized[key] = value;
});
return serialized;
}
async function transformString(string, options, buildOptions) {
const isParallelizable = transformIsParallelizable(
options.babel
).isParallelizable;
if (JOBS > 1 && isParallelizable) {
let pool = getWorkerPool();
_logger.info("transformString is parallelizable");
let serializedObj = {
babel: serialize(options.babel),
cacheKey: options.cacheKey,
};
let result = await pool.exec("transform", [string, serializedObj]);
if (result.error) {
// when the worker has an error it still resolves, but it has `error`
// and `stack` properties instead of `code` + `metadata`
//
// throw an error to properly fail the "top level" process as needed
throw new Error(result.error + result.stack);
}
return result;
} else {
if (JOBS <= 1) {
_logger.info("JOBS <= 1, skipping worker, using main thread");
} else {
_logger.info("transformString is NOT parallelizable");
}
return await transpiler.transformAsync(string, deserialize(options));
}
}
module.exports = {
jobs: JOBS,
getBabelVersion,
implementsParallelAPI,
isSerializable,
pluginsAreParallelizable,
callbacksAreParallelizable,
transformIsParallelizable,
deserialize,
serialize,
buildFromParallelApiInfo,
transformString,
humanizePlugin,
getWorkerPool
};