从0到1开发一个vscode插件之生成ts声明

1,018 阅读3分钟

前言

为什么要开发vscode插件,主要还是为了学习。 github地址

为什么写一个对象自动生成声明类型的插件,主要还是因为手动编写大量复杂的类型声明可能非常耗时且容易出错,通过开发 VS Code 插件来实现数据生成 TypeScript 声明,提高编码效率。

1. 需求分析

因为大多数插件是json转成ts,觉得比较麻烦,还是直接光标放在对象上生成更方便。

  • 暂时只针对ts、js文件生成。
  • 当定义一个对象时,只需要将光标放在对象的任意位置,自动生成声明类型。

2. 实现方式

2.1 创建项目

  1. 使用官方推荐的脚手架进行创建: 全局安装npm install -g yo generator-code

  2. 初始化项目

运行yo code或者分开运行yo-> code 都可以

image.png

  1. 项目结构
├─.eslintrc.json
├─.gitignore
├─.vscodeignore
├─.yarnrc
├─CHANGELOG.md
├─package.json
├─README.md
├─tsconfig.json
├─vsc-extension-quickstart.md
├─yarn.lock
├─src
|  ├─extension.ts   // 写逻辑代码文件
|  ├─test           // 测试文件
|  |  ├─runTest.ts
|  |  ├─suite
|  |  |   ├─extension.test.ts
|  |  |   └index.ts
├─.vscode
|    ├─extensions.json
|    ├─launch.json
|    ├─settings.json
|    └tasks.json

package.json

  "name": "data",
  "displayName": "data",
  "engines": {
    "vscode": "^1.79.0"
  },
  "activationEvents": [
    "onCommand:data.helloWorld"
  ],
  "main": "./out/extension.js",
  "contributes": {
    "commands": [
      {
        "command": "data.helloWorld",
        "title": "Hello World"  // ctrl+shift+p 调用命令时注册 对应extension.ts中注册的data.helloWorld
      }
    ]
  },
  "scripts": {
    "vscode:prepublish": "yarn run compile",
    "compile": "tsc -p ./",     // 编译
    "watch": "tsc -watch -p ./", // 监视
    "pretest": "yarn run compile && yarn run lint",
    "lint": "eslint src --ext ts",
    "test": "node ./out/test/runTest.js"
  },

extension.ts

import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {

	let disposable = vscode.commands.registerCommand('data.helloWorld', () => {
		vscode.window.showInformationMessage('Hello World from data!');
	});

	context.subscriptions.push(disposable);
}

  1. 调试 直接按F5调试,会自动新开一个vscode窗口,可以进行命令调用。

image.png

2.2 插件具体实现

  1. 获取当前光标的位置,和所有文本信息
  2. 将获取的文本,进行ast转换,enter获取对象的开始结束索引;VariableDeclaration进行判断
  3. 将获取对象的ast进行遍历,得到对应的文本
  4. 插入到上方的位置

extension.ts

import * as vscode from "vscode";
import { getFunctionCode } from "./utils"


/**
 * 
 * @param context 直接ast进行遍历生成
 */
export function activate(context: vscode.ExtensionContext) {
  let disposable = vscode.commands.registerCommand(
    "yinuo.data2ts",
    async () => {
      try {
                // get active editor and selection 获取当前文件 如果没有打开文件,报错
                const editor = vscode.window.activeTextEditor
                if (!editor) {
                        vscode.window.showErrorMessage('No open text editor')
                        return
                }

                const code = editor.document.getText()
                const index = editor.document.offsetAt(editor.selection.active)

                let result = getFunctionCode(code, index)
                if(!result){
                        return
                }
                const {startPosition,resultType} = result

                if (!resultType) {
                        return
                }

                // find the previous empty line and insert the declaration
                const startPos = new vscode.Position(startPosition - 2, 0)

                await editor.edit(editBuilder => {
                        editBuilder.insert(startPos, resultType)
                })
		} catch (error) {
        vscode.window.showErrorMessage(`Error: ${error}`);
      }
    }
  );

  context.subscriptions.push(disposable);
}

utils.ts

import { parse } from "@babel/parser";
import traverse from "@babel/traverse";
import * as t from "@babel/types";

type TCodeRes = {
	resultType: string;
	startPosition: number;
};

// 考虑光标所在位置的
export const getFunctionCode = (
	code: string,
	index: number
): TCodeRes | undefined => {
	let scopePath: any;
	let resultType = "";

	// 1. 第一步解析成ast树,筛选出变量的ast
	const ast = parse(code,{
		sourceType: 'module',
		plugins: ['typescript', 'decorators-legacy'],
	  });
	traverse(ast, {
		enter(path) {
			// 判断是否是变量声明
			if (t.isVariableDeclaration(path.node)) {
				if (path.node.start! <= index && path.node.end! >= index) {
					scopePath = path;
				}
			}
		},
		VariableDeclaration(path) {
			if (!scopePath) {return;}
			// 找出光标所在位置 找到开始结束节点
			const { start, end } = path.node;
			const startPosition = scopePath.node.start;
			const endPosition = scopePath.node.end;
			if (start! >= startPosition && end! <= endPosition) {
				// 判断是否是变量定义
				if (t.isVariableDeclaration(path.node)) {
					console.log(path.node, "path.node");
					const declaration = path.node.declarations[0]; // VariableDeclarator
					if (declaration && t.isIdentifier(declaration.id)) {
						// id : obj1 标识符信息
						const typeDeclaration = getTypeDeclaration(declaration.init!); // init 是所有键值对信息
						resultType = `type T${declaration.id.name} = ${typeDeclaration}`;
					}
				}
			}
		},
	});


	return { resultType, startPosition: scopePath.node.loc?.start.line };
};

// 此函数将基于给定的节点类型和属性生成类型声明
function getTypeDeclaration(node: t.Node): string {
	// 判断是否是对象表达式
	if (t.isObjectExpression(node)) {
		const properties = node.properties
			.map((prop) => {
				if (t.isObjectProperty(prop)) {
					return `\n\t${(prop.key as t.Identifier).name}: ${getTypeDeclaration(
						prop.value
					)}`;
				}
				return "";
			})
			.join(" ");

		return `{${properties}\n}`;
	} else if (t.isArrayExpression(node)) {
		const elementType =
			node.elements.length > 0 ? getTypeDeclaration(node.elements[0]!) : "any";
		return `${elementType}[]`;
	} else if (t.isStringLiteral(node)) {
		return "string";
	} else if (t.isNumericLiteral(node)) {
		return "number";
	} else if (t.isBooleanLiteral(node)) {
		return "boolean";
	} else if (t.isNullLiteral(node)) {
		return "null";
	} else if (t.isIdentifier(node) && node.name === "undefined") {
		return "undefined";
	} else {
		return "any";
	}
}

效果展示:

image.png

3. 打包发布

打包需要全局安装npm i -g vsce

发布有两种方式:

  1. vsce package 生成.vsix后缀文件
  2. vsce publish 发布到vscode插件市场

主要讲解第二种方式

3.1 vsce publish过程

  1. 注册azure账号,创建Personal Access Token,创建发布者。
  2. vsce login username 输入token
  3. vsce publish 即可

详细过程参考

发布过程中package中的名换必须与发布者一致否则报错

image.png

发布完成后

image.png

image.png

4. 总结

闪光点:不需要选中

插件不足:

  1. 暂时不支持vue文件
  2. 不支持深度嵌套提取公共类型
  3. 不支持自定义名称
  4. 不支持interface和Type切换
  5. 只支持对象的简单类型

第一次发布,有很多需要提升的地方。希望大家一起进步。^o^