Merge pull request #6781 from loganfsmyth/parsegen-plugins
Make official API for plugins to override parser/generator
This commit is contained in:
commit
d90ba531ee
@ -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`,
|
||||
);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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.
|
||||
*/
|
||||
|
||||
@ -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">,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user