TypeScript、Rollup 搭建工具庫
http://blog.maihaoche.com/typescript-rollup-da-jian-gong-ju-ku/
前景提要
公司內總是有許多通用的工具方法、業務功能,我們可以搭建一個工具庫來給各個項目使用。
要實現的需求:🤔
- 支持編輯器的快速補全和提示
- 自動化構建
- 支持自動生成 changlog
- 代碼通過 lint 和測試后才能提交、發布
涉及的庫
- eslint + @typescript-eslint/parser
- rollup
- jest
- @microsoft/api-extractor
- gulp
初始化項目
新建一個項目目錄如 fly-helper
, 並 npm init
初始化項目。
安裝 TypeScript
yarn add -D typescript
創建 src
目錄,入口文件,以及 ts 的配置文件
fly-helper
| |- src |- index.ts |- tsconfig.json
配置 tsconfig.json
/* tsconfig.json */
{
"compilerOptions": { /* 基礎配置 */ "target": "esnext", "lib": [ "dom", "esnext" ], "removeComments": false, "declaration": true, "sourceMap": true, /* 強類型檢查配置 */ "strict": true, "noImplicitAny": false, /* 模塊分析配置 */ "baseUrl": ".", "outDir": "./lib", "esModuleInterop": true, "moduleResolution": "node", "resolveJsonModule": true }, "include": [ "src" ] }
參考 commit
Ps:commit 中還增加了 .editorconfig ,來約束同學們的代碼格式
配置 eslint
TypeScirpt 已經全面采用 ESLint 作為代碼檢查 The future of TypeScript on ESLint
並且提供了 TypeScript 文件的解析器 @typescript-eslint/parser 和配置選項 @typescript-eslint/eslint-plugin
安裝
yarn add -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
目錄結構
fly-helper
|- .eslintignore |- .eslintrc.js |- tsconfig.eslint.json
Ps
tsconfig.eslint.json 我們根目錄中增加了一個 tsconfig 文件,它將用於 eslintrc.parserOptions.project
,由於該配置要求 incude 每個 ts、js 文件。而我們僅需要打包 src 目錄下的代碼,所以增加了該配置文件。
如果 eslintrc.parserOptions.project
配置為 tsconfig.json 。src 文件以外的 ts、js 文件都會報錯。
Parsing error: "parserOptions.project" has been set for @typescript-eslint/parser. The file does not match your project config: config.ts. The file must be included in at least one of the projects provided.eslint
雖然可以配置 eslintrc.parserOptions.createDefaultProgram
但會造成巨大的性能損耗。
issus: Parsing error: "parserOptions.project"...
配置 tsconfig.eslint.json
/* tsconfig.eslint.json */
{
"compilerOptions": { "baseUrl": ".", "resolveJsonModule": true, }, "include": [ "**/*.ts", "**/*.js" ] }
配置 .eslintrc.js
// .eslintrc.js const eslintrc = { parser: '@typescript-eslint/parser', // 使用 ts 解析器 extends: [ 'eslint:recommended', // eslint 推薦規則 'plugin:@typescript-eslint/recommended', // ts 推薦規則 ], plugins: [ '@typescript-eslint', ], env: { browser: true, node: true, es6: true, }, parserOptions: { project: './tsconfig.eslint.json', ecmaVersion: 2019, sourceType: 'module', ecmaFeatures: { experimentalObjectRestSpread: true } }, rules: {}, // 自定義 } module.exports = eslintrc
參考 commit
配置 rollup
vue、react 等許多流行庫都在使用 Rollup.js ,就不多介紹,直接看 官網 吧🤯
安裝
安裝 rollup 以及要用到的插件
yarn add -D rollup rollup-plugin-babel rollup-plugin-commonjs rollup-plugin-eslint rollup-plugin-node-resolve rollup-plugin-typescript2
安裝 babel 相關的庫
yarn add -D @babel/preset-env
目錄結構
fly-helper
| |- typings |- index.d.ts |- .babelrc |- rollup.config.ts
配置 .babelrc
/* .babelrc */
{
"presets": [ [ "@babel/preset-env", { /* Babel 會在 Rollup 有機會做處理之前,將我們的模塊轉成 CommonJS,導致 Rollup 的一些處理失敗 */ "modules": false } ] ] }
配置 rollup.config.ts
import path from 'path'
import { RollupOptions } from 'rollup'
import rollupTypescript from 'rollup-plugin-typescript2'
import babel from 'rollup-plugin-babel'
import resolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs'
import { eslint } from 'rollup-plugin-eslint'
import { DEFAULT_EXTENSIONS } from '@babel/core'
import pkg from './package.json'
const paths = {
input: path.join(__dirname, '/src/index.ts'),
output: path.join(__dirname, '/lib'),
}
// rollup 配置項
const rollupConfig: RollupOptions = {
input: paths.input,
output: [
// 輸出 commonjs 規范的代碼
{
file: path.join(paths.output, 'index.js'),
format: 'cjs',
name: pkg.name,
},
// 輸出 es 規范的代碼
{
file: path.join(paths.output, 'index.esm.js'),
format: 'es',
name: pkg.name,
},
],
// external: ['lodash'], // 指出應將哪些模塊視為外部模塊,如 Peer dependencies 中的依賴
// plugins 需要注意引用順序
plugins: [
// 驗證導入的文件
eslint({
throwOnError: true, // lint 結果有錯誤將會拋出異常
throwOnWarning: true,
include: ['src/**/*.ts'],
exclude: ['node_modules/**', 'lib/**', '*.js'],
}),
// 使得 rollup 支持 commonjs 規范,識別 commonjs 規范的依賴
commonjs(),
// 配合 commnjs 解析第三方模塊
resolve({
// 將自定義選項傳遞給解析插件
customResolveOptions: {
moduleDirectory: 'node_modules',
},
}),
rollupTypescript(),
babel({
runtimeHelpers: true,
// 只轉換源代碼,不運行外部依賴
exclude: 'node_modules/**',
// babel 默認不支持 ts 需要手動添加
extensions: [
...DEFAULT_EXTENSIONS,
'.ts',
],
}),
],
}
export default rollupConfig
一些注意事項:
- plugins 必須有順序的使用
- external 來設置三方庫為外部模塊,否則也會被打包進去,變得非常大哦
配置聲明文件
declare module 'rollup-plugin-babel'
declare module 'rollup-plugin-eslint'
由於部分插件還沒有 @types 庫,所以我們手動添加聲明文件
試一下
我們在 index.ts 文件下,隨意加入一個方法
export default function myFirstFunc (str: string) {
return `hello ${str}`
}
由於使用了 RollupOptions
接口,直接執行會報錯。我們要注釋掉第2行import { RollupOptions } from 'rollup'
,和第17行 const rollupConfig
后面的 : RollupOptions
。
然后執行 npx rollup --c rollup.config.ts
就生成了 index.js 和 index.esm.js 文件。分別對應着 commonjs 規范和 es 規范的文件。rollup 可是大力推行 es 規范啊,然后我們很多三方庫都仍舊使用 commonjs 規范,為了兼容,我們兩種規范都生成。
由於使用了 ts ,可以很方便的實現快速補全的需求,按照上面的例子,項目中使用這個包后,vscode 上輸入就會有如下效果
參考 commit
配置 jest
工具庫當然要寫測試啦,快開始吧
安裝
yarn add -D @types/jest eslint-plugin-jest jest ts-jest
目錄結構
fly-helper
|- test |- index.test.ts |- jest.config.js
配置 jest.config.js
// jest.config.js module.exports = { preset: 'ts-jest', testEnvironment: 'node', }
動手寫個 test 吧
// index.test.ts
import assert from 'assert'
import myFirstFunc from '../src'
describe('validate:', () => {
/**
* myFirstFunc
*/
describe('myFirstFunc', () => {
test(' return hello rollup ', () => {
assert.strictEqual(myFirstFunc('rollup'), 'hello rollup')
})
})
})
再配置 eslint
const eslintrc = { // ... extends: [ // ... 'plugin:jest/recommended', ], plugins: [ // ... 'jest', ], // ... }
增加 package.json scripts
"test": "jest --coverage --verbose -u"
- coverage 輸出測試覆蓋率
- verbose 層次顯示測試套件中每個測試的結果,會看着更加直觀啦
試一下
yarn test
是不是成功了呢😌
參考 commit
配置 @microsoft/api-extractor
當我們 src 下有多個文件時,打包后會生成多個聲明文件。
使用 @microsoft/api-extractor 這個庫是為了把所有的 .d.ts 合成一個,並且,還是可以根據寫的注釋自動生成文檔。
安裝
yarn add -D @microsoft/api-extractor
配置 api-extractor.json
/* api-extractor.json */
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", "mainEntryPointFilePath": "./lib/index.d.ts", "bundledPackages": [ ], "dtsRollup": { "enabled": true, "untrimmedFilePath": "./lib/index.d.ts" } }
增加 package.json scripts
"api": "api-extractor run",
嘗試一下
你可以嘗試多寫幾個方法,打包后會發現有多個 .d.ts 文件,然后執行 yarn api
加入ts doc 風格注釋
/**
* 返回 hello 開頭的字符串
* @param str - input string
* @returns 'hello xxx'
* @example
* ```ts
* myFirstFunc('ts') => 'hello ts'
* ```
*
* @beta
* @author ziming
*/
在使用的該方法的時候就會有提示啦
這里我已經增加了兩個方法,請看 下面的 commit
執行后,會發現 聲明都合在 index.d.ts 上啦。然后要把多余的給刪除掉,后面改成自動刪除它😕
😤還有一個 temp 文件夾,咱們配置一下 gitignore 不然它提交。tsdoc-metadata.json 可以暫時不管它,可以刪除掉。
后面配置 package.json 的 typing 會自動更改存放位置
參考 commit
之后使用方法就有這樣的提示,是不是會用的很方便嘞😉
gulp 自動化構建
安裝
yarn add -D gulp @types/gulp fs-extra @types/fs-extra @types/node ts-node chalk
配置 package.json
"main": "lib/index.js", "module": "lib/index.esm.js", "typings": "lib/index.d.js", "scripts": { /* ... */ "build": "gulp build", }
配置 gulpfile
我們思考一下構建流程🤔
- 刪除 lib 文件
- 呼叫 Rollup 打包
- api-extractor 生成統一的聲明文件,然后 刪除多余的聲明文件
- 完成
我們一步一步來
// 刪除 lib 文件
const clearLibFile: TaskFunc = async (cb) => {
fse.removeSync(paths.lib)
log.progress('Deleted lib file')
cb()
}
// rollup 打包
const buildByRollup: TaskFunc = async (cb) => {
const inputOptions = {
input: rollupConfig.input,
external: rollupConfig.external,
plugins: rollupConfig.plugins,
}
const outOptions = rollupConfig.output
const bundle = await rollup(inputOptions)
// 寫入需要遍歷輸出配置
if (Array.isArray(outOptions)) {
outOptions.forEach(async (outOption) => {
await bundle.write(outOption)
})
cb()
log.progress('Rollup built successfully')
}
}
// api-extractor 整理 .d.ts 文件
const apiExtractorGenerate: TaskFunc = async (cb) => {
const apiExtractorJsonPath: string = path.join(__dirname, './api-extractor.json')
// 加載並解析 api-extractor.json 文件
const extractorConfig: ExtractorConfig = await ExtractorConfig.loadFileAndPrepare(apiExtractorJsonPath)
// 判斷是否存在 index.d.ts 文件,這里必須異步先訪問一邊,不然后面找不到會報錯
const isExist: boolean = await fse.pathExists(extractorConfig.mainEntryPointFilePath)
if (!isExist) {
log.error('API Extractor not find index.d.ts')
return
}
// 調用 API
const extractorResult: ExtractorResult = await Extractor.invoke(extractorConfig, {
localBuild: true,
// 在輸出中顯示信息
showVerboseMessages: true,
})
if (extractorResult.succeeded) {
// 刪除多余的 .d.ts 文件
const libFiles: string[] = await fse.readdir(paths.lib)
libFiles.forEach(async file => {
if (file.endsWith('.d.ts') && !file.includes('index')) {
await fse.remove(path.join(paths.lib, file))
}
})
log.progress('API Extractor completed successfully')
cb()
} else {
log.error(`API Extractor completed with ${extractorResult.errorCount} errors`
+ ` and ${extractorResult.warningCount} warnings`)
}
}
// 完成
const complete: TaskFunc = (cb) => {
log.progress('---- end ----')
cb()
}
然后用一個 build 方法,將他們按順序合起來
export const build = series(clearLibFile, buildByRollup, apiExtractorGenerate, complete)
嘗試一下
yarn build
溜去 lib 文件下瞅瞅🧐,美滋滋。
參考 commit
changelog 自動生成
安裝
yarn add -D conventional-changelog-cli
配置 gulpfile
// gulpfile
import conventionalChangelog from 'conventional-changelog'
// 自定義生成 changelog
export const changelog: TaskFunc = async (cb) => {
const changelogPath: string = path.join(paths.root, 'CHANGELOG.md')
// 對命令 conventional-changelog -p angular -i CHANGELOG.md -w -r 0
const changelogPipe = await conventionalChangelog({
preset: 'angular',
releaseCount: 0,
})
changelogPipe.setEncoding('utf8')
const resultArray = ['# 工具庫更新日志\n\n']
changelogPipe.on('data', (chunk) => {
// 原來的 commits 路徑是進入提交列表
chunk = chunk.replace(/\/commits\//g, '/commit/')
resultArray.push(chunk)
})
changelogPipe.on('end', async () => {
await fse.createWriteStream(changelogPath).write(resultArray.join(''))
cb()
})
}
驚喜的發現 conventional-changelog 木得 @types 庫,繼續手動添加
// typings/index.d.ts
declare module 'conventional-changelog'
參考 commit
Ps
使用 conventional-changelog 需要注意一下
- 非常注意 commit 格式,格式采用 angular commit 規范,會識別 feat 和 fix 開頭的 commit ,然后自動生成
- 每次更改需要先升級 version 再去生成。后面會有例子
優化開發流程
安裝
yarn add -D husky lint-staged
package.json
話不多說,看代碼
"husky": { "hooks": { "pre-commit": "lint-staged & jest -u" } }, "lint-staged": { "*.{.ts,.js}": [ "eslint", "git add" ] }
之后提交代碼都會先 lint 驗證,再 jest 測試通過,才可以提交。規范團隊協作的代碼規范
優化發布流程
package.json
/* pushlish 的文件 */
"files": [ "lib", "LICENSE", "CHANGELOG.md", "README.md" ], /* 使得支持 tree shaking */ "sideEffects": "false", "script": { /* ... */ "changelog": "gulp changelog", "prepublishOnly": "yarn lint & yarn test & yarn changelog & yarn build" }
prepublishOnly 可以在 publish 的時候,先 lint 驗證, 再 jest 測試 , 再生成 changlog ,最后打包,最后發布。
至此,我們已經實現了全部需求。🥳
參考 commit
changelog 例子
-
我們假裝現在開始寫第一個方法。我刪除了上面的例子,增加了一個 calculate.ts
-
然后我們提交這次更改,commit 內容為
feat: 新增 calculateOneAddOne 計算 1 + 1 方法
-
執行 npm version major 升級主版本號 1.0.0。
版本規范參考 語義化版本 2.0.0
-
yarn changelog
看看你的 changelog.md 就自動生成了🥳
倉庫地址
參考
The future of TypeScript on ESLint
If you're writing a package, strongly consider using
pkg.module
Commit message 和 Change log 編寫指南