TypeScript、Rollup 搭建工具庫


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  
1

創建 src 目錄,入口文件,以及 ts 的配置文件

fly-helper  
 | |- src |- index.ts |- tsconfig.json 
12345

配置 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" ] } 
12345678910111213141516171819202122232425262728

參考 commit

1892d4

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  
1

目錄結構

fly-helper  
 |- .eslintignore |- .eslintrc.js |- tsconfig.eslint.json 
1234

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 
123

雖然可以配置 eslintrc.parserOptions.createDefaultProgram 但會造成巨大的性能損耗。

issus: Parsing error: "parserOptions.project"...

配置 tsconfig.eslint.json

/* tsconfig.eslint.json */
{
  "compilerOptions": { "baseUrl": ".", "resolveJsonModule": true, }, "include": [ "**/*.ts", "**/*.js" ] } 
1234567891011

配置 .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 
123456789101112131415161718192021222324252627

參考 commit

36f63d

配置 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  
1

安裝 babel 相關的庫

yarn add -D @babel/preset-env  
1

目錄結構

fly-helper  
 | |- typings |- index.d.ts |- .babelrc |- rollup.config.ts 
123456

配置 .babelrc

/* .babelrc */
{
  "presets": [ [ "@babel/preset-env", { /* Babel 會在 Rollup 有機會做處理之前,將我們的模塊轉成 CommonJS,導致 Rollup 的一些處理失敗 */ "modules": false } ] ] } 
123456789101112

配置 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  
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869

一些注意事項:

  • plugins 必須有順序的使用
  • external 來設置三方庫為外部模塊,否則也會被打包進去,變得非常大哦

配置聲明文件

declare module 'rollup-plugin-babel'  
declare module 'rollup-plugin-eslint'  
12

由於部分插件還沒有 @types 庫,所以我們手動添加聲明文件

試一下

我們在 index.ts 文件下,隨意加入一個方法

export default function myFirstFunc (str: string) {  
  return `hello ${str}`
}
123

由於使用了 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

0aab81

配置 jest

工具庫當然要寫測試啦,快開始吧

安裝

yarn add -D @types/jest eslint-plugin-jest jest ts-jest  
1

目錄結構

fly-helper  
 |- test |- index.test.ts |- jest.config.js 
1234

配置 jest.config.js

// jest.config.js module.exports = { preset: 'ts-jest', testEnvironment: 'node', } 
12345

動手寫個 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')
    })
  })
})
123456789101112131415

再配置 eslint

const eslintrc = { // ... extends: [ // ... 'plugin:jest/recommended', ], plugins: [ // ... 'jest', ], // ... } 
123456789101112

增加 package.json scripts

"test": "jest --coverage --verbose -u" 
1
  • coverage 輸出測試覆蓋率
  • verbose 層次顯示測試套件中每個測試的結果,會看着更加直觀啦

試一下

yarn test 
1

是不是成功了呢😌

參考 commit

9bbe5b

配置 @microsoft/api-extractor

當我們 src 下有多個文件時,打包后會生成多個聲明文件。

使用 @microsoft/api-extractor 這個庫是為了把所有的 .d.ts 合成一個,並且,還是可以根據寫的注釋自動生成文檔。

安裝

yarn add -D @microsoft/api-extractor  
1

配置 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" } } 
12345678910

增加 package.json scripts

"api": "api-extractor run", 
1

嘗試一下

你可以嘗試多寫幾個方法,打包后會發現有多個 .d.ts 文件,然后執行 yarn api

加入ts doc 風格注釋

/**
 * 返回 hello 開頭的字符串
 * @param str - input string
 * @returns 'hello xxx'
 * @example
 * ```ts
 * myFirstFunc('ts') => 'hello ts'
 * ```
 *
 * @beta
 * @author ziming
 */
123456789101112

在使用的該方法的時候就會有提示啦

這里我已經增加了兩個方法,請看 下面的 commit

執行后,會發現 聲明都合在 index.d.ts 上啦。然后要把多余的給刪除掉,后面改成自動刪除它😕

😤還有一個 temp 文件夾,咱們配置一下 gitignore 不然它提交。tsdoc-metadata.json 可以暫時不管它,可以刪除掉。

后面配置 package.json 的 typing 會自動更改存放位置

參考 commit

4e4b3d

之后使用方法就有這樣的提示,是不是會用的很方便嘞😉

gulp 自動化構建

安裝

yarn add -D gulp @types/gulp fs-extra @types/fs-extra @types/node ts-node chalk  
1

配置 package.json

  "main": "lib/index.js", "module": "lib/index.esm.js", "typings": "lib/index.d.js", "scripts": { /* ... */ "build": "gulp build", } 
12345678

配置 gulpfile

我們思考一下構建流程🤔

  1. 刪除 lib 文件
  2. 呼叫 Rollup 打包
  3. api-extractor 生成統一的聲明文件,然后 刪除多余的聲明文件
  4. 完成

我們一步一步來

// 刪除 lib 文件
const clearLibFile: TaskFunc = async (cb) => {  
  fse.removeSync(paths.lib)
  log.progress('Deleted lib file')
  cb()
}
123456
// 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')
  }
}
12345678910111213141516171819
// 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`)
  }
}
1234567891011121314151617181920212223242526272829303132333435
// 完成
const complete: TaskFunc = (cb) => {  
  log.progress('---- end ----')
  cb()
}
12345

然后用一個 build 方法,將他們按順序合起來

export const build = series(clearLibFile, buildByRollup, apiExtractorGenerate, complete)  
1

嘗試一下

yarn build  
1

溜去 lib 文件下瞅瞅🧐,美滋滋。

參考 commit

a5370c

changelog 自動生成

安裝

yarn add -D conventional-changelog-cli  
1

配置 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()
  })
}
123456789101112131415161718192021222324

驚喜的發現 conventional-changelog 木得 @types 庫,繼續手動添加

// typings/index.d.ts

declare module 'conventional-changelog'  
123

參考 commit

1f31ab

Ps

使用 conventional-changelog 需要注意一下

  • 非常注意 commit 格式,格式采用 angular commit 規范,會識別 feat 和 fix 開頭的 commit ,然后自動生成
  • 每次更改需要先升級 version 再去生成。后面會有例子

優化開發流程

安裝

yarn add -D husky lint-staged  
1

package.json

話不多說,看代碼

  "husky": { "hooks": { "pre-commit": "lint-staged & jest -u" } }, "lint-staged": { "*.{.ts,.js}": [ "eslint", "git add" ] } 
1234567891011

之后提交代碼都會先 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" } 
1234567891011121314

prepublishOnly 可以在 publish 的時候,先 lint 驗證, 再 jest 測試 , 再生成 changlog ,最后打包,最后發布。

至此,我們已經實現了全部需求。🥳

參考 commit

7f343f

changelog 例子

  • 我們假裝現在開始寫第一個方法。我刪除了上面的例子,增加了一個 calculate.ts

    請看倉庫地址 release/1.0.0 分支

  • 然后我們提交這次更改,commit 內容為 feat: 新增 calculateOneAddOne 計算 1 + 1 方法

  • 執行 npm version major 升級主版本號 1.0.0。

    更多升級版本的操作

    版本規范參考 語義化版本 2.0.0

  • yarn changelog 看看你的 changelog.md 就自動生成了🥳

倉庫地址

fly-helper/release/1.0.0

參考

TypeScript 入門教程

TypeSearch

The future of TypeScript on ESLint

Rollup.js 中文網

rollup - pkg.module

If you're writing a package, strongly consider using pkg.module

jest 中文文檔

api-extractor

tsdoc

gulp

Commit message 和 Change log 編寫指南

 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM