源码解读之TypeScript类型覆盖检测工具type-coverage

因为团队内部开启了一个持续的前端代码质量改进计划,其中一个专项就是TS类型覆盖率,期间用到了type-coverage这个仓库,所以借这篇文章分享一下这个工具,并顺便从源码阅读的角度来分析一下该工具的源码,我自己fork了一个仓库,完成了中文版本的ReadMe文件并对核心代码添加了关键注释,需要的同学可以点击传送门

一、基本介绍

源码解读之TypeScript类型覆盖检测工具type-coverage

type-coverage是一个用于检查typescript代码的类型覆盖率的CLI工具,TS代码的类型覆盖率能够在某种程度上反映代码的质量水平(因为使用TS最主要的一个原因之一就是它所提供的类型安全保证)。

type-coverage该工具将检查所有标识符的类型,类型覆盖率 = any类型的所有标识符 / 所有的标识符,值越高,则越优秀。

typescript-coverage-report是用于生成 TypeScript 覆盖率报告的节点命令行工具,该工具基于type-coverage生成器实现。

工具的主要使用场景

(1) 呈现把项目的JS代码渐进迁移到TS代码的进度。

(2) 呈现把项目中宽松TS代码渐进转变为严格TS代码的进度。

(3) 避免引入意外的any

(4) 持续改进该指标以提升代码的质量。

工具的安装

安装方式1 yarn global add type-coverage

安装方式2 pnpm i type-coverage -g

二、基本使用

命令行工具
 
直接运行获取项目的TS类型覆盖率

$ pnpm type-coverage   669 / 689 97.09%   type-coverage success. 

查看更多的操作指令

$ pnpm type-coverage --help      type-coverage [options] [-- file1.ts file2.ts ...]      -p, --project               string?   告诉 CLI `tsconfig.json` 文件的位置     --detail                    boolean?  显示详情     --at-least                  number?   设置闸值,如果覆盖率小于该值,则失败     --debug                     boolean?  显示调试信息     --strict                    boolean?  是否开启严格模式     --ignore-catch              boolean?  忽略捕获     --cache                     boolean?  是否使用缓存     --ignore-files              string[]? 忽略文件     --ignore-unread             boolean?  允许写入Any类型的变量     -h,--help                   boolean?  显示帮助信息     --is                        number?   设置闸值,如果覆盖率不等于该值,则失败     --update                    boolean?  如果 package.json 中存在typeCoverage选项,则根据 "atLeast" or "is" 值更新     --update-if-higher          boolean?  如果新的类型覆盖率更高,则将package.json 中的"typeCoverage"更新到当前这个结果     --ignore-nested             boolean?  忽略any类型的参数, eg: Promise<any>     --ignore-as-assertion       boolean?  忽略 as 断言, eg: foo as string     --ignore-type-assertion     boolean?  忽略类型断言, eg: <string>foo     --ignore-non-null-assertion boolean?  忽略非空断言, eg: foo!     --ignore-object             boolean?  Object 类型不视为 any,, eg: foo: Object     --ignore-empty-type         boolean?  忽略空类型, eg: foo: {}     --show-relative-path        boolean?  在详细信息中显示相对路径     --history-file              string?   保存历史记录的文件名     --no-detail-when-failed     boolean?  当CLI失败时不显示详细信息     --report-semantic-error     boolean?  报告 typescript 语义错误     -- file1.ts file2.ts ...    string[]? 仅检查这些文件, 适用于 lint-staged     --cache-directory           string?   设置缓存目录 

获取覆盖率的详情

$ pnpm type-coverage --detail        /wendingding/client/src/http.ts:21:52: headers     /wendingding/client/src/http.ts:24:14: headers     /wendingding/client/src/http.ts:28:19: headers     /wendingding/client/src/http.ts:35:20: data     /wendingding/client/src/http.ts:36:25: data     /wendingding/client/src/http.ts:38:8: error     /wendingding/client/src/http.ts:57:31: error     /wendingding/client/src/http.ts:66:44: data     /wendingding/client/src/http.ts:67:39: data     /wendingding/client/src/http.ts:74:43: data     /wendingding/client/src/http.ts:75:38: data     /wendingding/client/src/http.ts:78:45: data     /wendingding/client/src/http.ts:79:40: data     /wendingding/client/src/main.ts:22:37: el     /wendingding/client/src/main.ts:23:7: blocks     /wendingding/client/src/main.ts:23:16: el     /wendingding/client/src/main.ts:23:19: querySelectorAll     /wendingding/client/src/main.ts:24:3: blocks     /wendingding/client/src/main.ts:24:10: forEach     /wendingding/client/src/main.ts:24:19: block     669 / 689 97.09%     type-coverage success. 

详情中的格式说明 文件路径:类型未覆盖变量所在的代码行:类型未覆盖变量在这一行中的位置(indexOf): 变量名

设置闸值

$ pnpm type-coverage --is 99     669 / 689 97.09%   The type coverage rate(97.09%) is not the target(99%). 

严格模式

$ pnpm type-coverage --strict    667 / 689 96.80% 

使用严格模式来进行计算,通常会降低类型覆盖率。

在严格模式下,具体执行的时候会有几点处理上的区别:

(1) 如果标识符的存在且至少包含一个any, 比如any[], ReadonlyArray<any>, Promise<any>, Foo<number, any>,那么他将被视为any

(2) 类型断言,类似 foo as string, foo!, <string>foo 将会被识别为未覆盖,排除foo as const, <const>foo, foo as unknown和由isTypeAssignableTo支持的其它安全类型断言

(3)Object类型(like foo: Object) 和 空类型 (like foo: {}) 将被视为any。

覆盖率报告

推荐使用来生成类型覆盖率的报告
安装包

$ yarn add --dev typescript-coverage-report  # OR  $ npm install --save-dev typescript-coverage-report  # OR  $ pnpm i typescript-coverage-report 

运行包

$ yarn typescript-coverage-report  # OR  $ npm run typescript-coverage-report  # OR pnpm  typescript-coverage-report 

其他更多的操作命令

% pnpm  typescript-coverage-report --help Usage: typescript-coverage-report [options]  Node command tool to generate TypeScript coverage report.  Options:   -V, --version                  版本号   -o, --outputDir [string]       设置生产报告的位置(输出目录)   -t, --threshold [number]       需要的最低覆盖率. (默认值: 80)   -s, --strict [boolean]         是否开启严格模式. (默认值: 否)   -d, --debug [boolean]          是否显示调试信息. (默认值: 否)   -c, --cache [boolean]          保存并复用缓存中的类型检查结果. (默认值: 否)   -p, --project [string]         tsconfig 文件的文件路径, eg: --project "./app/tsconfig.app.json" (默认值: ".")   -i, --ignore-files [string[]]  忽略指定文件, eg: --ignore-files "demo1/*.ts" --ignore-files "demo2/foo.ts" (默认值: 否)   --ignore-catch [boolean]       ignore type any for (try-)catch clause variable (default: false)   -u, --ignore-unread [boolean]  允许写入具有Any类型的变量 (default: 否)   -h, --help                     显示帮助信息 

控制台的输出

源码解读之TypeScript类型覆盖检测工具type-coverage

上述命令执行后默认会在根目录生成coverage-ts文件夹,下面给出目录结构

. ├── assets │   ├── source-file.css │   └── source-file.js ├── files │   └── src ├── index.html └── typescript-coverage.json 

打开coverage-ts/index.html文件,能够以网页的方式查看更加详细的报告内容。

源码解读之TypeScript类型覆盖检测工具type-coverage

通过网页版本的报告能够更直观的帮助我们来进行专项的优化。

源码解读之TypeScript类型覆盖检测工具type-coverage

VSCode插件

工具库还提供了VSCVod插件版本,在VSCode插件管理器中,搜索然后安装插件后,在代码编写阶段就能够得到类型提示。

源码解读之TypeScript类型覆盖检测工具type-coverage

可以通过 Preferences - Settings - Extensions - Type Coverage 来对插件进行配置,如果 VSCode 插件的结果与 CLI 的结果不同,则有可能项目根目录的 tsconfig.json 与 CLI tsconfig.json 配置不同或存在冲突。

如果插件无法正常工作,可以参考下issues/86#issuecomment找找看有没有对应的解决办法

Pull PR中使用

当然,我们也可以把该工具集成到
源码解读之TypeScript类型覆盖检测工具type-coverage

四、内部结构和工作原理

项目的结构

. ├── cli │   ├── README.md │   ├── bin │   │   └── type-coverage │   ├── package.json │   └── src │       ├── index.ts │       ├── lib.d.ts │       └── tsconfig.json ├── core │   ├── README.md │   ├── package.json │   └── src │       ├── cache.ts │       ├── checker.ts │       ├── core.ts │       ├── dependencies.ts │       ├── ignore.ts │       ├── index.ts │       ├── interfaces.ts │       ├── tsconfig.json │       └── tsconfig.ts ├── plugin │   ├── README.md │   ├── package.json │   └── src │       ├── index.ts │       └── tsconfig.json └── vscode     ├── README.md     ├── package.json     └── src         ├── index.ts         └── tsconfig.json  9 directories, 25 files 

核心模块的关系

源码解读之TypeScript类型覆盖检测工具type-coverage

核心方法梳理

命令行工具的调用逻辑

async function executeCommandLine() {   /* 接收命令行参数 */   const argv = minimist(process.argv.slice(2), { '--': true }) as unknown as CliArgs    const { ... 省略 } = await getTarget(argv);    /* 核心主流程 */   const { correctCount, totalCount, anys } = await lint(project, {... 省略配置项});    /* 计算类型覆盖率 */   const percent = Math.floor(10000 * correctCount / totalCount) / 100   const percentString = percent.toFixed(2)   console.log(`${correctCount} / ${totalCount} ${percentString}%`) } 

主流程(Core)

//core.ts  const defaultLintOptions: LintOptions = {   debug: false,   files: undefined,   oldProgram: undefined,   strict: false,   enableCache: false,   ignoreCatch: false,   ignoreFiles: undefined,   ignoreUnreadAnys: false,   fileCounts: false,   ignoreNested: false,   ignoreAsAssertion: false,   ignoreTypeAssertion: false,   ignoreNonNullAssertion: false,   ignoreObject: false,   ignoreEmptyType: false,   reportSemanticError: false, }   /* 核心函数:主流程 */ export async function lint(project: string, options?: Partial<LintOptions>) {   /* 检测的前置条件(参数) */   const lintOptions = { ...defaultLintOptions, ...options }      /* 获取项目的根目录和编译选项 */   const { rootNames, compilerOptions } = await getProjectRootNamesAndCompilerOptions(project)    /* 通过ts创建处理程序 */   const program = ts.createProgram(rootNames, compilerOptions, undefined, lintOptions.oldProgram)    /* 获取类型检查器 */   const checker = program.getTypeChecker()    /* 声明用于保存文件的集合 */   const allFiles = new Set<string>()   /* 声明用于保存文件信息的数组 */   const sourceFileInfos: SourceFileInfo[] = []   /* 根据配置参数从缓存中读取类型检查结果(缓存的数据) */   const typeCheckResult = await readCache(lintOptions.enableCache, lintOptions.cacheDirectory)   /* 读取配置参数中的忽略文件信息 */   const ignoreFileGlobs = lintOptions.ignoreFiles     ? (typeof lintOptions.ignoreFiles === 'string'       ? [lintOptions.ignoreFiles]       : lintOptions.ignoreFiles)     : undefined      /* 获取所有的SourceFiles并遍历 */   for (const sourceFile of program.getSourceFiles()) {     let file = sourceFile.fileName     if (!file.includes('node_modules')) {       /* 如果不是绝对路径 */       if (!lintOptions.absolutePath) {         /* process.cwd() 是当前Node进程执行时的文件夹地址,也就是工作目录,保证了文件在不同的目录下执行时,路径始终不变 */         /* __dirname 是被执行的js文件的地址,也就是文件所在目录 */         /* 计算得到文件的相对路径 */         file = path.relative(process.cwd(), file)         /* 如果路径以..开头则跳过该文件 */         if (file.startsWith('..')) {           continue         }       }       /* 如果lintOptions.files中不包含该文件,则跳过 */       if (lintOptions.files && !lintOptions.files.includes(file)) {         continue       }       /* 如果该文件存在于忽略配置项中,则跳过 */       if (ignoreFileGlobs && ignoreFileGlobs.some((f) => minimatch(file, f))) {         continue       }       /* 添加文件到集合 */       allFiles.add(file)        /* 计算文件的哈希值 */       const hash = await getFileHash(file, lintOptions.enableCache)        /* 检查该文件是否存在缓存数据 */       const cache = typeCheckResult.cache[file]        /* 如果存在缓存数据 */       if (cache) {         /* 如果配置项定义了ignoreNested 则忽略 嵌套的any */         if (lintOptions.ignoreNested) {           cache.anys = cache.anys.filter((c) => c.kind !== FileAnyInfoKind.containsAny)         }          /* 如果配置项定义了ignoreAsAssertion 则忽略 不安全的as */         if (lintOptions.ignoreAsAssertion) {           cache.anys = cache.anys.filter((c) => c.kind !== FileAnyInfoKind.unsafeAs)         }         /* 如果配置项定义了ignoreTypeAssertion 则忽略 不安全的类型断言 */         if (lintOptions.ignoreTypeAssertion) {           cache.anys = cache.anys.filter((c) => c.kind !== FileAnyInfoKind.unsafeTypeAssertion)         }          /* 如果配置项定义了ignoreNonNullAssertion 则忽略 不安全的非空断言 */         if (lintOptions.ignoreNonNullAssertion) {           cache.anys = cache.anys.filter((c) => c.kind !== FileAnyInfoKind.unsafeNonNull)         }       }        /* 更新sourceFileInfos对象数组 */       sourceFileInfos.push({         file, /* 文件路径 */         sourceFile,         hash,/* 哈希值 */         cache: cache && cache.hash === hash ? cache : undefined /* 该文件的缓存信息 */       })     }   }    /* 如果启用了缓存 */   if (lintOptions.enableCache) {     /* 获取依赖集合 */     const dependencies = collectDependencies(sourceFileInfos, allFiles)      /* 遍历sourceFileInfos */     for (const sourceFileInfo of sourceFileInfos) {       /* 如果没有使用缓存,那就清理依赖 */       if (!sourceFileInfo.cache) {         clearCacheOfDependencies(sourceFileInfo, dependencies, sourceFileInfos)       }     }   }    let correctCount = 0   let totalCount = 0    /* 声明anys数组 */   const anys: AnyInfo[] = []   /* 声明fileCounts映射 */   const fileCounts =     new Map<string, Pick<FileTypeCheckResult, 'correctCount' | 'totalCount'>>()  /* 遍历sourceFileInfos */   for (const { sourceFile, file, hash, cache } of sourceFileInfos) {     /* 如果存在缓存,那么直接根据缓存处理后就跳过 */     if (cache) {       /* 累加correctCount和totalCount */       correctCount += cache.correctCount       totalCount += cache.totalCount        /* 把缓存的anys合并到anys数据中 */       anys.push(...cache.anys.map((a) => ({ file, ...a })))        if (lintOptions.fileCounts) {         /* 统计每个文件的数据 */         fileCounts.set(file, {           correctCount: cache.correctCount,           totalCount: cache.totalCount,         })       }       continue     }      /* 获取忽略的集合 */     const ingoreMap = collectIgnoreMap(sourceFile, file)      /* 组织上下文对象 */     const context: FileContext = {       file,       sourceFile,       typeCheckResult: {         correctCount: 0,         totalCount: 0,         anys: []       },       ignoreCatch: lintOptions.ignoreCatch,       ignoreUnreadAnys: lintOptions.ignoreUnreadAnys,       catchVariables: {},       debug: lintOptions.debug,       strict: lintOptions.strict,       processAny: lintOptions.processAny,       checker,       ingoreMap,       ignoreNested: lintOptions.ignoreNested,       ignoreAsAssertion: lintOptions.ignoreAsAssertion,       ignoreTypeAssertion: lintOptions.ignoreTypeAssertion,       ignoreNonNullAssertion: lintOptions.ignoreNonNullAssertion,       ignoreObject: lintOptions.ignoreObject,       ignoreEmptyType: lintOptions.ignoreEmptyType,     }      /* 关键流程:单个文件遍历所有的子节点 */     sourceFile.forEachChild(node => {       /* 检测节点,并更新context的值 */       /* ?为什么选择引用传递?? */       checkNode(node, context)     })      /* 更新correctCount  把当前文件的数据累加上*/     correctCount += context.typeCheckResult.correctCount     /* 更新totalCount 把当前文件的数据累加上*/     totalCount += context.typeCheckResult.totalCount      /* 把当前文件的anys数据累加 */     anys.push(...context.typeCheckResult.anys.map((a) => ({ file, ...a })))      if (lintOptions.reportSemanticError) {       const diagnostics = program.getSemanticDiagnostics(sourceFile)       for (const diagnostic of diagnostics) {         if (diagnostic.start !== undefined) {           totalCount++           let text: string           if (typeof diagnostic.messageText === 'string') {             text = diagnostic.messageText           } else {             text = diagnostic.messageText.messageText           }           const { line, character } = ts.getLineAndCharacterOfPosition(sourceFile, diagnostic.start)           anys.push({             line,             character,             text,             kind: FileAnyInfoKind.semanticError,             file,           })         }       }     }      /* 如果需要统计每个文件的信息 */     if (lintOptions.fileCounts) {       /* 更新当前文件的统计结果 */       fileCounts.set(file, {         correctCount: context.typeCheckResult.correctCount,         totalCount: context.typeCheckResult.totalCount       })     }      /* 如果启用了缓存 */     if (lintOptions.enableCache) {       /* 把本次计算的结果保存一份到缓存对象中 */       const resultCache = typeCheckResult.cache[file]       /* 如果该缓存对象已经存在,那么就更新数据,否则那就新建缓存对象 */       if (resultCache) {         resultCache.hash = hash         resultCache.correctCount = context.typeCheckResult.correctCount         resultCache.totalCount = context.typeCheckResult.totalCount         resultCache.anys = context.typeCheckResult.anys       } else {         typeCheckResult.cache[file] = {           hash,           ...context.typeCheckResult         }       }     }   }    /* 再操作的最后,检查是否启用了缓存 */   if (lintOptions.enableCache) {     /* 如果启用了缓存,那就把缓存数据保存起来 */     await saveCache(typeCheckResult, lintOptions.cacheDirectory)   }    // 返回计算的结果   return { correctCount, totalCount, anys, program, fileCounts } } 

core.ts文件中,提供了两个同步和异步两种方式来执行任务,他们分别是lintlintSync,上面的代码给出了核心代码和部分注释,整体来看处理逻辑比较简单,这里给出源码中这些函数间的关系图。

源码解读之TypeScript类型覆盖检测工具type-coverage

核心逻辑就是根据目录先获取所有的文件,然后再遍历这些文件,接着依次处理每个文件中的标识符,统计correctCount、totalCount和anys。

关键模块(checker)

//checkNode方法 核心检测函数(递归调用) export function checkNode(node: ts.Node | undefined, context: FileContext): void {   if (node === undefined) {     return   }    if (context.debug) {     const { line, character } = ts.getLineAndCharacterOfPosition(context.sourceFile, node.getStart(context.sourceFile))     console.log(`node: ${context.file}:${line + 1}:${character + 1}: ${node.getText(context.sourceFile)} ${node.kind}(kind)`)   }    checkNodes(node.decorators, context)   checkNodes(node.modifiers, context)    if (skippedNodeKinds.has(node.kind)) {     return   }    /* 关键字 */   if (node.kind === ts.SyntaxKind.ThisKeyword) {     collectData(node, context)     return   }   /* 标识符 */   if (ts.isIdentifier(node)) {     if (context.catchVariables[node.escapedText as string]) {       return     }     collectData(node, context)     return   }   /* 其他处理 */   if (ts.isQualifiedName(node)) {     checkNode(node.left, context)     checkNode(node.right, context)     return   } }  // Nodes 检查 function checkNodes(nodes: ts.NodeArray<ts.Node> | undefined, context: FileContext): void {   if (nodes === undefined) {     return   }   for (const node of nodes) {     checkNode(node, context)   } }  /* 收集器 */ function collectData(node: ts.Node, context: FileContext) {   const types: ts.Type[] = []   const type = context.checker.getTypeAtLocation(node)   if (type) {     types.push(type)   }   const contextualType = context.checker.getContextualType(node as ts.Expression)   if (contextualType) {     types.push(contextualType)   }    if (types.length > 0) {     context.typeCheckResult.totalCount++     if (types.every((t) => typeIsAnyOrInTypeArguments(t, context.strict && !context.ignoreNested, context))) {       const kind = types.every((t) => typeIsAnyOrInTypeArguments(t, false, context)) ? FileAnyInfoKind.any : FileAnyInfoKind.containsAny       const success = collectAny(node, context, kind)       if (!success) {         //收集所有的any         collectNotAny(node, context, type)       }     } else {       //收集所有的 notAny       collectNotAny(node, context, type)     }   } } 

主流程和checker类型检查模块的函数调用关系

源码解读之TypeScript类型覆盖检测工具type-coverage

项目源码中核心函数间的调用关系图

源码解读之TypeScript类型覆盖检测工具type-coverage

按照功能模块来划分的话,主要包含主函数lint、缓存处理(saveCache、readCache等)、和类型检查(checkNode等),其中checkNode中涉及到了很多Node节点的类型,而ts.相关的方法也都值得关注。

四、拓展内容

type-coverage项目依赖的主要模块(包)

模块(包) 描述
Definitely Typed TypeScript 类型定义
minimist 命令行参数解析器
fast-glob 文件系统操作
minimatch 路径匹配库
normalize-path 规范化路径
clean-scripts 元文件脚本清理CLI
clean-release 文件操作的CLI
rimraf 删除文件(夹)
tslib TS运行时库
tsutils Typescript工具函数

部门依赖(模块)的补充说明

Definitely Typed这是一个TypeScript 类型定义的仓库,下面这些依赖都根植于这个库

@types/minimatch @types/minimist @types/node @types/normalize-path 

minimist是参数解析器核心库。

在项目中的使用场景

//文件路径 packages/cli/src/index.ts import minimist = require('minimist')  //... const argv = minimist(process.argv.slice(2), { '--': true }) as unknown as CliArgs const showVersion = argv.v || argv.version ... 

fast-glob遍历文件系统并根据 Unix Bash shell 使用的规则返回匹配指定模式的路径名集合。

项目中的使用场景

//文件路径 /packages/core/src/tsconfig.ts import fg = require('fast-glob') //... const includeFiles = await fg(rules, {       ignore,       cwd: dirname,     }) files.push(...includeFiles) 

minimatch是 npm 内部使用的匹配库,它通过将 glob 表达式转换为 JavaScriptRegExp 对象来工作。

项目中的使用场景

//文件路径 /packages/core/src/core.ts#L3 import minimatch = require('minimatch') //... if (ignoreFileGlobs && ignoreFileGlobs.some((f) => minimatch(file, f))) {   continue } 

具体的使用示例

minimatch("bar.foo", "*.foo") // true! minimatch("bar.foo", "*.bar") // false! minimatch("bar.foo", "*.+(bar|foo)", { debug: true }) // true, and noisy!  minimatch('/a/b', '/a/*/c/d', { partial: true })  // true, might be /a/b/c/d minimatch('/a/b', '/**/d', { partial: true })     // true, might be /a/b/.../d minimatch('/x/y/z', '/a/**/z', { partial: true }) // false, because x !== a 

normalize-path规范化路径的库。

项目中的使用场景

import normalize = require('normalize-path') async function getRootNames(config: JsonConfig, dirname: string) { //...  let rules: string[] = [] for (const file of include) {   const currentPath = path.resolve(dirname, file)   const stats = await statAsync(currentPath)   if (stats === undefined || stats.isFile()) {     rules.push(currentPath)   } else if (stats.isDirectory()) {     rules.push(`${currentPath.endsWith('/') ? currentPath.substring(0, currentPath.length - 1) : currentPath}/**/*`)   } }  rules = rules.map((r) => normalize(r)) ignore = ignore.map((r) => normalize(r)) } 

clean-scripts是使 package.json脚本干净的 CLI 工具。

项目中的使用场景

//文件路径  /clean-scripts.config.ts import { Tasks, readWorkspaceDependencies } from 'clean-scripts'  const tsFiles = `"packages/**/src/**/*.ts"`  const workspaces = readWorkspaceDependencies()  export default {   build: [     new Tasks(workspaces.map((d) => ({       name: d.name,       script: [         `rimraf ${d.path}/dist/`,         `tsc -p ${d.path}/src/`,       ],       dependencies: d.dependencies     }))),     ...workspaces.map((d) => `node packages/cli/dist/index.js -p ${d.path}/src --detail --strict --suppressError`)   ],   lint: {     ts: `eslint --ext .js,.ts ${tsFiles}`,     export: `no-unused-export ${tsFiles} --need-module tslib --need-module ts-plugin-type-coverage --ignore-module vscode --strict`,     markdown: `markdownlint README.md`   },   test: [],   fix: `eslint --ext .js,.ts ${tsFiles} --fix` } 

在上面的build命令中发现了rimraf,在lint命令中发现了tslib这个工具包。

tslib是一个用于TypeScript的运行时库,其中包含所有 TypeScript 辅助函数。

rimraf以包的形式包装rm -rf命令,用来删除文件和文件夹的,不管文件夹是否为空,都可删除。

适用场景项目中build文件的时候每次都会生成一个dist目录,有时需要把dist目录里的所以旧文件全部删掉,就可以使用rimraf命令

安装和使用

$ pnpm install rimraf -g 

clean-release是 CLI 工具,用于将要发布的文件复制到tmp clean 目录中,用于 npm 发布、electronjs打包、docker映像创建或部署。

项目中的使用

//文件路径  /clean-release.config.ts#L8 import { Configuration } from 'clean-release'  const config: Configuration = {   include: [     'packages/*/dist/*',     'packages/*/es/*',     'packages/*/bin/*',     'packages/*/package.json',     'packages/*/README.md',   ],   exclude: [   ],   askVersion: true,   changesGitStaged: true,   postScript: ({ dir, tag, version, effectedWorkspacePaths = [] }) => [     ...effectedWorkspacePaths.map((w) => w.map((e) => {       if (e === 'packages/vscode') {         return tag ? undefined : `cd "${dir}/${e}" && yarn install --registry=https://registry.npmjs.org/ && rm -f "${dir}/yarn.lock" && vsce publish ${version}`       }       return tag         ? `npm publish "${dir}/${e}" --access public --tag ${tag}`         : `npm publish "${dir}/${e}" --access public`     })),     `git-commits-to-changelog --release ${version}`,     'git add CHANGELOG.md',     `git commit -m "${version}"`,     `git tag -a v${version} -m 'v${version}'`,     'git push',     `git push origin v${version}`   ] } export default config 

tsutilsTypescript工具函数。

在type-coverage的项目中,该工具库用来处理忽略,即当注释中存在type-coverage:ignore-linetype-coverage:ignore-next-line的时候,这部分代码将不被记入到类型覆盖率的计算中。

下面列出项目中使用该工具的代码部分

//文件路径:/packages/core/src/ignore.ts#L4 import * as ts from 'typescript' import * as utils from 'tsutils/util'  export function collectIgnoreMap(sourceFile: ts.SourceFile, file: string) {   const ingoreMap: { [file: string]: Set<number> } = {}   utils.forEachComment(sourceFile, (_, comment) => {     const commentText = comment.kind === ts.SyntaxKind.SingleLineCommentTrivia       ? sourceFile.text.substring(comment.pos + 2, comment.end).trim()       : sourceFile.text.substring(comment.pos + 2, comment.end - 2).trim()     if (commentText.includes('type-coverage:ignore-next-line')) {       if (!ingoreMap[file]) {         ingoreMap[file] = new Set()       }       const line = ts.getLineAndCharacterOfPosition(sourceFile, comment.pos).line       ingoreMap[file]?.add(line + 1)     } else if (commentText.includes('type-coverage:ignore-line')) {       if (!ingoreMap[file]) {         ingoreMap[file] = new Set()       }       const line = ts.getLineAndCharacterOfPosition(sourceFile, comment.pos).line       ingoreMap[file]?.add(line)     }   })    return ingoreMap } 

发表评论

相关文章