How to add an import to the file with Babel

Man*_*uel 8 babeljs

Say you have a file with:

AddReactImport();
Run Code Online (Sandbox Code Playgroud)

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
        }
      }
    }
  };
}
Run Code Online (Sandbox Code Playgroud)

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.

Man*_*uel 9

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);
      }
    }
  };
}
Run Code Online (Sandbox Code Playgroud)


Dom*_*omi 7

如果要注入代码,只需使用@babel/template为其生成 AST 节点即可;然后根据需要注入它。

序言:Babel 文档不是最好的

我也同意,即使在 2020 年,信息也很少。我得到了我的大部分信息的通过实际工作babel的源代码,看着所有的工具(types,traverse,path,code-frame等...),他们使用的帮手,现有插件(例如istanbul到有点了解的JS基本仪器), webpackbabel-loader和更多...

例如:(unshiftContainer实际上,babel-traverse一般来说)没有官方文档,但您可以在此处找到它的源代码(令人着迷的是,它接受单个节点或节点数组!)

策略#1(更新版本)

在这种特殊情况下,我会:

  1. 创建一个 @babel/template
  2. 在我的插件开始时准备一次 AST
  3. Program仅当找到特定的函数调用时才将其注入(即根路径)一次

注意:模板也支持变量。如果您想包装现有节点或想根据上下文生成相同代码的轻微变化,则非常有用。

代码(使用策略#1)

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);
        }
      }
    }
  };
};
Run Code Online (Sandbox Code Playgroud)

策略#2(旧版本)

另一种选择是:

  1. 使用实用函数从源 ( parseSource)生成 AST
  2. 在我的插件开始时准备一次 AST
  3. Program仅当找到特定的函数调用时才将其注入(即根路径)一次

代码(使用策略#2)

与上面相同,但使用您自己的编译器函数(效率不如@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;
}
Run Code Online (Sandbox Code Playgroud)

可能的陷阱

  1. 当注入/替换新节点等时,babel 将再次在其上运行所有插件。这就是为什么您的第一个检测插件可能会遇到赌注的无限循环:您想记住而不是重新访问以前访问过的节点(我正在使用 a Set)。
  2. 包装节点时情况会变得更糟。包裹(例如用@babel/template)的节点实际上是副本,而不是原始节点。在这种情况下,您要记住它已被检测并跳过它以防您再次遇到它,或者再次:无限循环!
  3. 如果您不想检测由任何插件(不仅仅是您的)发出的节点,即您只想对原始源代码进行操作,您可以通过检查它们是否具有loc属性来跳过它们(注入的节点通常没有loc财产)。
  4. 在您的情况下,您正在尝试添加一个import语句,如果没有启用正确的插件或没有将程序类型设置为module.