因為團隊內部開啟了一個持續的前端代碼質量改進計劃,其中一個專項就是TS類型覆蓋率,期間用到了type-coverage這個倉庫,所以借這篇文章分享一下這個工具,並順便從源碼閱讀的角度來分析一下該工具的源碼,我自己fork了一個倉庫,完成了中文版本的ReadMe文件並對核心代碼添加了關鍵註釋,需要的同 ...
因為團隊內部開啟了一個持續的前端代碼質量改進計劃,其中一個專項就是TS類型覆蓋率,期間用到了type-coverage這個倉庫,所以借這篇文章分享一下這個工具,並順便從源碼閱讀的角度來分析一下該工具的源碼,我自己fork了一個倉庫,完成了中文版本的ReadMe文件並對核心代碼添加了關鍵註釋,需要的同學可以點擊傳送門。
一、基本介紹
![](http://img.wendingding.vip/blog/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 顯示幫助信息
控制台的輸出
![](http://img.wendingding.vip/blog/type-coverage02.png)
上述命令執行後預設會在根目錄生成coverage-ts
文件夾,下麵給出目錄結構
.
├── assets
│ ├── source-file.css
│ └── source-file.js
├── files
│ └── src
├── index.html
└── typescript-coverage.json
打開coverage-ts/index.html
文件,能夠以網頁的方式查看更加詳細的報告內容。
![](http://img.wendingding.vip/blog/type-coverage03.png)
通過網頁版本的報告能夠更直觀的幫助我們來進行專項的優化。
![](http://img.wendingding.vip/blog/type-coverage04.png)
VSCode插件
工具庫還提供了VSCVod插件版本,在VSCode插件管理器中,搜索然後安裝插件後,在代碼編寫階段就能夠得到類型提示。
![](http://img.wendingding.vip/blog/type-coverage07.png)
可以通過 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
核心模塊的關係
![](http://img.wendingding.vip/blog/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
文件中,提供了兩個同步和非同步兩種方式來執行任務,他們分別是lint
和lintSync
,上面的代碼給出了核心代碼和部分註釋,整體來看處理邏輯比較簡單,這裡給出源碼中這些函數間的關係圖。
![](http://img.wendingding.vip/blog/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類型檢查模塊的函數調用關係
![](http://img.wendingding.vip/blog/type-coverage08.png)
項目源碼中核心函數間的調用關係圖
![](http://img.wendingding.vip/blog/type-coverage10.png)
按照功能模塊來劃分的話,主要包含主函數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-line
和type-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
}
原創文章,訪問個人站點 文頂頂 以獲得更好的閱讀體驗。
版權聲明:著作權歸作者所有,商業轉載請聯繫文頂頂獲得授權,非商業轉載請註明出處。