Merge pull request #3281 from DmitrySoshnikov/pass_per_preset

[RFC] Pass per preset
This commit is contained in:
Amjad Masad
2016-01-22 14:29:07 -08:00
4 changed files with 188 additions and 35 deletions

View File

@@ -8,7 +8,6 @@ import * as metadataVisitor from "./metadata";
import convertSourceMap from "convert-source-map";
import OptionManager from "./options/option-manager";
import type Pipeline from "../pipeline";
import type Plugin from "../plugin";
import PluginPass from "../plugin-pass";
import shebangRegex from "shebang-regex";
import { NodePath, Hub, Scope } from "babel-traverse";
@@ -61,8 +60,21 @@ export default class File extends Store {
this.pluginVisitors = [];
this.pluginPasses = [];
this.pluginStack = [];
this.buildPlugins();
// Plugins for top-level options.
this.buildPluginsForOptions(this.opts);
// If we are in the "pass per preset" mode, build
// also plugins for each preset.
if (this.opts.passPerPreset) {
// All the "per preset" options are inherited from the main options.
this.perPresetOpts = [];
this.opts.presets.forEach(presetOpts => {
let perPresetOpts = Object.assign(Object.create(this.opts), presetOpts);
this.perPresetOpts.push(perPresetOpts);
this.buildPluginsForOptions(perPresetOpts);
});
}
this.metadata = {
usedHelpers: [],
@@ -95,7 +107,6 @@ export default class File extends Store {
pluginVisitors: Array<Object>;
pluginPasses: Array<PluginPass>;
pluginStack: Array<Plugin>;
pipeline: Pipeline;
parserOpts: BabelParserOptions;
log: Logger;
@@ -165,21 +176,29 @@ export default class File extends Store {
return opts;
}
buildPlugins() {
let plugins: Array<[PluginPass, Object]> = this.opts.plugins.concat(INTERNAL_PLUGINS);
buildPluginsForOptions(opts) {
if (!Array.isArray(opts.plugins)) {
return;
}
let plugins: Array<[PluginPass, Object]> = opts.plugins.concat(INTERNAL_PLUGINS);
let currentPluginVisitors = [];
let currentPluginPasses = [];
// init plugins!
for (let ref of plugins) {
let [plugin, pluginOpts] = ref; // todo: fix - can't embed in loop head because of flow bug
this.pluginStack.push(plugin);
this.pluginVisitors.push(plugin.visitor);
this.pluginPasses.push(new PluginPass(this, plugin, pluginOpts));
currentPluginVisitors.push(plugin.visitor);
currentPluginPasses.push(new PluginPass(this, plugin, pluginOpts));
if (plugin.manipulateOptions) {
plugin.manipulateOptions(this.opts, this.parserOpts, this);
plugin.manipulateOptions(opts, this.parserOpts, this);
}
}
this.pluginVisitors.push(currentPluginVisitors);
this.pluginPasses.push(currentPluginPasses);
}
getModuleName(): ?string {
@@ -419,11 +438,16 @@ export default class File extends Store {
}
transform(): BabelFileResult {
this.call("pre");
this.log.debug(`Start transform traverse`);
traverse(this.ast, traverse.visitors.merge(this.pluginVisitors, this.pluginPasses), this.scope);
this.log.debug(`End transform traverse`);
this.call("post");
// In the "pass per preset" mode, we have grouped passes.
// Otherwise, there is only one plain pluginPasses array.
this.pluginPasses.forEach((pluginPasses, index) => {
this.call("pre", pluginPasses);
this.log.debug(`Start transform traverse`);
traverse(this.ast, traverse.visitors.merge(this.pluginVisitors[index], pluginPasses), this.scope);
this.log.debug(`End transform traverse`);
this.call("post", pluginPasses);
});
return this.generate();
}
@@ -483,8 +507,8 @@ export default class File extends Store {
return util.shouldIgnore(opts.filename, opts.ignore, opts.only);
}
call(key: "pre" | "post") {
for (let pass of (this.pluginPasses: Array<PluginPass>)) {
call(key: "pre" | "post", pluginPasses: Array<PluginPass>) {
for (let pass of pluginPasses) {
let plugin = pass.plugin;
let fn = plugin[key];
if (fn) fn.call(pass, this);

View File

@@ -183,5 +183,12 @@ module.exports = {
moduleId: {
description: "specify a custom name for module ids",
type: "string"
}
},
passPerPreset: {
description: "Whether to spawn a traversal pass per a preset. By default all presets are merged.",
type: "boolean",
default: false,
hidden: true,
},
};

View File

@@ -157,22 +157,23 @@ export default class OptionManager {
throw err;
}
this.mergeOptions(opts, loc, null, path.dirname(loc));
this.mergeOptions(opts, this.options, loc, null, path.dirname(loc));
this.resolvedConfigs.push(loc);
return !!opts;
}
/**
* This is called when we want to merge the input `opts` into our
* base options.
* This is called when we want to merge the input `opts` into the
* base options (passed as the `extendingOpts`: at top-level it's the
* main options, at presets level it's presets options).
*
* - `alias` is used to output pretty traces back to the original source.
* - `loc` is used to point to the original config.
* - `dirname` is used to resolve plugins relative to it.
*/
mergeOptions(rawOpts?: Object, alias: string = "foreign", loc?: string, dirname?: string) {
mergeOptions(rawOpts?: Object, extendingOpts?: Object, alias: string = "foreign", loc?: string, dirname?: string) {
if (!rawOpts) return;
//
@@ -221,8 +222,17 @@ export default class OptionManager {
// resolve presets
if (opts.presets) {
this.mergePresets(opts.presets, dirname);
delete opts.presets;
// If we're in the "pass per preset" mode, we resolve the presets
// and keep them for further execution to calculate the options.
if (opts.passPerPreset) {
opts.presets = this.resolvePresets(opts.presets, dirname, (preset, presetLoc) => {
this.mergeOptions(preset, preset, presetLoc, presetLoc, dirname);
});
} else {
// Otherwise, just merge presets options into the main options.
this.mergePresets(opts.presets, dirname);
delete opts.presets;
}
}
// env
@@ -233,29 +243,57 @@ export default class OptionManager {
delete opts.env;
}
// merge them into this current files options
merge(this.options, opts);
// Merge them into current extending options in case of top-level
// options. In case of presets, just re-assign options which are got
// normalized during the `mergeOptions`.
if (rawOpts !== extendingOpts) {
merge(extendingOpts, opts);
} else {
Object.assign(extendingOpts, opts);
}
// merge in env options
this.mergeOptions(envOpts, `${alias}.env.${envKey}`, null, dirname);
this.mergeOptions(envOpts, extendingOpts, `${alias}.env.${envKey}`, null, dirname);
}
/**
* Merges all presets into the main options in case we are not in the
* "pass per preset" mode. Otherwise, options are calculated per preset.
*/
mergePresets(presets: Array<string | Object>, dirname: string) {
for (let val of presets) {
this.resolvePresets(presets, dirname, (presetOpts, presetLoc) => {
this.mergeOptions(
presetOpts,
this.options,
presetLoc,
presetLoc,
path.dirname(presetLoc)
);
});
}
/**
* Resolves presets options which can be either direct object data,
* or a module name to require.
*/
resolvePresets(presets: Array<string | Object>, dirname: string, onResolve?) {
return presets.map(val => {
if (typeof val === "string") {
let presetLoc = resolve(`babel-preset-${val}`, dirname) || resolve(val, dirname);
if (presetLoc) {
let presetOpts = require(presetLoc);
this.mergeOptions(presetOpts, presetLoc, presetLoc, path.dirname(presetLoc));
let val = require(presetLoc);
onResolve && onResolve(val, presetLoc);
return val;
} else {
throw new Error(`Couldn't find preset ${JSON.stringify(val)} relative to directory ${JSON.stringify(dirname)}`);
}
} else if (typeof val === "object") {
this.mergeOptions(val);
onResolve && onResolve(val);
return val;
} else {
throw new Error("todo");
throw new Error(`Unsupported preset format: ${val}.`);
}
}
});
}
addIgnoreConfig(loc) {
@@ -266,7 +304,7 @@ export default class OptionManager {
.map((line) => line.replace(/#(.*?)$/, "").trim())
.filter((line) => !!line);
this.mergeOptions({ ignore: lines }, loc);
this.mergeOptions({ ignore: lines }, this.options, loc);
}
findConfigs(loc) {
@@ -333,7 +371,7 @@ export default class OptionManager {
}
// merge in base options
this.mergeOptions(opts, "base", null, filename && path.dirname(filename));
this.mergeOptions(opts, this.options, "base", null, filename && path.dirname(filename));
// normalise
this.normaliseOptions(opts);

View File

@@ -4,6 +4,7 @@ var Pipeline = require("../lib/transformation/pipeline");
var sourceMap = require("source-map");
var assert = require("assert");
var File = require("../lib/transformation/file").default;
var Plugin = require("../lib/transformation/plugin");
function assertIgnored(result) {
assert.ok(result.ignored);
@@ -44,6 +45,89 @@ suite("api", function () {
});
});
test("pass per preset", function () {
var aliasBaseType = null;
function execTest(passPerPreset) {
return babel.transform('type Foo = number; let x = (y): Foo => y;', {
passPerPreset: passPerPreset,
presets: [
// First preset with our plugin, "before"
{
plugins: [
new Plugin({
visitor: {
Function: function(path) {
var node = path.node;
var scope = path.scope;
var alias = scope
.getProgramParent()
.getBinding(node.returnType.typeAnnotation.id.name)
.path
.node;
// In case of `passPerPreset` being `false`, the
// alias node is already removed by Flow plugin.
if (!alias) {
return;
}
// In case of `passPerPreset` being `true`, the
// alias node should still exist.
aliasBaseType = alias.right.type; // NumberTypeAnnotation
}
}
})
]
},
// ES2015 preset
require(__dirname + "/../../babel-preset-es2015"),
// Third preset for Flow.
{
plugins: [
require(__dirname + "/../../babel-plugin-syntax-flow"),
require(__dirname + "/../../babel-plugin-transform-flow-strip-types"),
]
}
],
});
}
// 1. passPerPreset: true
var result = execTest(true);
assert.equal(aliasBaseType, "NumberTypeAnnotation");
assert.deepEqual([
'"use strict";',
'',
'var x = function x(y) {',
' return y;',
'};'
].join("\n"), result.code);
// 2. passPerPreset: false
aliasBaseType = null;
var result = execTest(false);
assert.equal(aliasBaseType, null);
assert.deepEqual([
'"use strict";',
'',
'var x = function x(y) {',
' return y;',
'};'
].join("\n"), result.code);
});
test("source map merging", function () {
var result = babel.transform([
'function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }',