2

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

 1 year ago
source link: https://www.cnblogs.com/wendingding/p/16985448.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

一、基本介绍

type-coverage06.png

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                     显示帮助信息

控制台的输出

type-coverage02.png

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

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

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

type-coverage03.png

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

type-coverage04.png

VSCode插件

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

type-coverage07.png

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

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

Pull PR中使用

当然,我们也可以把该工具集成到

type-coverage01.png

四、内部结构和工作原理

项目的结构

.
├── 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

核心模块的关系

type-coverage05.png

核心方法梳理

命令行工具的调用逻辑

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,上面的代码给出了核心代码和部分注释,整体来看处理逻辑比较简单,这里给出源码中这些函数间的关系图。

type-coverage09.png

核心逻辑就是根据目录先获取所有的文件,然后再遍历这些文件,接着依次处理每个文件中的标识符,统计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类型检查模块的函数调用关系

type-coverage08.png

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

type-coverage10.png

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

四、拓展内容

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

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

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
}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK