parse-typescript-component-interface

使用 AST 解析 React TypeScript Component 接口定义

Stars
12

AST React TypeScript Component

TypeScript React Markdown props

props

import React from "react";

export interface IProps {
  // 
  text: string;
  // 
  onClick: () => void;
}

const Button = ({ text }) => {
  return <button>{text}</button>;
};

export default Button;

IProps

AST AST

  1. AST
  2. AST interface
  3. interface definition
code -> ast -> interface -> definitions

-> AST

babel

const parser = require("@babel/parser");

function transformCode2Ast(code) {
  return parser.parse(code, {
    sourceType: "module",
    plugins: [
      "jsx",
      "typescript",
      "asyncGenerators",
      "bigInt",
      "classProperties",
      "classPrivateProperties",
      "classPrivateMethods",
      ["decorators", { decoratorsBeforeExport: false }],
      "doExpressions",
      "dynamicImport",
      "exportDefaultFrom",
      "exportNamespaceFrom",
      "functionBind",
      "functionSent",
      "importMeta",
      "logicalAssignment",
      "nullishCoalescingOperator",
      "numericSeparator",
      "objectRestSpread",
      "optionalCatchBinding",
      "optionalChaining",
      ["pipelineOperator", { proposal: "minimal" }],
      "throwExpressions",
      "topLevelAwait",
      "estree",
    ],
  });
}

Button AST

Node {
  type: 'File',
  start: 0,
  end: 207,
  loc:
   SourceLocation {
     start: Position { line: 1, column: 0 },
     end: Position { line: 16, column: 0 } },
  errors: [],
  program:
   Node {
     type: 'Program',
     start: 0,
     end: 207,
     loc: SourceLocation { start: [Position], end: [Position] },
     sourceType: 'module',
     interpreter: null,
     body: [ [Node], [Node], [Node], [Node] ] },
  comments:
   [ { type: 'CommentLine',
       value: ' ',
       start: 57,
       end: 62,
       loc: [SourceLocation] },
     { type: 'CommentLine',
       value: ' ',
       start: 81,
       end: 88,
       loc: [SourceLocation] } ] }

AST

ast-types ast

const { visit } = require("ast-types");

function findInterface(ast) {
  let ret = Object.create(null);
  let currentInterface = null;

  visit(ast, {
    visitTSInterfaceDeclaration(nodePath) {
      currentInterface = nodePath.value.id.name;
      this.traverse(nodePath);
    },
    visitTSPropertySignature(nodePath) {
      ret[currentInterface] = ret[currentInterface] || [];
      ret[currentInterface].push(nodePath.value);
      return false;
    },
  });
  return ret;
}

AST Interface

Node {
  type: 'TSPropertySignature',
  start: 65,
  end: 78,
  loc: [SourceLocation],
  key: [Node],
  computed: false,
  typeAnnotation: [Node],
  leadingComments: [Array],
  trailingComments: [Array] },
Node {
  type: 'TSPropertySignature',
  start: 91,
  end: 111,
  loc: [SourceLocation],
  key: [Node],
  computed: false,
  typeAnnotation: [Node],
  leadingComments: [Array] } ] }

typeAnnotation

TypeAnnotation

typeAnnotation

const get = require("lodash/get");

function parseTSTypeReference(typeName) {
  const type = get(typeName, "type");
  switch (type) {
    case "TSQualifiedName":
      return `${get(typeName, "left.name")}.${get(typeName, "right.name")}`;
    default:
      return `Unknown ReferenceType`;
  }
}

function parseTSFunctionType(parameters, typeAnnotation) {
  const parseTSFunctionParameters = (parameters) => {
    if (!parameters || !parameters.length) {
      return `()`;
    }
    let args = parameters.map((parameter) => {
      return `${get(parameter, "name")}: ${parseTypeAnnotation(
        get(parameter, "typeAnnotation.typeAnnotation")
      )}`;
    });
    return "( " + args.join(", ") + ")";
  };
  const parseTSFunctionReturn = (typeAnnotation) => {
    const type = get(typeAnnotation, "type");
    switch (type) {
      case "TSVoidKeyword":
        return "void";
      case "TSTypeReference":
        return parseTSTypeReference(get(typeAnnotation, "typeName"));
      default:
        return `Unknown FunctionType`;
    }
  };
  return `${parseTSFunctionParameters(parameters)} => ${parseTSFunctionReturn(
    typeAnnotation
  )}`;
}

function parseTSTypeLiteral(members) {
  const ret = parseInterfaceDefinitions(members);
  let args = ret.map((t) => `${t.name}: ${t.type}`);
  return "{ " + args.join(", ") + " }";
}

function parseTypeAnnotation(typeAnnotation) {
  const type = get(typeAnnotation, "type");
  switch (type) {
    case "TSNumberKeyword":
    case "TSStringKeyword":
    case "TSBoleanKeyword":
    case "TSNullKeyword":
    case "TSUndefinedKeyword":
    case "TSSymbolKeyword":
    case "TSAnyKeyword":
      return type.match(/TS(\w+)Keyword/)[1].toLowerCase();
    case "TSUnionType":
      return get(typeAnnotation, "types", [])
        .map((type) => get(type, "literal.value"))
        .join(" | ");
    case "TSFunctionType":
      return parseTSFunctionType(
        get(typeAnnotation, "parameters"),
        get(typeAnnotation, "typeAnnotation.typeAnnotation")
      );
    case "TSTypeReference":
      return parseTSTypeReference(get(typeAnnotation, "typeName"));
    case "TSTypeLiteral":
      return parseTSTypeLiteral(get(typeAnnotation, "members"));
    default:
      return "UnKnowType";
  }
}

function parseInterfaceDefinitions(nodePaths) {
  const parseInterfaceDefinitionsNode = (nodePath) => {
    const name = get(nodePath, "key.name");
    const comments = get(nodePath, "leadingComments.0.value", "")
      .trim()
      .split(/[\r\n]/)
      .map((str) => str.trim().replace(/^\*/g, "").trim())
      .filter(Boolean);
    const typeAnnotation = get(nodePath, "typeAnnotation.typeAnnotation");
    const type = parseTypeAnnotation(typeAnnotation);
    return { name, type, comments };
  };
  return nodePaths.map(parseInterfaceDefinitionsNode);
}

Button

[
  [
    {
      name: "text",
      type: "string",
      comments: [""],
    },
    {
      name: "onClick",
      type: "() => void",
      comments: [""],
    },
  ],
];

function parseTypeScriptComponentInterface(code) {
  let ast = transformCode2Ast(code);
  let interfaces = findInterfaces(ast);
  let definitions = Object.keys(interfaces).reduce((a, c) => {
    a[c] = a[c] || [];
    a[c].push(parseInterfaceDefinitions(interfaces[c]));
    return a;
  }, Object.create(null));
  return definitions;
}

const code = `
import React from 'react';

export interface IProps {
  /**
   * button 
   * 
   */
  text: string;
  // 
  onClick: () => void;
  //  3
  props3: (arg: any) => void;
  //  4
  props4: (arg: { name: string, age: number }) => React.Node
}

const Button = ({ text }) => {
  return <button>{text}</button>;
};

export default Button;
`;

let ret = parseTypeScriptComponentInterface(code);
{
  "IProps": [
    [
      {
        "name": "text",
        "type": "string",
        "comments": [
          "button",
          ""
        ]
      },
      {
        "name": "onClick",
        "type": "() => void",
        "comments": [
          ""
        ]
      },
      {
        "name": "props3",
        "type": "( arg: any) => void",
        "comments": [
          " 3"
        ]
      },
      {
        "name": "props4",
        "type": "( arg: { name: string, age: number }) => React.Node",
        "comments": [
          " 4"
        ]
      }
    ]
  ]
}