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`, `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 }; 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) { function standardizeName(type: "plugin" | "preset", name: string) {
// Let absolute and relative paths through. // Let absolute and relative paths through.
if (path.isAbsolute(name)) return name; if (path.isAbsolute(name)) return name;

View File

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

View File

@ -1,7 +1,7 @@
// @flow // @flow
import * as context from "../index"; import * as context from "../index";
import Plugin from "./plugin"; import Plugin, { validatePluginObject } from "./plugin";
import defaults from "lodash/defaults"; import defaults from "lodash/defaults";
import merge from "lodash/merge"; import merge from "lodash/merge";
import buildConfigChain, { type ConfigItem } from "./build-config-chain"; import buildConfigChain, { type ConfigItem } from "./build-config-chain";
@ -12,12 +12,7 @@ import { makeWeakCache } from "./caching";
import { getEnv } from "./helpers/environment"; import { getEnv } from "./helpers/environment";
import { validate, type ValidatedOptions, type PluginItem } from "./options"; import { validate, type ValidatedOptions, type PluginItem } from "./options";
import { import { loadPlugin, loadPreset } from "./loading/files";
loadPlugin,
loadPreset,
loadParser,
loadGenerator,
} from "./loading/files";
type MergeOptions = type MergeOptions =
| ConfigItem | ConfigItem
@ -28,15 +23,6 @@ type MergeOptions =
dirname: string, dirname: string,
}; };
const ALLOWED_PLUGIN_KEYS = new Set([
"name",
"manipulateOptions",
"pre",
"post",
"visitor",
"inherits",
]);
export default function manageOptions(opts: {}): { export default function manageOptions(opts: {}): {
options: Object, options: Object,
passes: Array<Array<Plugin>>, passes: Array<Array<Plugin>>,
@ -205,7 +191,7 @@ const loadConfig = makeWeakCache((config: MergeOptions): {
plugins: Array<BasicDescriptor>, plugins: Array<BasicDescriptor>,
presets: Array<BasicDescriptor>, presets: Array<BasicDescriptor>,
} => { } => {
const options = normalizeOptions(config); const options = config.options;
const plugins = (config.options.plugins || []).map((plugin, index) => const plugins = (config.options.plugins || []).map((plugin, index) =>
createDescriptor(plugin, loadPlugin, config.dirname, { createDescriptor(plugin, loadPlugin, config.dirname, {
@ -275,37 +261,16 @@ function loadPluginDescriptor(descriptor: BasicDescriptor): Plugin {
} }
const instantiatePlugin = makeWeakCache( const instantiatePlugin = makeWeakCache(
( ({ value, options, dirname, alias }: LoadedDescriptor, cache): Plugin => {
{ value: pluginObj, options, dirname, alias }: LoadedDescriptor, const pluginObj = validatePluginObject(value);
cache,
): Plugin => { const plugin = Object.assign({}, pluginObj);
Object.keys(pluginObj).forEach(key => { if (plugin.visitor) {
if (!ALLOWED_PLUGIN_KEYS.has(key)) { plugin.visitor = traverse.explode(clone(plugin.visitor));
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.",
);
} }
const plugin = Object.assign({}, pluginObj, {
visitor: clone(pluginObj.visitor || {}),
});
traverse.explode(plugin.visitor);
let inheritsDescriptor;
let inherits;
if (plugin.inherits) { if (plugin.inherits) {
inheritsDescriptor = { const inheritsDescriptor = {
alias: `${alias}$inherits`, alias: `${alias}$inherits`,
value: plugin.inherits, value: plugin.inherits,
options, options,
@ -313,7 +278,7 @@ const instantiatePlugin = makeWeakCache(
}; };
// If the inherited plugin changes, reinstantiate this plugin. // If the inherited plugin changes, reinstantiate this plugin.
inherits = cache.invalidate(() => const inherits = cache.invalidate(() =>
loadPluginDescriptor(inheritsDescriptor), loadPluginDescriptor(inheritsDescriptor),
); );
@ -324,8 +289,8 @@ const instantiatePlugin = makeWeakCache(
plugin.manipulateOptions, plugin.manipulateOptions,
); );
plugin.visitor = traverse.visitors.merge([ plugin.visitor = traverse.visitors.merge([
inherits.visitor, inherits.visitor || {},
plugin.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. * Given a plugin/preset item, resolve it into a standard format.
*/ */

View File

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

View File

@ -1,43 +1,123 @@
// @flow // @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 { export default class Plugin {
key: ?string; key: ?string;
manipulateOptions: ?Function; manipulateOptions: Function | void;
post: ?Function; post: Function | void;
pre: ?Function; pre: Function | void;
visitor: ?{}; visitor: {};
parserOverride: Function | void;
generatorOverride: Function | void;
options: {}; options: {};
constructor(plugin: {}, options: {}, key?: string) { constructor(plugin: PluginObject, 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");
}
this.key = plugin.name || key; this.key = plugin.name || key;
this.manipulateOptions = plugin.manipulateOptions; this.manipulateOptions = plugin.manipulateOptions;
this.post = plugin.post; this.post = plugin.post;
this.pre = plugin.pre; this.pre = plugin.pre;
this.visitor = plugin.visitor; this.visitor = plugin.visitor || {};
this.parserOverride = plugin.parserOverride;
this.generatorOverride = plugin.generatorOverride;
this.options = options; this.options = options;
} }
} }

View File

@ -1,5 +1,6 @@
// @flow // @flow
import type { PluginPasses } from "../../config";
import convertSourceMap, { type SourceMap } from "convert-source-map"; import convertSourceMap, { type SourceMap } from "convert-source-map";
import sourceMap from "source-map"; import sourceMap from "source-map";
import generate from "@babel/generator"; import generate from "@babel/generator";
@ -7,6 +8,7 @@ import generate from "@babel/generator";
import type File from "./file"; import type File from "./file";
export default function generateCode( export default function generateCode(
pluginPasses: PluginPasses,
file: File, file: File,
): { ): {
outputCode: string, outputCode: string,
@ -14,12 +16,33 @@ export default function generateCode(
} { } {
const { opts, ast, shebang, code, inputMap } = file; const { opts, ast, shebang, code, inputMap } = file;
let gen = generate; const results = [];
if (opts.generatorOpts && opts.generatorOpts.generator) { for (const plugins of pluginPasses) {
gen = opts.generatorOpts.generator; 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) { if (shebang) {
// add back shebang // add back shebang

View File

@ -10,7 +10,7 @@ import normalizeOptions from "./normalize-opts";
import normalizeFile from "./normalize-file"; import normalizeFile from "./normalize-file";
import generateCode from "./file/generate"; import generateCode from "./file/generate";
import File from "./file/file"; import type File from "./file/file";
export type FileResultCallback = { export type FileResultCallback = {
(Error, null): any, (Error, null): any,
@ -48,19 +48,24 @@ export function runSync(
code: string, code: string,
ast: ?(BabelNodeFile | BabelNodeProgram), ast: ?(BabelNodeFile | BabelNodeProgram),
): FileResult { ): FileResult {
const options = normalizeOptions(config); const file = normalizeFile(
const input = normalizeFile(options, code, ast); config.passes,
normalizeOptions(config),
const file = new File(options, input); code,
ast,
);
transformFile(file, config.passes); 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 { return {
metadata: file.metadata, metadata: file.metadata,
options: options, options: opts,
ast: options.ast ? file.ast : null, ast: opts.ast ? file.ast : null,
code: outputCode === undefined ? null : outputCode, code: outputCode === undefined ? null : outputCode,
map: outputMap === undefined ? null : outputMap, map: outputMap === undefined ? null : outputMap,
}; };

View File

@ -1,9 +1,11 @@
// @flow // @flow
import * as t from "@babel/types"; import * as t from "@babel/types";
import type { PluginPasses } from "../config";
import convertSourceMap, { typeof Converter } from "convert-source-map"; import convertSourceMap, { typeof Converter } from "convert-source-map";
import { parse } from "babylon"; import { parse } from "babylon";
import { codeFrameColumns } from "@babel/code-frame"; import { codeFrameColumns } from "@babel/code-frame";
import File from "./file/file";
const shebangRegex = /^#!.*/; const shebangRegex = /^#!.*/;
@ -15,10 +17,11 @@ export type NormalizedFile = {
}; };
export default function normalizeFile( export default function normalizeFile(
pluginPasses: PluginPasses,
options: Object, options: Object,
code: string, code: string,
ast: ?(BabelNodeFile | BabelNodeProgram), ast: ?(BabelNodeFile | BabelNodeProgram),
): NormalizedFile { ): File {
code = `${code || ""}`; code = `${code || ""}`;
let shebang = null; let shebang = null;
@ -45,35 +48,37 @@ export default function normalizeFile(
throw new Error("AST root must be a Program or File node"); throw new Error("AST root must be a Program or File node");
} }
} else { } else {
ast = parser(options, code); ast = parser(pluginPasses, options, code);
} }
return { return new File(options, {
code, code,
ast, ast,
shebang, shebang,
inputMap, 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 { 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) { } catch (err) {
const loc = err.loc; const loc = err.loc;
if (loc) { if (loc) {