From 52d337e4d9dd8319bad033bdbf5647257d8ff7f3 Mon Sep 17 00:00:00 2001 From: Logan Smyth Date: Wed, 8 Nov 2017 14:41:05 -0800 Subject: [PATCH 1/2] Move plugin object validation into plugin file. --- .../src/config/option-assertions.js | 6 + .../babel-core/src/config/option-manager.js | 52 ++------ packages/babel-core/src/config/options.js | 8 +- packages/babel-core/src/config/plugin.js | 117 +++++++++++++----- 4 files changed, 108 insertions(+), 75 deletions(-) diff --git a/packages/babel-core/src/config/option-assertions.js b/packages/babel-core/src/config/option-assertions.js index 01c65e4e14..bc0ce7fae6 100644 --- a/packages/babel-core/src/config/option-assertions.js +++ b/packages/babel-core/src/config/option-assertions.js @@ -12,6 +12,12 @@ import type { RootInputSourceMapOption, } from "./options"; +export type ValidatorSet = { + [string]: Validator, +}; + +export type Validator = (string, mixed) => T; + export function assertSourceMaps( key: string, value: mixed, diff --git a/packages/babel-core/src/config/option-manager.js b/packages/babel-core/src/config/option-manager.js index 0c4cd84ffa..20cab68f1c 100644 --- a/packages/babel-core/src/config/option-manager.js +++ b/packages/babel-core/src/config/option-manager.js @@ -1,7 +1,7 @@ // @flow import * as context from "../index"; -import Plugin from "./plugin"; +import Plugin, { validatePluginObject } from "./plugin"; import defaults from "lodash/defaults"; import merge from "lodash/merge"; import buildConfigChain, { type ConfigItem } from "./build-config-chain"; @@ -28,15 +28,6 @@ type MergeOptions = dirname: string, }; -const ALLOWED_PLUGIN_KEYS = new Set([ - "name", - "manipulateOptions", - "pre", - "post", - "visitor", - "inherits", -]); - export default function manageOptions(opts: {}): { options: Object, passes: Array>, @@ -275,37 +266,16 @@ function loadPluginDescriptor(descriptor: BasicDescriptor): Plugin { } const instantiatePlugin = makeWeakCache( - ( - { value: pluginObj, options, dirname, alias }: LoadedDescriptor, - cache, - ): Plugin => { - Object.keys(pluginObj).forEach(key => { - if (!ALLOWED_PLUGIN_KEYS.has(key)) { - throw new Error( - `Plugin ${alias} provided an invalid property of ${key}`, - ); - } - }); - if ( - pluginObj.visitor && - (pluginObj.visitor.enter || pluginObj.visitor.exit) - ) { - throw new Error( - "Plugins aren't allowed to specify catch-all enter/exit handlers. " + - "Please target individual nodes.", - ); + ({ value, options, dirname, alias }: LoadedDescriptor, cache): Plugin => { + const pluginObj = validatePluginObject(value); + + const plugin = Object.assign({}, pluginObj); + if (plugin.visitor) { + plugin.visitor = traverse.explode(clone(plugin.visitor)); } - const plugin = Object.assign({}, pluginObj, { - visitor: clone(pluginObj.visitor || {}), - }); - - traverse.explode(plugin.visitor); - - let inheritsDescriptor; - let inherits; if (plugin.inherits) { - inheritsDescriptor = { + const inheritsDescriptor = { alias: `${alias}$inherits`, value: plugin.inherits, options, @@ -313,7 +283,7 @@ const instantiatePlugin = makeWeakCache( }; // If the inherited plugin changes, reinstantiate this plugin. - inherits = cache.invalidate(() => + const inherits = cache.invalidate(() => loadPluginDescriptor(inheritsDescriptor), ); @@ -324,8 +294,8 @@ const instantiatePlugin = makeWeakCache( plugin.manipulateOptions, ); plugin.visitor = traverse.visitors.merge([ - inherits.visitor, - plugin.visitor, + inherits.visitor || {}, + plugin.visitor || {}, ]); } diff --git a/packages/babel-core/src/config/options.js b/packages/babel-core/src/config/options.js index 8720dfe267..e47babff92 100644 --- a/packages/babel-core/src/config/options.js +++ b/packages/babel-core/src/config/options.js @@ -12,14 +12,10 @@ import { assertSourceMaps, assertCompact, assertSourceType, + type ValidatorSet, + type Validator, } from "./option-assertions"; -type ValidatorSet = { - [string]: Validator, -}; - -type Validator = (string, mixed) => T; - const ROOT_VALIDATORS: ValidatorSet = { filename: (assertString: Validator< $PropertyType, diff --git a/packages/babel-core/src/config/plugin.js b/packages/babel-core/src/config/plugin.js index 615a5f5ece..edc251d0ce 100644 --- a/packages/babel-core/src/config/plugin.js +++ b/packages/babel-core/src/config/plugin.js @@ -1,43 +1,104 @@ // @flow -// $FlowIssue recursion? +import { + assertString, + assertFunction, + assertObject, + type ValidatorSet, + type Validator, +} from "./option-assertions"; + +const VALIDATORS: ValidatorSet = { + name: (assertString: Validator<$PropertyType>), + manipulateOptions: (assertFunction: Validator< + $PropertyType, + >), + pre: (assertFunction: Validator<$PropertyType>), + post: (assertFunction: Validator<$PropertyType>), + inherits: (assertFunction: Validator< + $PropertyType, + >), + visitor: (assertVisitorMap: Validator< + $PropertyType, + >), +}; + +function assertVisitorMap(key: string, value: mixed): VisitorMap { + const obj = assertObject(key, value); + if (obj) { + Object.keys(obj).forEach(prop => assertVisitorHandler(prop, obj[prop])); + + if (obj.enter || obj.exit) { + throw new Error( + `.${key} cannot contain catch-all "enter" or "exit" handlers. Please target individual nodes.`, + ); + } + } + return (obj: any); +} + +function assertVisitorHandler( + key: string, + value: mixed, +): VisitorHandler | void { + if (value && typeof value === "object") { + Object.keys(value).forEach(handler => { + if (handler !== "enter" && handler !== "exit") { + throw new Error( + `.visitor["${key}"] may only have .enter and/or .exit handlers.`, + ); + } + }); + } else if (typeof value !== "function") { + throw new Error(`.visitor["${key}"] must be a function`); + } + + return (value: any); +} + +type VisitorHandler = Function | { enter?: Function, exit?: Function }; +export type VisitorMap = { + [string]: VisitorHandler, +}; + +export type PluginObject = { + name?: string, + manipulateOptions?: Function, + + pre?: Function, + post?: Function, + + inherits?: Function, + visitor?: VisitorMap, +}; + +export function validatePluginObject(obj: {}): PluginObject { + Object.keys(obj).forEach(key => { + const validator = VALIDATORS[key]; + + if (validator) validator(key, obj[key]); + else throw new Error(`.${key} is not a valid Plugin property`); + }); + + return (obj: any); +} + export default class Plugin { key: ?string; - manipulateOptions: ?Function; - post: ?Function; - pre: ?Function; - visitor: ?{}; + manipulateOptions: Function | void; + post: Function | void; + pre: Function | void; + visitor: {}; options: {}; - constructor(plugin: {}, options: {}, key?: string) { - if (plugin.name != null && typeof plugin.name !== "string") { - throw new Error("Plugin .name must be a string, null, or undefined"); - } - if ( - plugin.manipulateOptions != null && - typeof plugin.manipulateOptions !== "function" - ) { - throw new Error( - "Plugin .manipulateOptions must be a function, null, or undefined", - ); - } - if (plugin.post != null && typeof plugin.post !== "function") { - throw new Error("Plugin .post must be a function, null, or undefined"); - } - if (plugin.pre != null && typeof plugin.pre !== "function") { - throw new Error("Plugin .pre must be a function, null, or undefined"); - } - if (plugin.visitor != null && typeof plugin.visitor !== "object") { - throw new Error("Plugin .visitor must be an object, null, or undefined"); - } - + constructor(plugin: PluginObject, options: {}, key?: string) { this.key = plugin.name || key; this.manipulateOptions = plugin.manipulateOptions; this.post = plugin.post; this.pre = plugin.pre; - this.visitor = plugin.visitor; + this.visitor = plugin.visitor || {}; this.options = options; } } From fc7fcfac0a5c2835d50e7ec02b1a59e77416bdb0 Mon Sep 17 00:00:00 2001 From: Logan Smyth Date: Wed, 8 Nov 2017 16:37:22 -0800 Subject: [PATCH 2/2] Expose a clear API for plugins to override the parser/generator. --- .../src/config/loading/files/index-browser.js | 18 ------ .../src/config/loading/files/plugins.js | 56 ------------------- .../babel-core/src/config/option-manager.js | 38 +------------ packages/babel-core/src/config/plugin.js | 19 +++++++ .../src/transformation/file/generate.js | 31 ++++++++-- .../babel-core/src/transformation/index.js | 21 ++++--- .../src/transformation/normalize-file.js | 47 +++++++++------- 7 files changed, 87 insertions(+), 143 deletions(-) diff --git a/packages/babel-core/src/config/loading/files/index-browser.js b/packages/babel-core/src/config/loading/files/index-browser.js index 2c8f80d79f..1d0b249225 100644 --- a/packages/babel-core/src/config/loading/files/index-browser.js +++ b/packages/babel-core/src/config/loading/files/index-browser.js @@ -42,21 +42,3 @@ export function loadPreset( `Cannot load preset ${name} relative to ${dirname} in a browser`, ); } - -export function loadParser( - name: string, - dirname: string, -): { filepath: string, value: Function } { - throw new Error( - `Cannot load parser ${name} relative to ${dirname} in a browser`, - ); -} - -export function loadGenerator( - name: string, - dirname: string, -): { filepath: string, value: Function } { - throw new Error( - `Cannot load generator ${name} relative to ${dirname} in a browser`, - ); -} diff --git a/packages/babel-core/src/config/loading/files/plugins.js b/packages/babel-core/src/config/loading/files/plugins.js index eed68a4d31..68b369225d 100644 --- a/packages/babel-core/src/config/loading/files/plugins.js +++ b/packages/babel-core/src/config/loading/files/plugins.js @@ -57,62 +57,6 @@ export function loadPreset( return { filepath, value }; } -export function loadParser( - name: string, - dirname: string, -): { filepath: string, value: Function } { - const filepath = resolve.sync(name, { basedir: dirname }); - - const mod = requireModule("parser", filepath); - - if (!mod) { - throw new Error( - `Parser ${name} relative to ${dirname} does not export an object`, - ); - } - if (typeof mod.parse !== "function") { - throw new Error( - `Parser ${name} relative to ${dirname} does not export a .parse function`, - ); - } - const value = mod.parse; - - debug("Loaded parser %o from %o.", name, dirname); - - return { - filepath, - value, - }; -} - -export function loadGenerator( - name: string, - dirname: string, -): { filepath: string, value: Function } { - const filepath = resolve.sync(name, { basedir: dirname }); - - const mod = requireModule("generator", filepath); - - if (!mod) { - throw new Error( - `Generator ${name} relative to ${dirname} does not export an object`, - ); - } - if (typeof mod.print !== "function") { - throw new Error( - `Generator ${name} relative to ${dirname} does not export a .print function`, - ); - } - const value = mod.print; - - debug("Loaded generator %o from %o.", name, dirname); - - return { - filepath, - value, - }; -} - function standardizeName(type: "plugin" | "preset", name: string) { // Let absolute and relative paths through. if (path.isAbsolute(name)) return name; diff --git a/packages/babel-core/src/config/option-manager.js b/packages/babel-core/src/config/option-manager.js index 20cab68f1c..6162b47df9 100644 --- a/packages/babel-core/src/config/option-manager.js +++ b/packages/babel-core/src/config/option-manager.js @@ -12,12 +12,7 @@ import { makeWeakCache } from "./caching"; import { getEnv } from "./helpers/environment"; import { validate, type ValidatedOptions, type PluginItem } from "./options"; -import { - loadPlugin, - loadPreset, - loadParser, - loadGenerator, -} from "./loading/files"; +import { loadPlugin, loadPreset } from "./loading/files"; type MergeOptions = | ConfigItem @@ -196,7 +191,7 @@ const loadConfig = makeWeakCache((config: MergeOptions): { plugins: Array, presets: Array, } => { - const options = normalizeOptions(config); + const options = config.options; const plugins = (config.options.plugins || []).map((plugin, index) => createDescriptor(plugin, loadPlugin, config.dirname, { @@ -321,35 +316,6 @@ const instantiatePreset = makeWeakCache( }, ); -/** - * Validate and return the options object for the config. - */ -function normalizeOptions(config) { - // - const options = Object.assign({}, config.options); - - if (options.parserOpts && typeof options.parserOpts.parser === "string") { - options.parserOpts = Object.assign({}, options.parserOpts); - (options.parserOpts: any).parser = loadParser( - options.parserOpts.parser, - config.dirname, - ).value; - } - - if ( - options.generatorOpts && - typeof options.generatorOpts.generator === "string" - ) { - options.generatorOpts = Object.assign({}, options.generatorOpts); - (options.generatorOpts: any).generator = loadGenerator( - options.generatorOpts.generator, - config.dirname, - ).value; - } - - return options; -} - /** * Given a plugin/preset item, resolve it into a standard format. */ diff --git a/packages/babel-core/src/config/plugin.js b/packages/babel-core/src/config/plugin.js index edc251d0ce..97b1364777 100644 --- a/packages/babel-core/src/config/plugin.js +++ b/packages/babel-core/src/config/plugin.js @@ -8,6 +8,9 @@ import { type Validator, } from "./option-assertions"; +// Note: The casts here are just meant to be static assertions to make sure +// that the assertion functions actually assert that the value's type matches +// the declared types. const VALIDATORS: ValidatorSet = { name: (assertString: Validator<$PropertyType>), manipulateOptions: (assertFunction: Validator< @@ -21,6 +24,13 @@ const VALIDATORS: ValidatorSet = { visitor: (assertVisitorMap: Validator< $PropertyType, >), + + parserOverride: (assertFunction: Validator< + $PropertyType, + >), + generatorOverride: (assertFunction: Validator< + $PropertyType, + >), }; function assertVisitorMap(key: string, value: mixed): VisitorMap { @@ -70,6 +80,9 @@ export type PluginObject = { inherits?: Function, visitor?: VisitorMap, + + parserOverride?: Function, + generatorOverride?: Function, }; export function validatePluginObject(obj: {}): PluginObject { @@ -90,6 +103,9 @@ export default class Plugin { pre: Function | void; visitor: {}; + parserOverride: Function | void; + generatorOverride: Function | void; + options: {}; constructor(plugin: PluginObject, options: {}, key?: string) { @@ -99,6 +115,9 @@ export default class Plugin { this.post = plugin.post; this.pre = plugin.pre; this.visitor = plugin.visitor || {}; + this.parserOverride = plugin.parserOverride; + this.generatorOverride = plugin.generatorOverride; + this.options = options; } } diff --git a/packages/babel-core/src/transformation/file/generate.js b/packages/babel-core/src/transformation/file/generate.js index e31c1726a1..ac8a1e5de7 100644 --- a/packages/babel-core/src/transformation/file/generate.js +++ b/packages/babel-core/src/transformation/file/generate.js @@ -1,5 +1,6 @@ // @flow +import type { PluginPasses } from "../../config"; import convertSourceMap, { type SourceMap } from "convert-source-map"; import sourceMap from "source-map"; import generate from "@babel/generator"; @@ -7,6 +8,7 @@ import generate from "@babel/generator"; import type File from "./file"; export default function generateCode( + pluginPasses: PluginPasses, file: File, ): { outputCode: string, @@ -14,12 +16,33 @@ export default function generateCode( } { const { opts, ast, shebang, code, inputMap } = file; - let gen = generate; - if (opts.generatorOpts && opts.generatorOpts.generator) { - gen = opts.generatorOpts.generator; + const results = []; + for (const plugins of pluginPasses) { + for (const plugin of plugins) { + const { generatorOverride } = plugin; + if (generatorOverride) { + const result = generatorOverride( + ast, + opts.generatorOpts, + code, + generate, + ); + + if (result !== undefined) results.push(result); + } + } } - let { code: outputCode, map: outputMap } = gen(ast, opts.generatorOpts, code); + let result; + if (results.length === 0) { + result = generate(ast, opts.generatorOpts, code); + } else if (results.length === 1) { + result = results[0]; + } else { + throw new Error("More than one plugin attempted to override codegen."); + } + + let { code: outputCode, map: outputMap } = result; if (shebang) { // add back shebang diff --git a/packages/babel-core/src/transformation/index.js b/packages/babel-core/src/transformation/index.js index e72eff1804..ecb2a30b12 100644 --- a/packages/babel-core/src/transformation/index.js +++ b/packages/babel-core/src/transformation/index.js @@ -10,7 +10,7 @@ import normalizeOptions from "./normalize-opts"; import normalizeFile from "./normalize-file"; import generateCode from "./file/generate"; -import File from "./file/file"; +import type File from "./file/file"; export type FileResultCallback = { (Error, null): any, @@ -48,19 +48,24 @@ export function runSync( code: string, ast: ?(BabelNodeFile | BabelNodeProgram), ): FileResult { - const options = normalizeOptions(config); - const input = normalizeFile(options, code, ast); - - const file = new File(options, input); + const file = normalizeFile( + config.passes, + normalizeOptions(config), + code, + ast, + ); transformFile(file, config.passes); - const { outputCode, outputMap } = options.code ? generateCode(file) : {}; + const opts = file.opts; + const { outputCode, outputMap } = opts.code + ? generateCode(config.passes, file) + : {}; return { metadata: file.metadata, - options: options, - ast: options.ast ? file.ast : null, + options: opts, + ast: opts.ast ? file.ast : null, code: outputCode === undefined ? null : outputCode, map: outputMap === undefined ? null : outputMap, }; diff --git a/packages/babel-core/src/transformation/normalize-file.js b/packages/babel-core/src/transformation/normalize-file.js index f264c4eb1f..2b10c05c9e 100644 --- a/packages/babel-core/src/transformation/normalize-file.js +++ b/packages/babel-core/src/transformation/normalize-file.js @@ -1,9 +1,11 @@ // @flow import * as t from "@babel/types"; +import type { PluginPasses } from "../config"; import convertSourceMap, { typeof Converter } from "convert-source-map"; import { parse } from "babylon"; import { codeFrameColumns } from "@babel/code-frame"; +import File from "./file/file"; const shebangRegex = /^#!.*/; @@ -15,10 +17,11 @@ export type NormalizedFile = { }; export default function normalizeFile( + pluginPasses: PluginPasses, options: Object, code: string, ast: ?(BabelNodeFile | BabelNodeProgram), -): NormalizedFile { +): File { code = `${code || ""}`; let shebang = null; @@ -45,35 +48,37 @@ export default function normalizeFile( throw new Error("AST root must be a Program or File node"); } } else { - ast = parser(options, code); + ast = parser(pluginPasses, options, code); } - return { + return new File(options, { code, ast, shebang, inputMap, - }; + }); } -function parser(options, code) { - let parseCode = parse; - - let { parserOpts } = options; - if (parserOpts.parser) { - parseCode = parserOpts.parser; - - parserOpts = Object.assign({}, parserOpts, { - parser: { - parse(source) { - return parse(source, parserOpts); - }, - }, - }); - } - +function parser(pluginPasses, options, code) { try { - return parseCode(code, parserOpts); + const results = []; + for (const plugins of pluginPasses) { + for (const plugin of plugins) { + const { parserOverride } = plugin; + if (parserOverride) { + const ast = parserOverride(code, options.parserOpts, parse); + + if (ast !== undefined) results.push(ast); + } + } + } + + if (results.length === 0) { + return parse(code, options.parserOpts); + } else if (results.length === 1) { + return results[0]; + } + throw new Error("More than one plugin attempted to override parsing."); } catch (err) { const loc = err.loc; if (loc) {