Merge pull request #6781 from loganfsmyth/parsegen-plugins

Make official API for plugins to override parser/generator
This commit is contained in:
Logan Smyth 2017-11-09 10:45:12 -08:00 committed by GitHub
commit d90ba531ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 195 additions and 218 deletions

View File

@ -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`,
);
}

View File

@ -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;

View File

@ -12,6 +12,12 @@ import type {
RootInputSourceMapOption,
} from "./options";
export type ValidatorSet = {
[string]: Validator<any>,
};
export type Validator<T> = (string, mixed) => T;
export function assertSourceMaps(
key: string,
value: mixed,

View File

@ -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";
@ -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
@ -28,15 +23,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<Array<Plugin>>,
@ -205,7 +191,7 @@ const loadConfig = makeWeakCache((config: MergeOptions): {
plugins: Array<BasicDescriptor>,
presets: Array<BasicDescriptor>,
} => {
const options = normalizeOptions(config);
const options = config.options;
const plugins = (config.options.plugins || []).map((plugin, index) =>
createDescriptor(plugin, loadPlugin, config.dirname, {
@ -275,37 +261,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 +278,7 @@ const instantiatePlugin = makeWeakCache(
};
// If the inherited plugin changes, reinstantiate this plugin.
inherits = cache.invalidate(() =>
const inherits = cache.invalidate(() =>
loadPluginDescriptor(inheritsDescriptor),
);
@ -324,8 +289,8 @@ const instantiatePlugin = makeWeakCache(
plugin.manipulateOptions,
);
plugin.visitor = traverse.visitors.merge([
inherits.visitor,
plugin.visitor,
inherits.visitor || {},
plugin.visitor || {},
]);
}
@ -351,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.
*/

View File

@ -12,14 +12,10 @@ import {
assertSourceMaps,
assertCompact,
assertSourceType,
type ValidatorSet,
type Validator,
} from "./option-assertions";
type ValidatorSet = {
[string]: Validator<any>,
};
type Validator<T> = (string, mixed) => T;
const ROOT_VALIDATORS: ValidatorSet = {
filename: (assertString: Validator<
$PropertyType<ValidatedOptions, "filename">,

View File

@ -1,43 +1,123 @@
// @flow
// $FlowIssue recursion?
import {
assertString,
assertFunction,
assertObject,
type ValidatorSet,
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<PluginObject, "name">>),
manipulateOptions: (assertFunction: Validator<
$PropertyType<PluginObject, "manipulateOptions">,
>),
pre: (assertFunction: Validator<$PropertyType<PluginObject, "pre">>),
post: (assertFunction: Validator<$PropertyType<PluginObject, "post">>),
inherits: (assertFunction: Validator<
$PropertyType<PluginObject, "inherits">,
>),
visitor: (assertVisitorMap: Validator<
$PropertyType<PluginObject, "visitor">,
>),
parserOverride: (assertFunction: Validator<
$PropertyType<PluginObject, "parserOverride">,
>),
generatorOverride: (assertFunction: Validator<
$PropertyType<PluginObject, "generatorOverride">,
>),
};
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,
parserOverride?: Function,
generatorOverride?: Function,
};
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: {};
parserOverride: Function | void;
generatorOverride: Function | void;
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.parserOverride = plugin.parserOverride;
this.generatorOverride = plugin.generatorOverride;
this.options = options;
}
}

View File

@ -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

View File

@ -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,
};

View File

@ -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;
function parser(pluginPasses, options, code) {
try {
const results = [];
for (const plugins of pluginPasses) {
for (const plugin of plugins) {
const { parserOverride } = plugin;
if (parserOverride) {
const ast = parserOverride(code, options.parserOpts, parse);
let { parserOpts } = options;
if (parserOpts.parser) {
parseCode = parserOpts.parser;
parserOpts = Object.assign({}, parserOpts, {
parser: {
parse(source) {
return parse(source, parserOpts);
},
},
});
if (ast !== undefined) results.push(ast);
}
}
}
try {
return parseCode(code, parserOpts);
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) {