阅读 3612

TypeScript、Rollup 搭建工具库

前景提要

公司内总是有许多通用的工具方法、业务功能,我们可以搭建一个工具库来给各个项目使用。

要实现的需求:🤔

  • 支持编辑器的快速补全和提示
  • 自动化构建
  • 支持自动生成 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

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 
复制代码

目录结构

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

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
复制代码

安装 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

0aab81

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

9bbe5b

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

4e4b3d

之后使用方法就有这样的提示,是不是会用的很方便嘞😉

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

我们思考一下构建流程🤔

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

我们一步一步来

// 删除 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

a5370c

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

1f31ab

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

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 编写指南