使用 Rollup + TypeScript 编写库

2 年前(已编辑)
编程 / ,
2594
2
这篇文章上次修改于 2 年前,可能部分内容已经不适用,如有疑问可询问作者。

本文的主题是一步一步建立 Rollup + TypeScript 代码模板。

前言

首先看看,我们需要做什么。通常一个库,在发布前他的目录树是这样的。

.
├── dist
├── esm
├── lib
├── node_modules
├── package.json
├── pnpm-lock.yaml
├── rollup.config.js
├── src
├── tsconfig.json
├── vite.config.js

其中,dist 目录一般是通过 Rollup 等打包器打包后的入口文件,一般具有多种格式,以不同后缀命令,如: index.cjs.js index.esm.js。lib 和 esm 目录可以是 TypeScript 编译后生成的文件,目录下的结构基本和原项目结构相同,只是后缀变为 js,lib 一般为 CommonJS 格式,esm 为 ESModule 格式。而这些是一个库最基本的需要发布的文件。

初始化项目

通过如下命令快速开始:

npm init -y
npm i -D typescript rollup @rollup/plugin-typescript @rollup/plugin-commonjs @rollup/plugin-node-resolve rollup-plugin-terse rollup-plugin-peer-deps-external
npx run tsc --init
mkdir src
touch src/index.ts
echo 'export {}' >> src/index.ts

注:基本配置不再过多赘述,@rollup/plugin-commonjs 为 ES6 转换插件,@rollup/plugin-node-resolve 为 Node 模块解析插件,rollup-plugin-terse 为代码压缩插件,rollup-plugin-peer-deps-external 为打包时使用外部库插件(就是说,打包的时候不把依赖库打包进去,node_modules 依赖链你也知道)。

建立 rollup.config.js,编写基本配置以支持 TypeScript。

//@ts-check
import commonjs from '@rollup/plugin-commonjs'
import { nodeResolve } from '@rollup/plugin-node-resolve'
import typescript from '@rollup/plugin-typescript'
import peerDepsExternal from 'rollup-plugin-peer-deps-external'
import { terser } from 'rollup-plugin-terser'

const packageJson = require('./package.json')

const umdName = packageJson.name

const globals = {
  ...packageJson.devDependencies,
}

const dir = 'dist'

/**
 * @type {import('rollup').RollupOptions[]}
 */
const config = [
  {
    input: 'src/index.ts',
    // ignore lib
    external: ['lodash', 'lodash-es', ...Object.keys(globals)],

    output: [
      {
        file: dir + '/index.umd.js',
        format: 'umd',
        sourcemap: true,
        name: umdName,
      },
      {
        file: dir + '/index.umd.min.js',
        format: 'umd',
        sourcemap: true,
        name: umdName,
        plugins: [terser()],
      },
      {
        file: dir + '/index.cjs.js',
        format: 'cjs',
        sourcemap: true,
      },
      {
        file: dir + '/index.cjs.min.js',
        format: 'cjs',
        sourcemap: true,
        plugins: [terser()],
      },
      {
        file: dir + '/index.esm.js',
        format: 'es',
        sourcemap: true,
      },
      {
        file: dir + '/index.esm.min.js',
        format: 'es',
        sourcemap: true,
        plugins: [terser()],
      },
    ],
    plugins: [
      nodeResolve(),
      commonjs({ include: 'node_modules/**' }),
      typescript({ tsconfig: './src/tsconfig.json', declaration: false }),

      // @ts-ignore
      peerDepsExternal(),
    ],

    treeshake: true,
  },
]

export default config

配置之后,使用 rollup -c,就可以编译打包 ts 文件到 dist 目录了。

但是这才刚刚开始。

Path Alias

一般的也会用 Path Alias 方便方法的引入。

在 tsconfig.json 配置 paths,如

{
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "~/*": [
        "*"
      ]
    },
  }
}

就可以用 import foo from '~/' 的形式了。为什么要讲这个。因为这个是巨坑。请看下节。

TSC 编译与 Path Alias

上面说了 Rollup 的打包,再来说说 TSC,其实也比较简单。一般的,我会在根目录下新建一个 tsconfig.json 作为基本 tsconfig,然后在建立 build 用 tsconfig,开发用 tsconfig,都是从根目录的 extends 出来。这样做的好处就是不同环境用不同配置比较灵活。

大概这就像这样:

.
├── package.json
├── pnpm-lock.yaml
├── readme.md
├── renovate.json
├── rollup.config.js
├── src
│   ├── tsconfig.build.json # build
│   ├── tsconfig.cjs.json # cjs build
│   ├── tsconfig.json # dev
├── tsconfig.json  # base
├── vite.config.js
└── yarn-error.log

前面说了 tsc 编译也要两种格式一个是 ESM,另一个是 CJS。就需要编写两个配置了,唯一的不同其实就是 outDirmodule。然后编译跑这行就行了。

{
  "scripts": {
    "build": "tsc --build src/tsconfig.build.json && tsc --build src/tsconfig.cjs.json"
  }
}

好像没有什么不对,但是仔细一看人傻了,tsc 编译之后的产物没有把 Path Alias 转换过来。但是这个库被调包侠调过来之后,它的环境咋知道 ~ alias 是个啥,况且 js 也不读 tsconfig 的配置。

这可咋整。

一查发现别的 CLI 都用了一个工具叫 tsconfigs-paths,但是这玩意好像只是个库,用起来比较麻烦。在这之后有大佬写一个 TypeScript 的插件叫 @zerollup/ts-transform-paths。用于解决这个问题。由于目前 TypeScript 还不支持自定义 transformer 所以得用 ttypescript 替换 TypeScript。

npm i -g @zerollup/ts-transform-paths ttypescript
// tsconfig.json
{
  "compilerOptions": {
    "outDir": "./dist",
    "baseUrl": "./src",
    "paths": {
      "~/*": [
        "*"
      ]
    },
    "plugins": [
      {
        "transform": "@zerollup/ts-transform-paths",
      }
    ]
  }
}

tsc 全换成 ttsc,

ttsc --build src/tsconfig.build.json && ttsc --build src/tsconfig.cjs.json"

之后。

打包 DTS

DTS 就是 tsc 生成 d.ts,我们要做的就是把 dts 也打包一份,全部扔到一个 index.d.ts。这样的话如果引用的是任何一个 dist 下的 index.js (比如dist/index.esm.js)都会识别到 type definition。

但是,@rollup/plugin-typescriptrollup-plugins-typescript2 都没有这一功能。

之后就发现了一个神器 dts-bundle-generator。可以做到这个需求,同时它也支持 Path Alias 的转换。

使用也非常的简单。

dts-bundle-generator -o build/index.d.ts src/index.ts --project tsconfig.json  --no-check

一些不能工作的点

  1. ttsc 在 typescript 4.5.2 的环境可能会报错。TypeError: Cannot read properties of undefined (reading 'impliedNodeFormat')。 解决方式:降级到 4.4.4
  2. dts-bundle-generator 不能支持没有提前引入的泛型的值的解析(也可能是 目前 TS 的 bug)参考:https://github.com/timocov/dts-bundle-generator/issues/178

Package.json

注明需要发布的文件,以及入口文件、类型文件。

{
  "main": "build/index.cjs.js",
  "module": "build/index.esm.js",
  "types": "build/index.d.ts",
  "unpkg": "build/index.umd.min.js"
}

完整模板:rollup-typescript-lib

评论区加载中...