A, start

@babel/core, @babel/parser, @babel/traverse. Let’s look at @babel/ Generator.

This library is relatively simple, generating code mainly from the AST, and this article will cover the core logic and give a simple example.

2. API description

Take a look at the official @babel/ Generator documentation to see how the API is used:

import { parse } from "@babel/parser";
import generate from "@babel/generator";

const code = "class Example {}";
const ast = parse(code);

const output = generate(
  ast,
  {
    /* options */
  },
  code
);
Copy the code

Three, source code analysis

The @babel/ Generator version for this analysis is V7.16.0.

Structure of 1.

The directory structure of @babel/generator is as follows:

- generators // There are different generation modes for different types of nodes
  - base.js
  - class.js
  - expression.js
  ...
- node // This is mainly the judgment of Spaces and parentheses
  - index.js
  - parentheses.js
  - whitespace.js
- buffer.js  // The Buffer class, which stores the final return information, contains the method to operate _buf
- printer.js // Printer class, middle layer, containing print method
- index.js / / the Generator class, inherited from the Printer, export the generate method
Copy the code

2. Core ideas

@babel/ Generator maintains an internal _buf, which is the code string. Iterate through the AST, adding or changing the _buf depending on the type of Node, and return it.

3. Operation mechanism

Here’s a simple example:

const { parse } = require("@babel/parser");
const generate = require("@babel/generator").default;

const a = "const a = 1;";
const ast = parse(a);
const code = generate(ast)

console.log('code', code)
Copy the code

Get the AST with @babel/ Parser and call @babel/ Generator generate.

This AST is mainly composed of File, Program, VariableDeclarator, VariableDeclarator, and NumericLiteral nodes.

{
  "type": "File"."start": 0."end": 11."program": {
    "type": "Program"."start": 0."end": 11."sourceType": "module"."interpreter": null."body": [{"type": "VariableDeclaration"."start": 0."end": 11."declarations": [{"type": "VariableDeclarator"."start": 6."end": 11."id": {
              "type": "Identifier"."start": 6."end": 7."name": "a"
            },
            "init": {
              "type": "NumericLiteral"."start": 10."end": 11."extra": {
                "rawValue": 1."raw": "1"
              },
              "value": 1}}]."kind": "const"}],}.}Copy the code

After calling generate, instantiate a Generator and then call the generate method on it. It also calls the super.generate method, as defined in the Printer class.

export default function generate(ast: t.Node, opts? : GeneratorOptions, code? : string | { [filename: string]: string },) :any {
  const gen = new Generator(ast, opts, code);
  return gen.generate();
}   


class Generator extends Printer {
  generate() {
    return super.generate(this.ast); }}class Printer {
  generate(ast) {
    this.print(ast);
    this._maybeAddAuxComment();

    return this._buf.get(); }}Copy the code

PrintMethod = printMethod = printMethod = printMethod = printMethod = printMethod = printMethod = printMethod

class Printer {
  print(node, parent?) {
    if(! node)return;

    const oldConcise = this.format.concise;
    if (node._compact) {
      this.format.concise = true;
    }

    const printMethod = this[node.type];
    // ...

    const loc = isProgram(node) || isFile(node) ? null : node.loc;
    this.withSource("start", loc, () = > {
      printMethod.call(this, node, parent);
    });
    // ...}}Copy the code

This File function comes from the generators directory and is used for different types of Node.

import * as generatorFunctions from "./generators";

Object.assign(Printer.prototype, generatorFunctions);
Copy the code

This. WithSource is then called, and printMethod is called in its callback. The withSource method mainly deals with sourcemap generation. If sourcemAP generation is not needed, cb is directly called, in this case File function.

class Printer {
    withSource(prop: string, loc: any, cb: () = > void) :void {
    this._catchUp(prop, loc);

    this._buf.withSource(prop, loc, cb); }}class Buffer {
  withSource(prop: string, loc: t.SourceLocation, cb: () = > void) :void {
  if (!this._map) return cb();
  // ...}}Copy the code

The File function to judge whether the node. The program, so, execute this. Print (node. The program. The interpreter, the node), has returned to the print method. Our example has Node. program, but Node.program. interpreter is null, so re-entering the print method does nothing and returns directly.

This. Print (node.program, node).

export function File(this: Printer, node: t.File) {
  if (node.program) {
    this.print(node.program.interpreter, node);
  }

  this.print(node.program, node);
}

export function Program(this: Printer, node: t.Program) {
  this.printInnerComments(node, false);

  this.printSequence(node.directives, node);
  if (node.directives && node.directives.length) this.newline();

  this.printSequence(node.body, node);
}
Copy the code

There is no cache property on node this time, so this.printSequence(node.body, node) will be called. We don’t use the print method because Node. body is an array type.

The printJoin method is called in printSequence, which iterates over the nodes passed in, calling the _printNewline and this.print methods.

Re-entering print calls the VariableDeclaration method.

class Printer {
  printSequence(nodes, parent, opts: { statement? : boolean; indent? : boolean; addNewlines? :Function;
    } = {},
  ) {
    opts.statement = true;
    return this.printJoin(nodes, parent, opts);
  }

  printJoin(nodes: Array<any> | undefined | null, parent: any, opts: any = {}) {
    if(! nodes? .length)return;

    if (opts.indent) this.indent();

    const newlineOpts = {
      addNewlines: opts.addNewlines,
    };

    for (let i = 0; i < nodes.length; i++) {
      const node = nodes[i];
      if(! node)continue;

      if (opts.statement) this._printNewline(true, node, parent, newlineOpts);

      this.print(node, parent);

      if (opts.iterator) {
        opts.iterator(node, i);
      }

      if (opts.separator && i < nodes.length - 1) {
        opts.separator.call(this);
      }

      if (opts.statement) this._printNewline(false, node, parent, newlineOpts);
    }

    if (opts.indent) this.dedent(); }}Copy the code

For generators/statement.js, the VariableDeclaration method will call this.word(node.kind) first, and the word method will call the _append method in Buffer. Add a character — node.kind — to _buf, which is const.

Then call this.space() to add a space.

Then call this.printList(node.declarations, node, {separator}.

export function VariableDeclaration(
  this: Printer,
  node: t.VariableDeclaration,
  parent: t.Node,
) {
  if (node.declare) {
    this.word("declare");
    this.space();
  }

  this.word(node.kind);
  this.space();

  let hasInits = false;
  if(! isFor(parent)) {for (const declar of node.declarations as Array<any>) {
      if (declar.init) {
        hasInits = true; }}}let separator;
  if (hasInits) {
    separator =
      node.kind === "const"
        ? constDeclarationIndent
        : variableDeclarationIndent;
  }

  this.printList(node.declarations, node, { separator });

  if (isFor(parent)) {
    if (isForStatement(parent)) {
      if (parent.init === node) return;
    } else {
      if (parent.left === node) return; }}this.semicolon();
}

class Printer {
  word(str: string): void {
    if (
      this._endsWithWord ||
      (this.endsWith(charCodes.slash) && str.charCodeAt(0) === charCodes.slash)
    ) {
      this._space();
    }

    this._maybeAddAuxComment();
    this._append(str);

    this._endsWithWord = true;
  }

  _append(str: string, queue: boolean = false) {
    this._maybeAddParen(str);
    this._maybeIndent(str);

    if (queue) this._buf.queue(str);
    else this._buf.append(str);

    this._endsWithWord = false;
    this._endsWithInteger = false; }}class Buffer {
  append(str: string): void {
    this._flush();
    const { line, column, filename, identifierName, force } =
      this._sourcePosition;
    this._append(str, line, column, identifierName, filename, force);
  }

  _append(
    str: string,
    line: number,
    column: number, identifierName? : string |null, filename? : string |null, force? : boolean, ):void {
    this._buf += str;
    this._last = str.charCodeAt(str.length - 1);

    let i = str.indexOf("\n");
    let last = 0;

    if(i ! = =0) {
      this._mark(line, column, identifierName, filename, force);
    }

    while(i ! = = -1) {
      this._position.line++;
      this._position.column = 0;
      last = i + 1;

      if (last < str.length) {
        this._mark(++line, 0, identifierName, filename, force);
      }
      i = str.indexOf("\n", last);
    }
    this._position.column += str.length - last; }}Copy the code

Separator the difference between printList and printSequence is that printList provides separator. .

Node.declarations is iterated over, calling the print method. In the example, there is only one element in Node. declarations that has type VariableDeclarator, that is, the VariableDeclarator method is called.

class Printer {
  printList(items, parent, opts: { separator? :Function; indent? : boolean; statement? : boolean } = {},) {
    if (opts.separator == null) {
      opts.separator = commaSeparator;
    }

    return this.printJoin(items, parent, opts); }}Copy the code

The main logic of VariableDeclarator method is as follows:

  • callthis.print(node.id, node)Here,node.idistypeforIdentifiertheNode
  • node.initIf present, callspace,tokenMethods and so on, herenode.initfortypeforNumericLiteraltheNode
export function VariableDeclarator(this: Printer, node: t.VariableDeclarator) {
  this.print(node.id, node);
  if (node.definite) this.token("!"); // TS
  this.print(node.id.typeAnnotation, node);
  if (node.init) {
    this.space();
    this.token("=");
    this.space();
    this.print(node.init, node); }}Copy the code

We already know that the print method mainly calls a function with the same name as Node. type, which calls the Identifier function and the NumericLiteral function, and finally adds a = 1 to _buf

export function Identifier(this: Printer, node: t.Identifier) {
  this.exactSource(node.loc, () = > {
    this.word(node.name);
  });
}

export function NumericLiteral(this: Printer, node: t.NumericLiteral) {
  const raw = this.getPossibleRaw(node);
  const opts = this.format.jsescOption;
  const value = node.value + "";
  if (opts.numbers) {
    this.number(jsesc(node.value, opts));
  } else if (raw == null) {
    this.number(value); // normalize
  } else if (this.format.minified) {
    this.number(raw.length < value.length ? raw : value);
  } else {
    this.number(raw); }}Copy the code

Finally, call this._buf.get() to return code, map and other information.

class Buffer {
  get(): any {
    this._flush();

    const map = this._map;
    const result = {
      code: this._buf.trimRight(),
      map: null.rawMappings: map? .getRawMappings(), };if (map) {
      Object.defineProperty(result, "map", {
        configurable: true.enumerable: true.get() {
          return (this.map = map.get());
        },
        set(value) {
          Object.defineProperty(this."map", { value, writable: true}); }}); }returnresult; }}Copy the code

The flow chart of 4.

Four,

This article briefly introduces the main logic of @babel/ Generator, which can be viewed as a layered architecture:

  • BufferLayer maintains core _buf, provide operations_bufand_queuetheappendMethods;
  • PrinterFor the middle abstraction layer, call the bottom layerBufferProvided in theappendAnd so onprint,printListMethods;
  • GeneratorProvides external apis for the top layer.

The idea of generating code is to analyze Node.type from the top-level File and Program, recursively call the corresponding generating function, continuously increase or modify _buf, and finally return.

Series of articles

  1. Babel basis
  2. @babel/core
  3. Babel source parser @babel/parser
  4. Babel traverse @babel/traverse
  5. @babel/generator