“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 🎉🎉🎉🎉🎉🎉🎉