Manuel
Manuel

Reputation: 11469

How to add an import to the file with Babel

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

Answers (3)

conartist6
conartist6

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

Domi
Domi

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.

Preamble: Babel documentation is not the best

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!)

Strategy #1 (updated version)

In this particular case, I would:

  1. Create a @babel/template
  2. prepare that AST once at the start of my plugin
  3. inject it into Program (i.e. the root path) once, only if the particular function call has been found

NOTE: 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.

Code (using Strategy #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);
        }
      }
    }
  };
};

Strategy #2 (old version)

An alternative is:

  1. use a utility function to generate an AST from source (parseSource)
  2. prepare that AST once at the start of my plugin
  3. inject it into Program (i.e. the root path) once, only if the particular function call has been found

Code (using Strategy #2)

Same 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;
}

Possible Pitfalls

  1. When a new node is injected/replaced etc, babel will run all plugins on them again. This is why your first instrumentation plugin is likely to encounter an infinite loop right of the bet: you want to remember and not re-visit previously visited nodes (I'm using a Set for that).
  2. It gets worse when wrapping nodes. Nodes wrapped (e.g. with @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 💥!
  3. If you don't want to instrument nodes that have been emitted by any plugin (not just yours), that is you want to only operate on the original source code, you can skip them by checking whether they have a loc property (injected nodes usually do not have a loc property).
  4. In your case, you are trying to add an import statement which won't always work without the right plugins enabled or without program-type set to module.

Upvotes: 9

Manuel
Manuel

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

Related Questions