Reputation: 11469
Say you have a file with:
AddReactImport();
And the plugin:
export default function ({types: t }) {
return {
visitor: {
CallExpression(p) {
if (p.node.callee.name === "AddReactImport") {
// add import if it's not there
}
}
}
};
}
How do you add import React from 'react';
at the top of the file/tree if it's not there already.
I think more important than the answer is how you find out how to do it. Please tell me because I'm having a hard time finding info sources on how to develop Babel plugins. My sources right now are: Plugin Handbook,Babel Types, AST Spec, this blog post, and the AST explorer. It feels like using an English-German dictionary to try to speak German.
Upvotes: 10
Views: 5734
Reputation: 447
I believe there's an even better way now: babel-helper-module-imports
For you the code would be
import { addDefault } from "@babel/helper-module-imports";
addDefault(path, 'React', { nameHint: "React" })
Upvotes: 2
Reputation: 24598
If you want to inject code, just use @babel/template
to generate the AST node for it; then inject it as you need to.
I also agree that, even in 2020, information is sparse. I am getting most of my info by actually working through the babel
source code, looking at all the tools (types
, traverse
, path
, code-frame
etc...), the helpers they use, existing plugins (e.g. istanbul
to learn a bit about basic instrumentation in JS), the webpack babel-loader
and more...
For example: unshiftContainer
(and actually, babel-traverse
in general) has no official documentation, but you can find it's source code here (fascinatingly enough, it accepts either a single node or an array of nodes!)
In this particular case, I would:
@babel/template
Program
(i.e. the root path) once, only if the particular function call has been foundNOTE: Templates also support variables. Very useful if you want to wrap existing nodes or want to produce slight variations of the same code, depending on context.
import template from "@babel/template";
// template
const buildImport = template(`
import React from 'react';
`);
// plugin
const plugin = function () {
const importDeclaration = buildImport();
let imported = false;
let root;
return {
visitor: {
Program(path) {
root = path;
},
CallExpression(path) {
if (!imported && path.node.callee.name === "AddMyImport") {
// add import if it's not there
imported = true;
root.unshiftContainer('body', importDeclaration);
}
}
}
};
};
An alternative is:
parseSource
)Program
(i.e. the root path) once, only if the particular function call has been foundSame as above but with your own compiler function (not as efficient as @babel/template
):
/**
* Helper: Generate AST from source through `@babel/parser`.
* Copied from somewhere... I think it was `@babel/traverse`
* @param {*} source
*/
export function parseSource(source) {
let ast;
try {
source = `${source}`;
ast = parse(source);
} catch (err) {
const loc = err.loc;
if (loc) {
err.message +=
"\n" +
codeFrameColumns(source, {
start: {
line: loc.line,
column: loc.column + 1,
},
});
}
throw err;
}
const nodes = ast.program.body;
nodes.forEach(n => traverse.removeProperties(n));
return nodes;
}
Set
for that).@babel/template
) are actually copies, not the original node. In that case, you want to remember that it is instrumented and skip it in case you come across it again, or, again: infinite loop 💥!loc
property (injected nodes usually do not have a loc
property).import
statement which won't always work without the right plugins enabled or without program-type set to module
.Upvotes: 9
Reputation: 11469
export default function ({types: t }) {
return {
visitor: {
Program(path) {
const identifier = t.identifier('React');
const importDefaultSpecifier = t.importDefaultSpecifier(identifier);
const importDeclaration = t.importDeclaration([importDefaultSpecifier], t.stringLiteral('react'));
path.unshiftContainer('body', importDeclaration);
}
}
};
}
Upvotes: 11