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: will
javascript
The code compiles toAbstract Syntax Tree (AST)
(Hereafter abbreviatedAST
) - @ Babel/traverse by: traversal
AST
With this plugin, we can use theAST
Add, delete, check and change any node on - @babel/types: AST node types. From this library we can generate the desired
AST node
- @ Babel/generator: compiling
AST
To generate ajavascript
code
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.js
The results are as follows
Now, to get back to business, here’s what we expect:
- in
@NgModule
Add 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 Type
Then we can do the sameclass
Node 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_PROVIDERS
Can get the current node typeIdentifier
, which can be interpreted as our variable/identifier, whose parent isImportSpecifier
Type.
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