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:
- call
this.print(node.id, node)
Here,node.id
istype
forIdentifier
theNode
node.init
If present, callspace
,token
Methods and so on, herenode.init
fortype
forNumericLiteral
theNode
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:
Buffer
Layer maintains core _buf
, provide operations_buf
and_queue
theappend
Methods;Printer
For the middle abstraction layer, call the bottom layerBuffer
Provided in theappend
And so onprint
,printList
Methods;Generator
Provides 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
- Babel basis
- @babel/core
- Babel source parser @babel/parser
- Babel traverse @babel/traverse
- @babel/generator