跳到主要内容

Typescript解析器之Midway配置项读取

· 10 分钟阅读
月子喵

最近在使用 Midway 框架写一个小项目, 虽然装饰器在很多时候提供了非常多辅助作用, 但是用多了会发现一件事情, 很多的装饰器参数都是字符串, 这就导致了编辑器完全没有办法做语法补全🤷‍♀️, 非常的蛋疼.

为了缓解这个问题我打算写一个关于 Midway 框架的 LSP

graph TD
`EggAppConfig`Interface --> config
config --> Ref
Ref -->
הההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההה
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

我这边使用的是 ts-node 调试的时候用是 `node --inspect -r ts-node/register <filename.ts>

目前在找工作, 如果这家公司用的是 midway 我就把这个插件补完, 发布出来并补上其他功能 😄

测试文件

首先为了编写的简单, 先约定在src/config/config.d.ts 里面一个 继承 EggAppConfig 结构的声明

import { EggAppConfig } from 'midway'
import AliyunConfig from './aliyun.ts'
export interface RedisUrl {
redis_url: string
}
export interface MyProjectConfig extends EggAppConfig {
mongo_url: string
redis: RedisUrl,
aliyun: AliyunConfig
}
הההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההה
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
// `aliyun.ts`
export default interface AliyunConfig {
accessKey: string
key: {
ddd: string
},
dddd: string[]
}
הההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההה
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

效果

效果

初始化 Typescript 的 LanguageService

这个不多说, 不知道的看 ts 的文档, 那个文件的地址就是我们约定的 types 的存放位置, 可自己计算出来, 当然搜索也是没问题的

import * as ts from "typescript";
import * as fs from 'fs'
const files: ts.MapLike<{ version: number }> = {};
const rootFileNames: string[] = ["/Users/haozi/cat/cv_record/src/config/config.d.ts"];
// initialize the list of files
rootFileNames.forEach(fileName => {
files[fileName] = { version: 0 };
});
// Create the language service host to allow the LS to communicate with the host
const servicesHost: ts.LanguageServiceHost = {
getScriptFileNames: () => rootFileNames,
getScriptVersion: fileName =>
files[fileName] && files[fileName].version.toString(),
getScriptSnapshot: fileName => {
if (!fs.existsSync(fileName)) {
return undefined;
}
return ts.ScriptSnapshot.fromString(fs.readFileSync(fileName).toString());
},
getCurrentDirectory: () => process.cwd(),
getCompilationSettings: () => ({}),
getDefaultLibFileName: options => ts.getDefaultLibFilePath(options),
fileExists: ts.sys.fileExists,
readFile: ts.sys.readFile,
readDirectory: ts.sys.readDirectory
};
// Create the language service files
const services = ts.createLanguageService(
servicesHost,
ts.createDocumentRegistry()
);
(global as any).ts = ts;
(global as any).services = services; // , ts, , 访, !!!
הההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההה
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

搜索继承自EggAppConfig的 interface

遍历整个文件的节点, 如果找到了 interface 类型的, 那么就看他有没有继承, 如果又继承就看他的继承里面有没有一个叫做 EggAppConfig

function searchEggConfigNode(source: ts.SourceFile) {
let configNode = null;
ts.forEachChild(source, (node) => {
if (ts.isInterfaceDeclaration(node)) {
if (node.heritageClauses) {
node.heritageClauses.forEach((dnode) => {
dnode.types.forEach(_node => {
if ((_node.expression as ts.Identifier).escapedText === 'EggAppConfig') {
configNode = node;
}
})
});
}
}
})
return configNode;
}
הההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההה
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

搜索到 Config 节点之后, 遍历节点

遍历节点的时候, 针对不同的类型做不同的处理是, 这里写的有点粗糙, 可能有点细节没有考虑到,比如带复杂类型的数组(没处理)

  • 如果是 Interface 类型的数据, 就先寻找他的父类, 然后再计算他的成员
  • 如果已经是 PropertySignature那么就记录他的 kind 和名字, 跳转
  • 如果是 TypeReference的类型, 就要去寻找他的真实的定义,和位置
  • 如果 kind 的是 TypeLiteralNode 说明是一个数组结构, 需要记录为数组 并且记录他的类型
function mapConfig(configNode: ts.Node, config: Object = {}) {
if (ts.isInterfaceDeclaration(configNode)) {
if (configNode.heritageClauses && configNode.heritageClauses.length > 0) {
configNode.heritageClauses.forEach(node => {
Object.assign(config, mapConfig(node, {}))
})
}
if (configNode.members) {
configNode.members.forEach((member) => {
Object.assign(config, mapConfig(member, {}))
})
}
}
if (ts.isPropertySignature(configNode)) {
if (configNode.type.kind === ts.SyntaxKind.TypeReference) {
findDefinition(configNode).forEach(definition => {
Object.assign(config, {
[configNode.name.getText()]: {
children: mapConfig(definition, {}),
node: configNode
}
})
})
} else {
config[configNode.name.getText()] = {
kind: configNode.type.kind,
node: configNode
}
if (ts.isArrayTypeNode(configNode.type)) {
config[(configNode.name as ts.Identifier).text].elementKind = (configNode.type as ts
                  .ArrayTypeNode).elementType.kind
}
if (ts.isTypeLiteralNode(configNode.type)) {
Object.assign(config, {
[configNode.name.getText()]: {
children: mapConfig(configNode.type, {}),
node: configNode.type
}
})
}
}
}
if (ts.isImportDeclaration(configNode)) {
findDefinition(configNode).map(definition => {
return mapConfig(definition, {})
 
הההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההה
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

寻找申明的真实位置

很多时候都会遇到这个蛋疼的问题, ts 的 getDefinitionAtPosition的这个函数并不准确, 比如搜索EggAppConfig的 ImportDeclaration, 会返回给你 3 个定义,分别是

  • egg
  • egg-view
  • egg-multipart

搜索EggAppConfig

实际上你阅读以下源码可以清楚的知道是egg包里面的定义, 但是这玩意就给你返回了 3 个, webStorm 是 0 个 (MDZZ)

这里只是测试代码, 所以选择三个都返回, 在mapConfig 里面也是 3 个都合并

// , fileName sourceFile, Program SourceFileS,
function findSourceFile(program: ts.Program, fileName: string): ts.SourceFile {
for (const file of program.getSourceFiles()) {
if (file.fileName === fileName) {
return file;
}
}
return undefined;
}
function findDefinition(node: ts.Node): any[] {
let pos = 0;
// ts ,
// pos , + 1 ,
      getDefinitionAtPosition pos ,
if (ts.isHeritageClause(node)) {
pos = node.types[0].pos + 1;
}
if (ts.isPropertySignature(node)) {
pos = node.type.pos + 1
}
const definitions = services.getDefinitionAtPosition(node.getSourceFile().fileName, pos);
if (definitions && definitions.length > 0) {
// ,
return definitions.map(definition => {
const source = findSourceFile(program, definition.fileName);
return findNodeAtPos(source, definition.textSpan.start - 1);
})
}
return [];
}
הההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההה
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

getDefinitionAtPosition只会返回一个这样的数据结构

{
"fileName": "/Users/haozi/cat/cv_record/node_modules/midway/node_modules/midway-web/node_modules/egg
      /node_modules/egg-multipart/index.d.ts", //
"textSpan": { //
"start": 2208,
"length": 12
},
"kind": "interface", //
"name": "EggAppConfig", //
"containerName": "\"egg\"",
"contextSpan": {
"start": 2198,
"length": 458
}
}
הההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההה
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

所以我们需要吧 文件名, 位置, 拿去再搜索一次, 由于 ts.forEachChild 只能遍历一层, 增加了辅助函数遍历深层的Node,即 如果还有子节点就递归调用 deepNodeMap 直到没有子节点为止, 对比的时候要注意

  • 如果是 Interface 类型的数据, 那么就去读取他的 name.pos
  • 如果是普通的直接对比 pos 数据即可
//
const flattenDeep = (arr): any[] => Array.isArray(arr)
? arr.reduce( (a, b) => a.concat(flattenDeep(b)) , [])
: [arr]
//
function deepNodeMap<T extends ts.Node>(nodes: T[], callback: (node: T) => void, res: any[] = []): any[] {
if (nodes.length > 0) {
return flattenDeep(nodes.map((value) => {
if (value.getChildCount() > 0) {
return [callback(value), deepNodeMap(value.getChildren(), callback, res)];
} else {
return [callback(value)]
}
})).filter(_ => _)
}
return res;
}
// pos
function findNodeAtPos<T extends ts.Node>(sourceFile: ts.SourceFile, pos: number): T[] | null {
const res = deepNodeMap(sourceFile.getChildren(), node => {
if (ts.isInterfaceDeclaration(node)) {
if (node.name.pos === pos) {
return node as any as T;
}
} else {
if (node.pos === pos) {
return node as any as T;
}
}
})
// , Interface , ,
// EggAppConfig [NodeObject, IdentifierObject],
// NodeObject interface Node, IdentifierObject EggAppConfig Node
// Interface Node,
return res[0]
}
הההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההה
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

拿到了准确的位置信息和 Node 节点, 然后在 mapConfig 函数里面递归就行了

结束

如果拿到了这份配置表, 梳理一下结构, 非常容易的就可以根据输入的字符预测用户需要完成的配置项, 并且可以提前将 private | <Type> 这种结构帮他写好. 包括其他的 Service,Controller,Middleware都是同理, 搜索对应的就行

参考文档

[1]:microsoft/TypeScript - Using the Compiler API

Gitalking ...