The relevant code for this article is saved on Github source code. It is recommended that you read the article with this code and eat it easily.

preface

Today, while developing cli tools, I encountered a situation where after adding a sentry to a project by command, two lines of TS code were automatically added to the shared.module.ts file to introduce dependencies. As follows:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
import { COMPONENTS } from './components';
import { DIRECTIVES } from './directives';
import { ZorroModule } from '@modules/zorro/zorro.module';
import { PIPES } from './pipes';
import { SENTRY_PROVIDERS } from '@core/sentry'; // Code to add

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    RouterModule,
    TranslateModule,
    ReactiveFormsModule,
    ZorroModule,
  ],
  declarations: [...COMPONENTS, ...DIRECTIVES, ...PIPES],
  exports: [
    CommonModule,
    FormsModule,
    RouterModule,
    TranslateModule,
    ReactiveFormsModule,
    ZorroModule,
    ...COMPONENTS,
    ...DIRECTIVES,
    ...PIPES,
  ],
  providers: [SENTRY_PROVIDERS], // Code to add
})
class SharedModule {}

export { SharedModule };
Copy the code

At first we used the regular method to handle this, but in the process of reviewing the code, the big guy said that this method is too risky, and suggested to use Babel to handle this situation. Since Babel had only been used for simple compatibility processing before, and had never been used to generate code, I used my spare time to explore. This article mainly summarizes the use method of several core plug-ins of Babel and some skills in the development, hoping to give you some help.

Babel is introduced

When you think of Babel, the first thing that comes to mind is its compatibility approach. It converts ECMAScript 2015+ version code into backward-compatible JavaScript syntax so it can run in current and older browsers or other environments. But here, we’ll focus on how to use Babel to generate the javascript code we expect.

Let’s start with the Babel plug-in we’ll be using

  • @ Babel/parser: willjavascriptThe code compiles toAbstract Syntax Tree (AST)(Hereafter abbreviatedAST)
  • @ Babel/traverse by: traversalASTWith this plugin, we can use theASTAdd, delete, check and change any node on
  • @babel/types: AST node types. From this library we can generate the desiredAST node
  • @ Babel/generator: compilingASTTo generate ajavascriptcode

Create project && install dependencies

Mkdir ast-demo && CD ast-demo && NPM init # create project and initialize package.json mkdir code && CD code && touch demo NPM install @babel/ parser@babel /traverse @babel/types @babel/generator @babel/core --save-devCopy the code

Finally, paste the code to be parsed into the ast-demo/code/demo.ts file as follows:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
import { COMPONENTS } from './components';
import { DIRECTIVES } from './directives';
import { ZorroModule } from '@modules/zorro/zorro.module';
import { PIPES } from './pipes';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    RouterModule,
    TranslateModule,
    ReactiveFormsModule,
    ZorroModule,
  ],
  declarations: [...COMPONENTS, ...DIRECTIVES, ...PIPES],
  exports: [
    CommonModule,
    FormsModule,
    RouterModule,
    TranslateModule,
    ReactiveFormsModule,
    ZorroModule,
    ...COMPONENTS,
    ...DIRECTIVES,
    ...PIPES,
  ],
  providers: [].// Code to add
})
class SharedModule {}

export { SharedModule };
Copy the code

Converting javascript code

Javascript code is parsed using @babel/ Parser to generate AST

The first step is to parse the corresponding javascript code into an AST. Here, because it involves reading and writing files, we will use Node to deal with the following:

Note that when using @babel/ Parser, the decorators-legacy plugin must be added to recognize the decorators in the code to be parsed, otherwise an error will be reported:

SyntaxError: This experimental syntax requires enabling one of the following parser plugin(s): ‘decorators – legacy, decorators (11:0).

const { parse } = require('@babel/parser');
const path = require('path');
const fs = require('fs');
const codePath = './code/demo.ts'; // The code path to parse,
const file = fs.readFileSync(path.resolve(__dirname, codePath)).toString();
const ast = parse(file, {
  sourceType: 'module'.plugins: ['decorators-legacy'].// If there is a decorator in your code, you need to add this plugin.
});
Copy the code

Traverse AST nodes with @babel/traverse and handle special nodes

Once we have the corresponding AST, we can modify its nodes

Here we use the syntax import xx from xx as an example to add this code to run.js

const { parse } = require('@babel/parser');
const traverse = require('@babel/traverse').default; // Walk through the AST to process each node
const path = require('path');
const fs = require('fs');
const codePath = './code/demo.ts'; // The code path to parse,
const file = fs.readFileSync(path.resolve(__dirname, codePath)).toString();
const ast = parse(file, {
  sourceType: 'module'.plugins: ['decorators-legacy'.'typescript'].// If there is a decorator in your code, you need to add this plug-in. If you have TS features such as type declarations in your code, you need to add typescript plug-ins
});
let num = 0;
traverse(ast, {
  ImportDeclaration(path) {
    num++;
    console.log(num); // output 1,2,3,4,5,6,7,8,9}});Copy the code

Execute the commandnode run.jsThe results are as follows

Now, to get back to business, here’s what we expect:

  • in@NgModuleAdd a key-value pair to the decorator,providers: [SENTRY_PROVIDERS]

So how do we know which AST node type our Class SharedModule corresponds to?

Because there are so many TYPES of AST nodes, it can take a lot of time to check the official documentation. The AST Explorer is recommended.

If you want to obtain a specific node, select the corresponding code from the source code on the left, and the yellow part on the right is the node type

You know how to get itAST Node TypeThen we can do the sameclassNode type

As you can see in our code, the ClassDeclaration contains a node Decorator, not the class Decorator that we see in our code. This also fills in the holes in our previous article. If you add ImportDeclaration directly to the node before the @NgModule, it will be added inside the ClassDeclaration, not the desired result. Those familiar with decorators should also know that decorators can decorate classes, properties, methods, and so on, and do not exist in isolation. So if you understand decorators, the first thing that comes to mind here is that you should go to the front of the ClassDeclaration and add the desired node. Of course, you can get the result intuitively by using the AST Explorer.

Next, modify run.js and run it, using the path.node property to get the AST node

const { parse } = require('@babel/parser');
const traverse = require('@babel/traverse').default; // Walk through the AST to process each node
const path = require('path');
const fs = require('fs');
const codePath = './code/demo.ts'; // The code path to parse,
const file = fs.readFileSync(path.resolve(__dirname, codePath)).toString();
const ast = parse(file, {
  sourceType: 'module'.plugins: ['decorators-legacy'].// If there is a decorator in your code, you need to add this plugin.
});

traverse(ast, {
  ClassDeclaration(path) {
    console.log(path.node); // add it}});Copy the code

Create a new AS node using @babel/types

With @babel/traverse and AST Explorer, we found the AST node type for class sharedModule. Import {SENTRY_PROVIDERS} from ‘@core/sentry’.

This is where @babel/types comes in to help us create AST nodes. Refer to the @babel/types API documentation for details.

In the documentation, we can see a number of apis that can help you create any known AST node. So how do I know how to combine these apis to generate my code?

Take the line import {SENTRY_PROVIDERS} from ‘@core/sentry’ as an example. The AST Explorer is also required to observe the CORRESPONDING AST

Obviously, its AST node type is ImportDeclaration

Next, we look at how the @babel/ Types API document generates an ImportDeclaration node.

From the documentation, we learned that to generate code in the format import xx from XX, we need two parameter specifiers and source. So we can add the following code first

const t = require('@babel/types');

t.importDeclaration(specifiers, source); // specifiers, source as defined
Copy the code

And specifiers is of type Array < ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier > Array object. If you are not sure whether the node type ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier which one word, so you can return to the AST Explorer to view.

Click on theSENTRY_PROVIDERSCan get the current node typeIdentifier, which can be interpreted as our variable/identifier, whose parent isImportSpecifierType.

After confirming the type, return to @babel/types API document to check that the ImportSpecifier node is generated. Local and imported are required parameters, and local and imported are required parameters, which are Identifier types, i.e. variables.

Modify the code as follows

const t = require('@babel/types');
const local = t.Identifier('SENTRY_PROVIDERS');
const imported = t.Identifier('SENTRY_PROVIDERS');
const specifiers = [t.ImportSpecifier(local, imported)];
const importDeclaration = t.importDeclaration(specifiers, source); // Source is undefined
Copy the code

After generating the ImportSpecifier node, we look at the second parameter required by the ImportDeclaration, that is, the node type corresponding to source is StringLiteral, and use the same method to find the parameters required to generate StringLiteral nodes.

Modify the code as follows to obtain the AST corresponding to the final import xx from ‘xx’ syntax

const { parse } = require('@babel/parser');
const traverse = require('@babel/traverse').default; // Walk through the AST to process each node
const t = require('@babel/types');
const path = require('path');
const fs = require('fs');
const codePath = './code/demo.ts'; // The code path to parse,
const file = fs.readFileSync(path.resolve(__dirname, codePath)).toString();
const ast = parse(file, {
  sourceType: 'module'.plugins: ['decorators-legacy'].// If there is a decorator in your code, you need to add this plugin.
});

traverse(ast, {
  ClassDeclaration(path) {
    const local = t.Identifier('SENTRY_PROVIDERS');
    const imported = t.Identifier('SENTRY_PROVIDERS');
    const specifiers = [t.ImportSpecifier(local, imported)];
    const source = t.stringLiteral('@core/sentry');
    const importDeclaration = t.importDeclaration(specifiers, source);

    console.log(importDeclaration); }});Copy the code

Perform operations on the current AST node

After obtaining the AST from the ImportDeclaration, we need to modify the original AST to generate a new AST.

That’s where the @babel/traverse path parameter comes in. The parameters can be found in the Babel operation manual – Conversion operations. All known apis are described in the documentation.

We need to add the ImportDeclaration node in front of ClassDeclaration as follows:

const { parse } = require('@babel/parser');
const traverse = require('@babel/traverse').default; // Walk through the AST to process each node
const t = require('@babel/types');
const path = require('path');
const fs = require('fs');
const codePath = './code/demo.ts'; // The code path to parse,
const file = fs.readFileSync(path.resolve(__dirname, codePath)).toString();
const ast = parse(file, {
  sourceType: 'module'.plugins: ['decorators-legacy'].// If there is a decorator in your code, you need to add this plugin.
});

traverse(ast, {
  ClassDeclaration(path) {
    const local = t.Identifier('SENTRY_PROVIDERS');
    const imported = t.Identifier('SENTRY_PROVIDERS');
    const specifiers = [t.ImportSpecifier(local, imported)];
    const source = t.stringLiteral('@core/sentry');
    const importDeclaration = t.importDeclaration(specifiers, source);

    path.insertBefore(importDeclaration); // update it}});Copy the code

The next step is to add providers: [SENTRY_PROVIDERS] key-value pairs in the @NgModule decorator using the same method. Directly on the code:

const { parse } = require('@babel/parser');
const traverse = require('@babel/traverse').default; // Walk through the AST to process each node
const path = require('path');
const fs = require('fs');
const codePath = './code/demo.ts'; // The code path to parse,
const file = fs.readFileSync(path.resolve(__dirname, codePath)).toString();
const ast = parse(file, {
  sourceType: 'module'.plugins: ['decorators-legacy'].// If there is a decorator in your code, you need to add this plugin.
});
let code;
let hasProviders = false;

traverse(ast, {
  ClassDeclaration(path) {
    const local = t.Identifier('SENTRY_PROVIDERS');
    const imported = t.Identifier('SENTRY_PROVIDERS');
    const specifiers = [t.ImportSpecifier(local, imported)];
    const source = t.stringLiteral('@core/sentry');
    const importDeclaration = t.importDeclaration(specifiers, source);

    path.insertBefore(importDeclaration); // Insert the importDeclaration node before the current ClassDeclaration node
  },
  ObjectProperty(path) {
    // ObjectProperty corresponds to the key-value pair in js syntax, xx: xx
    if (path.node.key.name === 'providers') {
      // If the code already has key providers, add them directly
      hasProviders = true;
      path.node.value.elements.push(t.identifier('SENTRY_PROVIDERS')); // Path.node.value. elements You can use the AST Explorer to view the hierarchy
    }
    if(! hasProviders && isEnd(path.getAllNextSiblings())) {// If the last ObjectProperty is traversed and there is no providers property, add key-value pairs
      hasProviders = false;
      // Add a key-value pair after the current node
      path.insertAfter(
        t.objectProperty(t.identifier('providers'), t.arrayExpression()) ); }}});function isEnd(nodes) {
  return! nodes.some((item) = > item.node.type === 'ObjectProperty');
}
Copy the code

Use @babel/generator to generate code

Finally, its AST is compiled into code using @babel/ Generator. You can get more information from the @babel/ Generator API. The fs module is then used to write the code to the object file

Add the following code:

. fs.writeFileSync(codePath, generate(ast, {}, code).code);console.log('Success to generate it');

Copy the code

Complete code: Github source code

Refer to the link

Generate javascript code using Babel

Babel operation manual