Skip to content

Commit

Permalink
Automatically transpile dependencies with babel-preset-env (#559)
Browse files Browse the repository at this point in the history
  • Loading branch information
devongovett committed Feb 9, 2018
1 parent e3fcfa0 commit 665e6b1
Show file tree
Hide file tree
Showing 34 changed files with 473 additions and 34 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@
"babel-template": "^6.25.0",
"babel-types": "^6.25.0",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
"babel-preset-env": "^1.6.1",
"babylon": "^6.17.4",
"babylon-walk": "^1.0.2",
"browser-resolve": "^1.11.2",
"browserslist": "^2.11.2",
"chalk": "^2.1.0",
"chokidar": "^1.7.0",
"command-exists": "^1.2.2",
Expand All @@ -41,6 +43,7 @@
"posthtml": "^0.10.1",
"resolve": "^1.4.0",
"sanitize-filename": "^1.6.1",
"semver": "^5.4.1",
"serialize-to-js": "^1.1.1",
"serve-static": "^1.12.4",
"source-map": "0.6.1",
Expand All @@ -53,7 +56,6 @@
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-preset-env": "^1.6.1",
"bsb-js": "^1.0.1",
"codecov": "^3.0.0",
"coffeescript": "^2.0.3",
Expand Down
10 changes: 7 additions & 3 deletions src/Asset.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,18 @@ class Asset {
return URL.format(parsed);
}

async getConfig(filenames) {
async getConfig(filenames, opts = {}) {
// Resolve the config file
let conf = await config.resolve(this.name, filenames);
let conf = await config.resolve(opts.path || this.name, filenames);
if (conf) {
// Add as a dependency so it is added to the watcher and invalidates
// this asset when the config changes.
this.addDependency(conf, {includedInParent: true});
return await config.load(this.name, filenames);
if (opts.load === false) {
return conf;
}

return await config.load(opts.path || this.name, filenames);
}

return null;
Expand Down
6 changes: 2 additions & 4 deletions src/assets/JSAsset.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,9 @@ class JSAsset extends Asset {
};

// Check if there is a babel config file. If so, determine which parser plugins to enable
this.babelConfig =
(this.package && this.package.babel) ||
(await this.getConfig(['.babelrc', '.babelrc.js']));
this.babelConfig = await babel.getConfig(this);
if (this.babelConfig) {
const file = new BabelFile({filename: this.name});
const file = new BabelFile(this.babelConfig);
options.plugins.push(...file.parserOpts.plugins);
}

Expand Down
197 changes: 177 additions & 20 deletions src/transforms/babel.js
Original file line number Diff line number Diff line change
@@ -1,44 +1,201 @@
const babel = require('babel-core');
const presetEnv = require('babel-preset-env');
const getTargetEngines = require('../utils/getTargetEngines');
const localRequire = require('../utils/localRequire');
const path = require('path');

module.exports = async function(asset) {
if (!await shouldTransform(asset)) {
const NODE_MODULES = `${path.sep}node_modules${path.sep}`;
const ENV_PLUGINS = require('babel-preset-env/data/plugins');
const ENV_PRESETS = {
es2015: true,
es2016: true,
es2017: true,
latest: true,
env: true
};

async function babelTransform(asset) {
let config = await getConfig(asset);
if (!config) {
return;
}

await asset.parseIfNeeded();

let config = {
code: false,
filename: asset.name
};
// If this is an internally generated config, use our internal babel-core,
// otherwise require a local version from the package we're compiling.
let babel = config.internal
? require('babel-core')
: await localRequire('babel-core', asset.name);

if (asset.isES6Module) {
config.babelrc = false;
config.plugins = [
require('babel-plugin-transform-es2015-modules-commonjs')
];
// TODO: support other versions of babel
if (parseInt(babel.version, 10) !== 6) {
throw new Error(`Unsupported babel version: ${babel.version}`);
}

let res = babel.transformFromAst(asset.ast, asset.contents, config);
if (!res.ignored) {
asset.ast = res.ast;
asset.isAstDirty = true;
}
};
}

async function shouldTransform(asset) {
module.exports = babelTransform;

async function getConfig(asset) {
let config = await getBabelConfig(asset);
if (config) {
config.code = false;
config.filename = asset.name;
config.babelrc = false;

// Hide the internal property from babel
let internal = config.internal;
delete config.internal;
Object.defineProperty(config, 'internal', {
value: internal
});
}

return config;
}

babelTransform.getConfig = getConfig;

async function getBabelConfig(asset) {
// If asset is marked as an ES6 modules, this is a second pass after dependencies are extracted.
// Just compile modules to CommonJS.
if (asset.isES6Module) {
return true;
return {
internal: true,
plugins: [require('babel-plugin-transform-es2015-modules-commonjs')]
};
}

if (asset.babelConfig) {
return asset.babelConfig;
}

let babelrc = await getBabelRc(asset);
let envConfig = await getEnvConfig(asset, !!babelrc);

// Merge the babel-preset-env config and the babelrc if needed
if (babelrc) {
if (envConfig) {
// Filter out presets that are already applied by babel-preset-env
if (Array.isArray(babelrc.presets)) {
babelrc.presets = babelrc.presets.filter(preset => {
preset = Array.isArray(preset) ? preset[0] : preset;
return !ENV_PRESETS[preset];
});
}

// Filter out plugins that are already applied by babel-preset-env
if (Array.isArray(babelrc.plugins)) {
babelrc.plugins = babelrc.plugins.filter(plugin => {
plugin = Array.isArray(plugin) ? plugin[0] : plugin;
return !ENV_PLUGINS[plugin];
});
}

// Add plugins generated by babel-preset-env to get to the app's target engines.
babelrc.plugins = (babelrc.plugins || []).concat(envConfig.plugins);
}

return babelrc;
}

// If there is a babel-preset-env config, and it isn't empty use that
if (envConfig && envConfig.plugins.length > 0) {
return envConfig;
}

// Otherwise, don't run babel at all
return null;
}

/**
* Finds a .babelrc for an asset. By default, .babelrc files inside node_modules are not used.
* However, there are some exceptions:
* - if `browserify.transforms` includes "babelify" in package.json (for legacy module compat)
*/
async function getBabelRc(asset) {
// Support legacy browserify packages
let browserify = asset.package && asset.package.browserify;
if (browserify && Array.isArray(browserify.transform)) {
// Look for babelify in the browserify transform list
let babelify = browserify.transform.find(
t => (Array.isArray(t) ? t[0] : t) === 'babelify'
);

// If specified as an array, override the config with the one specified
if (Array.isArray(babelify) && babelify[1]) {
return babelify[1];
}

// Otherwise, return the .babelrc if babelify was found
return babelify ? await findBabelRc(asset) : null;
}

if (asset.ast) {
return !!asset.babelConfig;
// If this asset is not in node_modules, always use the .babelrc
if (!asset.name.includes(NODE_MODULES)) {
return await findBabelRc(asset);
}

// Otherwise, don't load .babelrc for node_modules.
// See https://github.com/parcel-bundler/parcel/issues/13.
return null;
}

async function findBabelRc(asset) {
if (asset.package && asset.package.babel) {
return true;
return asset.package.babel;
}

return await asset.getConfig(['.babelrc', '.babelrc.js']);
}

/**
* Generates a babel-preset-env config for an asset.
* This is done by finding the source module's target engines, and the app's
* target engines, and doing a diff to include only the necessary plugins.
*/
async function getEnvConfig(asset, isSourceModule) {
// Load the target engines for the app and generate a babel-preset-env config
let targetEngines = await getTargetEngines(asset, true);
let targetEnv = await getEnvPlugins(targetEngines);
if (!targetEnv) {
return null;
}

// If this is the app module, the source and target will be the same, so just compile everything.
// Otherwise, load the source engines and generate a babel-present-env config.
if (asset.name.includes(NODE_MODULES) && !isSourceModule) {
let sourceEngines = await getTargetEngines(asset, false);
let sourceEnv = (await getEnvPlugins(sourceEngines)) || targetEnv;

// Do a diff of the returned plugins. We only need to process the remaining plugins to get to the app target.
let sourcePlugins = new Set(sourceEnv.map(p => p[0]));
targetEnv = targetEnv.filter(plugin => {
return !sourcePlugins.has(plugin[0]);
});
}

return {plugins: targetEnv, internal: true};
}

const envCache = new Map();

async function getEnvPlugins(targets) {
if (!targets) {
return null;
}

let key = JSON.stringify(targets);
if (envCache.has(key)) {
return envCache.get(key);
}

let babelrc = await asset.getConfig(['.babelrc', '.babelrc.js']);
return !!babelrc;
let plugins = presetEnv.default({}, {targets, modules: false}).plugins;
envCache.set(key, plugins);
return plugins;
}
Loading

0 comments on commit 665e6b1

Please sign in to comment.