Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically transpile dependencies with babel-preset-env #559

Merged
merged 6 commits into from
Feb 9, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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