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.
First clean up the config files you created, and make sure you have babel-plugin-import installed. This will give you a config folder with 2 webpack config files for dev/prod environments. Open those files and locate the place where you need to insert the plugins property as documented on the instructions page.
Simply add a "scripts" field to your package. json and put the babel command inside there as build . This will run Babel the same way as before and the output will be present in lib directory, only now we are using a local copy. Alternatively, you can reference the babel cli inside of node_modules .
babelrc file is your local configuration for your code in your project. Generally you would put it in the root of your application repo. It will affect all files that Babel processes that are in the same directory or in sibling directories of the . babelrc .
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);
}
}
};
}
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
.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With