export default function({types: t}) { return { visitor: {} }; }
visitor
object whose name corresponds to the type of the abstract syntax tree (ASD) node, for example, FunctionDeclaration
or StringLiteral
( full list ), to which the path (path) to the node is passed. We are interested in nodes of type Identifier
.
export default function({types: t}) { return { visitor: { Identifier(path, {opts: options}) { } } }; }
.opts
property of the second argument. Through them we will pass the variable names and paths to the modules for which the import will be created. It will look like this:
{ plugins: [[ "babel-plugin-auto-import", { declarations: [{name: "React", path: "react"}] } ]] }
.["is" + ]()
. For example, path.isIdentifier()
. The path can be searched among child paths using the .find(callback)
method, and among parent paths using the .findParent(callback)
method. The .parentPath
property stores a link to the parent path.
Identifier
type is widely used in various types of nodes. We need only some of them. Suppose we have this code:
React.Component
{ type: "MemberExpression", object: { type: "Identifier", name: "React" }, property: { type: "Identifier", name: "Component" }, computed: false }
.type
property and some other properties specific to each type. Consider the root node - MemberExpression
. He has three properties. Object
is the expression to the left of the point. In this case, it is an identifier. The computed
property indicates whether there is an identifier or some expression on the right, for example, x["a" + b]
. Property
- actually, that to the right of a point.
Identifier
method will be called twice: for React
and Component
identifiers, respectively. The plugin should handle the React
ID, but skip the Component
ID. To do this, the identifier path must receive the parent path and, if it is a node of type MemberExpression
, check whether the identifier is a .object
property. We put the check into a separate function:
export default function({types: t}) { return { visitor: { Identifier(path, {opts: options}) { if (!isCorrectIdentifier(path)) return; } } }; function isCorrectIdentifier(path) { let {parentPath} = path; if (parentPath.isMemberExpression() && parentPath.get("object") == path) return true; } }
function isCorrectIdentifier(path) { let {parentPath} = path; if (parentPath.isArrayExpression()) return true; else if (parentPath.isArrowFunctionExpression()) return true; else if (parentPath.isAssignmentExpression() && parentPath.get("right") == path) return true; else if (parentPath.isAwaitExpression()) return true; else if (parentPath.isBinaryExpression()) return true; else if (parentPath.bindExpression && parentPath.bindExpression()) return true; else if (parentPath.isCallExpression()) return true; else if (parentPath.isClassDeclaration() && parentPath.get("superClass") == path) return true; else if (parentPath.isClassExpression() && parentPath.get("superClass") == path) return true; else if (parentPath.isConditionalExpression()) return true; else if (parentPath.isDecorator()) return true; else if (parentPath.isDoWhileStatement()) return true; else if (parentPath.isExpressionStatement()) return true; else if (parentPath.isExportDefaultDeclaration()) return true; else if (parentPath.isForInStatement()) return true; else if (parentPath.isForStatement()) return true; else if (parentPath.isIfStatement()) return true; else if (parentPath.isLogicalExpression()) return true; else if (parentPath.isMemberExpression() && parentPath.get("object") == path) return true; else if (parentPath.isNewExpression()) return true; else if (parentPath.isObjectProperty() && parentPath.get("value") == path) return !parentPath.node.shorthand; else if (parentPath.isReturnStatement()) return true; else if (parentPath.isSpreadElement()) return true; else if (parentPath.isSwitchStatement()) return true; else if (parentPath.isTaggedTemplateExpression()) return true; else if (parentPath.isThrowStatement()) return true; else if (parentPath.isUnaryExpression()) return true; else if (parentPath.isVariableDeclarator() && parentPath.get("init") == path) return true; return false; }
scope
. With it, we will iterate all scopes, starting with the current. The variables for the current scope are in the .bindings
property. The link to the parent scope is in the .parent
property. It remains to recursively go through all the variables of all scopes and check if our identifier is there.
export default function({types: t}) { return { visitor: { Identifier(path, {opts: options}) { if (!isCorrectIdentifier(path)) return; let {node: identifier, scope} = path; if (isDefined(identifier, scope)) return; } } }; // ... function isDefined(identifier, {bindings, parent}) { let variables = Object.keys(bindings); if (variables.some(has, identifier)) return true; return parent ? isDefined(identifier, parent) : false; } function has(identifier) { let {name} = this; return identifier == name; } }
options
from the declaration of “global” variables and process them:
let {declarations} = options; declarations.some(declaration => { if (declaration.name == identifier.name) { let program = path.findParent(path => path.isProgram()); insertImport(program, declaration); return true; } });
.reduce
method to get an array with paths like ImportDeclaration
:
function insertImport(program, { name, path }) { let programBody = program.get("body"); let currentImportDeclarations = programBody.reduce(currentPath => { if (currentPath.isImportDeclaration()) list.push(currentPath); return list; }, []); }
let importDidAppend = currentImportDeclarations.some(({node: importDeclaration}) => { if (importDeclaration.source.value == path) { return importDeclaration.specifiers.some(specifier => specifier.local.name == name); } });
t
. For each of the nodes has its own method. We need to create importDeclaration
. We look at the documentation and see that creating import requires specifiers (that is, the names of the variables being imported) and the path to the module.
export default ...
). Then create a node with a path to the module. This is a simple StringLiteral
.
let specifier = t.importDefaultSpecifier(t.identifier(name)); let pathToModule = t.stringLiteral(path);
let importDeclaration = t.importDeclaration([specifier], pathToModule);
.replaceWith(node)
method, or an array of nodes using the .replaceWithMultiple([...nodes])
method .replaceWithMultiple([...nodes])
. Can be removed using the .remove()
method. For insertion, the .insertBefore(node)
and .insertAfter(node)
methods are used to insert a node before or after the path, respectively.
program
node has a property .body
, which contains an array of expressions representing the program. To insert nodes into such “container” arrays, paths have special methods pushContainer
and unshiftContainer
. We use the latter:
program.unshiftContainer("body", importNode);
Source: https://habr.com/ru/post/330018/