Expose a clear API for plugins to override the parser/generator.

This commit is contained in:
Logan Smyth 2017-11-08 16:37:22 -08:00
parent 52d337e4d9
commit fc7fcfac0a
7 changed files with 87 additions and 143 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,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
@ -196,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, {
@ -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. * Given a plugin/preset item, resolve it into a standard format.
*/ */

View File

@ -8,6 +8,9 @@ import {
type Validator, type Validator,
} from "./option-assertions"; } 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 = { const VALIDATORS: ValidatorSet = {
name: (assertString: Validator<$PropertyType<PluginObject, "name">>), name: (assertString: Validator<$PropertyType<PluginObject, "name">>),
manipulateOptions: (assertFunction: Validator< manipulateOptions: (assertFunction: Validator<
@ -21,6 +24,13 @@ const VALIDATORS: ValidatorSet = {
visitor: (assertVisitorMap: Validator< visitor: (assertVisitorMap: Validator<
$PropertyType<PluginObject, "visitor">, $PropertyType<PluginObject, "visitor">,
>), >),
parserOverride: (assertFunction: Validator<
$PropertyType<PluginObject, "parserOverride">,
>),
generatorOverride: (assertFunction: Validator<
$PropertyType<PluginObject, "generatorOverride">,
>),
}; };
function assertVisitorMap(key: string, value: mixed): VisitorMap { function assertVisitorMap(key: string, value: mixed): VisitorMap {
@ -70,6 +80,9 @@ export type PluginObject = {
inherits?: Function, inherits?: Function,
visitor?: VisitorMap, visitor?: VisitorMap,
parserOverride?: Function,
generatorOverride?: Function,
}; };
export function validatePluginObject(obj: {}): PluginObject { export function validatePluginObject(obj: {}): PluginObject {
@ -90,6 +103,9 @@ export default class Plugin {
pre: Function | void; pre: Function | void;
visitor: {}; visitor: {};
parserOverride: Function | void;
generatorOverride: Function | void;
options: {}; options: {};
constructor(plugin: PluginObject, options: {}, key?: string) { constructor(plugin: PluginObject, options: {}, key?: string) {
@ -99,6 +115,9 @@ export default class Plugin {
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) { function parser(pluginPasses, 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);
},
},
});
}
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) {