“This is the 17th day of my participation in the First Challenge 2022.
PS: RECENTLY, I was looking at the Babel plugin of The god of Light and wanted to record some of my learning process.
Using the vsCode editor, when typing /**, press enter
The editor automatically generates comments based on the input and output arguments of the function.
sourceCode.ts
/** * example1 test code *@param The name name *@param The age age *@param Sex sex *@returns string* /
function example1(name: string, age: number, sex: boolean) :string {
return `hi, ${name}.${age}.${sex}`;
}
/** * class test */
class Guang {
name: string; / / the name attribute
constructor(name: string) {
this.name = name;
}
/** * method test */
sayHi(): string {
return `hi, I'm The ${this.name}`; }}Copy the code
ReadFileSync is used to read the file content. Babel Parser is used to parse the file content into an AST, and transformFromAstSync is used to transform the AST.
index.js
const { transformFromAstSync } = require('@babel/core');
const parser = require('@babel/parser');
const autoDocumentPlugin = require('./plugin/auto-document-plugin');
const fs = require('fs');
const path = require('path');
const sourceCode = fs.readFileSync(path.join(__dirname, './sourceCode.ts'), {
encoding: 'utf-8'
});
const ast = parser.parse(sourceCode, {
sourceType: 'unambiguous'.plugins: ['typescript']});const { code } = transformFromAstSync(ast, sourceCode, {
plugins: [[autoDocumentPlugin, {
outputDir: path.resolve(__dirname, './docs'),
format: 'json'}}]]);console.log(code);
Copy the code
The basic structure of the plug-in:
const { declare } = require('@babel/helper-plugin-utils');
const fse = require('fs-extra');
const path = require('path');
const autoDocumentPlugin = declare((api, options, dirname) = > {
api.assertVersion(7);
return {
pre(file){},visitor: {
FunctionDeclaration(path, state){},ClassDeclaration(path, state){}},post(file){}}});module.exports = autoDocumentPlugin;
Copy the code
Place an array of docs in the global File object to collect information.
const autoDocumentPlugin = declare((api, options, dirname) = > {
api.assertVersion(7);
return {
pre(file) {
file.set('docs',} []),visitor: {
FunctionDeclaration(path, state){},ClassDeclaration(path, state){}},post(file) {
const docs = file.get('docs'); }}});Copy the code
The FunctionDeclaration node and the ClassDelcaration node need to be handled
Because we create the Docs array ourselves, we push whatever data we need into the array.
FunctionDeclaration takes two arguments, path and state. In Path, you can get a lot of things you need.
We don’t care about the implementation details of a function. To call a function externally, you basically only need to know the function name, parameters, and return value types of the function, and that’s about it.
According to this idea, code writing.
FunctionDeclaration(path, state) {
const docs = state.file.get('docs');
docs.push({
type: 'function'.name: path.get('id'),
params: path.get('params').map(paramPath= > {
return {
name: path.get('id'),
type: path.get('returnType')}}),return: path.get('returnType'),
doc: path.node.leadingComments
})
},
Copy the code
Print out the obtained docs and find that the docs are not usable at all and need to continue processing the data.
FunctionDeclaration(path, state) {
const docs = state.file.get('docs');
docs.push({
type: 'function'.name: path.get('id').toString(),
params: path.get('params').map(paramPath= > {
return {
name: path.get('id').toString(),
type: path.get('returnType').getTypeAnnotation()
}
}),
return: path.get('returnType').getTypeAnnotation(),
doc: path.node.leadingComments[0].value
})
},
Copy the code
Now, it seems to be ok, and we have roughly the data we need
FunctionDeclaration(path, state) {
const docs = state.file.get('docs');
docs.push({
type: 'function'.name: path.get('id').toString(),
params: path.get('params').map(paramPath= > {
return {
name: paramPath.toString(),
type: resolveType(paramPath.getTypeAnnotation())
}
}),
return: returnType(path.get('returnType').getTypeAnnotation()),
doc: path.node.leadingComments && parseComment(path.node.leadingComments[0].value)
});
state.file.set('docs', docs);
},
Copy the code
- Annotation information is parsed with Doctrine
function resolveType(tsType) {
const typeAnnotation = tsType.typeAnnotation;
if(! typeAnnotation) {return;
}
switch (typeAnnotation.type) {
case 'TSStringKeyword':
return 'string';
case 'TSNumberKeyword':
return 'number';
case 'TSBooleanKeyword':
return 'boolean'; }}function returnType(tsType) {
switch (tsType.type) {
case 'TSStringKeyword':
return 'string';
case 'TSNumberKeyword':
return 'number';
case 'TSBooleanKeyword':
return 'boolean'; }}function parseComment(commentStr) {
if(! commentStr) {return;
}
return doctrine.parse(commentStr, {
unwrap: true
});
}
Copy the code
The processing of ClassDeclaration
Extract constructor, Method, and properties information.
ClassDeclaration(path, state) {
const docs = state.file.get('docs');
const classInfo = {
type: 'class'.name: path.get('id').toString(),
constructorInfo: {},
methodsInfo: [].propertiesInfo: [].doc: path.node.leadingComments && parseComment(path.node.leadingComments[0].value)
};
path.traverse({
ClassProperty(path) {
classInfo.propertiesInfo.push({
name: path.get('key').toString(),
type: resolveType(path.getTypeAnnotation()),
doc: [path.node.leadingComments, path.node.trailingComments].filter(Boolean).map(comment= > {
return parseComment(comment.value);
}).filter(Boolean)})},ClassMethod(path) {
if (path.node.kind === 'constructor') {
classInfo.constructorInfo = {
params: path.get('params').map(paramPath= > {
return {
name: paramPath.toString(),
type: resolveType(paramPath.getTypeAnnotation()),
doc: parseComment(path.node.leadingComments[0].value)
}
})
}
} else {
classInfo.methodsInfo.push({
name: path.get('key').toString(),
doc: parseComment(path.node.leadingComments[0].value),
params: path.get('params').map(paramPath= > {
return {
name: paramPath.toString(),
type: resolveType(paramPath.getTypeAnnotation())
}
}),
return: resolveType(path.getTypeAnnotation())
})
}
}
});
docs.push(classInfo);
state.file.set('docs', docs);
}
Copy the code
All the information is available in the POST phase, after which the document is generated.
JSON
For example, we want to generate a JSON format. In the POST phase, get the parameters passed in, use the generate function to generate the result, and call fs.writefilesync to write the preset path.
post(file) {
const docs = file.get('docs');
const res = generate(docs, options.format);
fse.ensureDirSync(options.outputDir);
fse.writeFileSync(path.join(options.outputDir, 'docs' + res.ext), res.content);
}
Copy the code
const renderer = require('./renderer');
function generate(docs, format = 'json') {
if (format === 'json') {
return {
ext: '.json'.content: renderer.json(docs)
}
}
}
Copy the code
renderer/json.js
module.exports = function (docs) {
return JSON.stringify(docs, null.2);
}
Copy the code
docs/docs.json
[{"type": "function"."name": "example1"."params": [{"name": "name"."type": "string"
},
{
"name": "age"."type": "number"
},
{
"name": "sex"."type": "boolean"}]."return": "string"."doc": {
"description": "Example1 Test code"."tags": [{"title": "param"."description": "Name"."type": null."name": "name"
},
{
"title": "param"."description": "Age"."type": null."name": "age"
},
{
"title": "param"."description": "Gender"."type": null."name": "sex"}]}}, {"type": "class"."name": "Guang"."constructorInfo": {
"params": [{"name": "name"."type": "string"."doc": {
"description": "Property name"."tags": []}}]},"methodsInfo": [{"name": "sayHi"."doc": {
"description": "Method test"."tags": []},"params": []}],"propertiesInfo": [{"name": "name"."type": "string"."doc": []}],"doc": {
"description": "Class test"."tags": []}}]Copy the code
Markdown
renderer/markdown.js
According to different types, different markown splicing is carried out
module.exports = function (docs) {
let str = ' ';
docs.forEach(doc= > {
if (doc.type === 'function') {
str += '## ' + doc.name + '\n\n';
str += doc.doc.description + '\n\n';
if (doc.doc.tags) {
doc.doc.tags.forEach(tag= > {
str += tag.name + ':' + tag.description + '\n\n';
})
}
str += '>' + doc.name + '(';
if (doc.params) {
str += doc.params.map(param= > {
return param.name + ':' + param.type;
}).join(', ');
}
str += ')\n';
str += '#### parameter: \n\n';
if (doc.params) {
str += doc.params.map(param= > {
return The '-' + param.name + '(' + param.type + ') ';
}).join('\n');
}
str += '\n\n'
} else if (doc.type === 'class') {
str += '## ' + doc.name + '\n\n';
str += doc.doc.description + '\n';
if (doc.doc.tags) {
doc.doc.tags.forEach(tag= > {
str += tag.name + ':' + tag.description + '\n';
})
}
str += '> new ' + doc.name + '(';
if (doc.params) {
str += doc.params.map(param= > {
return param.name + ':' + param.type;
}).join(', ');
}
str += ')\n';
str += '#### attribute: \n\n';
if (doc.propertiesInfo) {
doc.propertiesInfo.forEach(param= > {
str += The '-' + param.name + ':' + param.type + '\n';
});
}
str += '#### method: \n\n';
if (doc.methodsInfo) {
doc.methodsInfo.forEach(param= > {
str += The '-' + param.name + '\n';
});
}
str += '\n'
}
str += '\n'
})
return str;
}
Copy the code
If the format is markDown
transformFromAstSync(ast, sourceCode, {
plugins: [[autoDocumentPlugin, {
outputDir: path.resolve(__dirname, './docs'),
format: 'markdown'}}]]);Copy the code
Rewrite generate function
function generate(docs, format = 'json') {
if (format === 'markdown') {
return {
ext: '.md'.content: renderer.markdown(docs)
}
} else {
return {
ext: '.json'.content: renderer.json(docs)
}
}
}
Copy the code
We’re done 🎉🎉🎉🎉🎉🎉🎉