源碼解讀之TypeScript類型覆蓋檢測工具type-coverage

来源:https://www.cnblogs.com/wendingding/archive/2022/12/18/16985448.html
-Advertisement-
Play Games

因為團隊內部開啟了一個持續的前端代碼質量改進計劃,其中一個專項就是TS類型覆蓋率,期間用到了type-coverage這個倉庫,所以借這篇文章分享一下這個工具,並順便從源碼閱讀的角度來分析一下該工具的源碼,我自己fork了一個倉庫,完成了中文版本的ReadMe文件並對核心代碼添加了關鍵註釋,需要的同 ...


因為團隊內部開啟了一個持續的前端代碼質量改進計劃,其中一個專項就是TS類型覆蓋率,期間用到了type-coverage這個倉庫,所以借這篇文章分享一下這個工具,並順便從源碼閱讀的角度來分析一下該工具的源碼,我自己fork了一個倉庫,完成了中文版本的ReadMe文件並對核心代碼添加了關鍵註釋,需要的同學可以點擊傳送門

一、基本介紹

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                     顯示幫助信息

控制台的輸出

上述命令執行後預設會在根目錄生成coverage-ts文件夾,下麵給出目錄結構

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

打開coverage-ts/index.html文件,能夠以網頁的方式查看更加詳細的報告內容。

通過網頁版本的報告能夠更直觀的幫助我們來進行專項的優化。

VSCode插件

工具庫還提供了VSCVod插件版本,在VSCode插件管理器中,搜索然後安裝插件後,在代碼編寫階段就能夠得到類型提示。

可以通過 Preferences - Settings - Extensions - Type Coverage 來對插件進行配置,如果 VSCode 插件的結果與 CLI 的結果不同,則有可能項目根目錄的 tsconfig.json 與 CLI tsconfig.json 配置不同或存在衝突。

如果插件無法正常工作,可以參考下issues/86#issuecomment找找看有沒有對應的解決辦法

Pull PR中使用

當然,我們也可以把該工具集成到

四、內部結構和工作原理

項目的結構

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

核心模塊的關係

核心方法梳理

命令行工具的調用邏輯

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,上面的代碼給出了核心代碼和部分註釋,整體來看處理邏輯比較簡單,這裡給出源碼中這些函數間的關係圖。

核心邏輯就是根據目錄先獲取所有的文件,然後再遍歷這些文件,接著依次處理每個文件中的標識符,統計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類型檢查模塊的函數調用關係

項目源碼中核心函數間的調用關係圖

按照功能模塊來劃分的話,主要包含主函數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
}
原創文章,訪問個人站點 文頂頂 以獲得更好的閱讀體驗。 版權聲明:著作權歸作者所有,商業轉載請聯繫文頂頂獲得授權,非商業轉載請註明出處。
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 問題描述 fastjson通過代碼指定全局序列化返回時間格式,導致使用JSONField註解標註屬性的特殊日期返回格式失效 使用版本 | 應用名稱 | 版本 | | | | | springboot | 2.0.0.RELEASE | | fastjson | 1.2.83 | 全局設置代碼 pub ...
  • 一、前言 最近學習pyqt5中文教程時,最後一個例子製作了一個俄羅斯方塊小游戲,由於解釋的不是很清楚,所以源碼有點看不懂,查找網上資料後,大概弄懂了源碼的原理。 二、繪製主視窗 將主視窗居中,且設置了一個狀態欄來顯示三種信息:消除的行數,游戲暫停狀態或者游戲結束狀態。 class Tetris(QM ...
  • JZ46 把數字翻譯成字元串 描述 有一種將字母編碼成數字的方式:'a'->1, 'b->2', ... , 'z->26'。 現在給一串數字,返回有多少種可能的解碼結果 示例1 輸入: "12" 返回值:2 說明: 2種可能的解碼結果(”ab” 或”l”) 思路 思路: 對於普通數組1-9,解碼方 ...
  • 1、設計想法 原理與之前的串口發送模塊一樣,1位的數據位和8位的數據位再加上1位的停止位。唯一不同的是在接收的時候要考慮到有干擾的情況下,為了避免干擾,我們對每位數據進行多次採樣,按出現概率大的值為該數據位的值。 如果按照通常想法在每bits位中間取值的話,bit3位出現圖中的干擾很有可能會讀出錯誤 ...
  • win11特有的快捷鍵 win鍵就是圖案是windows圖標的那個按鍵 | 作用 | 快捷鍵 | | | | | 打開快速設置,win11是展開音量,wifi,藍牙的設置項,win10也可以用 | win + a | | 打開通知中心和日曆,win10無 | win + n | | 打開投屏,win ...
  • RDP,Remote Desktop Protocol,遠程桌面協議,是一個多通道(mutil-channel)的協議,讓用戶(客戶端或稱“本地電腦”)連上提供微軟終端機服務的電腦(伺服器端或稱“遠程電腦”)。大部分的Windows、Linux、FreeBSD、Mac OS X都有相應的客戶端。服務... ...
  • 1. 判斷本地是否已經安裝MySQL ① 在運行界面輸入services.msc進入服務界面,查看是否有MySQL服務 ② 進入任務管理器,點擊服務看是否有MySQL服務 2. 安裝MySQL(壓縮包版) 1. 下載MySQL社區伺服器(ZIP): MySQL zip下載 點擊No thanks,j ...
  • 本文是 CSS Houdini 之 CSS Painting API 系列第四篇。 現代 CSS 之高階圖片漸隱消失術 現代 CSS 高階技巧,像 Canvas 一樣自由繪圖構建樣式! 現代 CSS 高階技巧,完美的波浪進度條效果! 在上三篇中,我們詳細介紹了 CSS Painting API 是如 ...
一周排行
    -Advertisement-
    Play Games
  • .Net8.0 Blazor Hybird 桌面端 (WPF/Winform) 實測可以完整運行在 win7sp1/win10/win11. 如果用其他工具打包,還可以運行在mac/linux下, 傳送門BlazorHybrid 發佈為無依賴包方式 安裝 WebView2Runtime 1.57 M ...
  • 目錄前言PostgreSql安裝測試額外Nuget安裝Person.cs模擬運行Navicate連postgresql解決方案Garnet為什麼要選擇Garnet而不是RedisRedis不再開源Windows版的Redis是由微軟維護的Windows Redis版本老舊,後續可能不再更新Garne ...
  • C#TMS系統代碼-聯表報表學習 領導被裁了之後很快就有人上任了,幾乎是無縫銜接,很難讓我不想到這早就決定好了。我的職責沒有任何變化。感受下來這個系統封裝程度很高,我只要會調用方法就行。這個系統交付之後不會有太多問題,更多應該是做小需求,有大的開發任務應該也是第二期的事,嗯?怎麼感覺我變成運維了?而 ...
  • 我在隨筆《EAV模型(實體-屬性-值)的設計和低代碼的處理方案(1)》中介紹了一些基本的EAV模型設計知識和基於Winform場景下低代碼(或者說無代碼)的一些實現思路,在本篇隨筆中,我們來分析一下這種針對通用業務,且只需定義就能構建業務模塊存儲和界面的解決方案,其中的數據查詢處理的操作。 ...
  • 對某個遠程伺服器啟用和設置NTP服務(Windows系統) 打開註冊表 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\NtpServer 將 Enabled 的值設置為 1,這將啟用NTP伺服器功 ...
  • title: Django信號與擴展:深入理解與實踐 date: 2024/5/15 22:40:52 updated: 2024/5/15 22:40:52 categories: 後端開發 tags: Django 信號 松耦合 觀察者 擴展 安全 性能 第一部分:Django信號基礎 Djan ...
  • 使用xadmin2遇到的問題&解決 環境配置: 使用的模塊版本: 關聯的包 Django 3.2.15 mysqlclient 2.2.4 xadmin 2.0.1 django-crispy-forms >= 1.6.0 django-import-export >= 0.5.1 django-r ...
  • 今天我打算整點兒不一樣的內容,通過之前學習的TransformerMap和LazyMap鏈,想搞點不一樣的,所以我關註了另外一條鏈DefaultedMap鏈,主要調用鏈為: 調用鏈詳細描述: ObjectInputStream.readObject() DefaultedMap.readObject ...
  • 後端應用級開發者該如何擁抱 AI GC?就是在這樣的一個大的浪潮下,我們的傳統的應用級開發者。我們該如何選擇職業或者是如何去快速轉型,跟上這樣的一個行業的一個浪潮? 0 AI金字塔模型 越往上它的整個難度就是職業機會也好,或者說是整個的這個運作也好,它的難度會越大,然後越往下機會就會越多,所以這是一 ...
  • @Autowired是Spring框架提供的註解,@Resource是Java EE 5規範提供的註解。 @Autowired預設按照類型自動裝配,而@Resource預設按照名稱自動裝配。 @Autowired支持@Qualifier註解來指定裝配哪一個具有相同類型的bean,而@Resourc... ...