likes
comments
collection
share

从零开发自己的工具库(一)配置 TS + Rollup + Jest

作者站长头像
站长
· 阅读数 2

日常开发中,我们经常会用到一些通用的方法,导致每次写新项目,都得复制粘贴。我们可以将这些通用处理逻辑封装成工具库,发布到 NPM 上。这样,每次只要 install 以下即可,其他开发者能共享这些库,也算是为开源做出一点贡献~

接下来,我们就尝试自己从零开始搭建一个工具库,成品可以参考 utils ,欢迎 star 🤞❤️

创建项目

首先在自己的 Github 上创建项目后,拉取到本地,然后 npm init -y

现在,项目中就有了最初的 package.json 文件。当然,这份文件还不够完善,在后续的各种配置中,我们会逐步修改这份文件。

喜欢用 pnpmyarnbun 或是其他工具的也一样,此处无需赘述。

配置 TypeScript

安装 TypeScript

npm i typescript -D 安装 TypeScript 依赖。

npx --package typescript tsc --init 生成一个默认的配置文件。

在 TypeScript 项目中,最重要的就是这份 tsconfig.json 配置文件,它给项目提供了使用 TS 时的语法规范、编译标准以及环境配置等等选项,属性非常多,不过生成的默认文件已经给我们配置好了部分选项,并给所有的选项添加了注释,你也可以通过访问 aka.ms/tsconfig 来查阅相关配置。

拆分 tsconfig.json

我们将其中和语法相关的编译器选项单独提取出来,放在 tsconfig.base.json 中:

{
    "compilerOptions": {
        "incremental": true,
        "target": "es5",
        "lib": ["ESNext", "DOM", "ES2018"],
        "jsx": "preserve",
        "experimentalDecorators": true,
        "jsxFragmentFactory": "Fragment",
        "module": "esnext",
        "moduleResolution": "node",
        "baseUrl": "./",
        "paths": {
            "@/*": ["packages/*"]
        },
        "resolveJsonModule": true,
        "allowJs": true,
        "checkJs": true,
        "declaration": true,
        "sourceMap": true,
        "importHelpers": true,
        "isolatedModules": true,
        "allowSyntheticDefaultImports": true,
        "esModuleInterop": true,
        "forceConsistentCasingInFileNames": true,
        "strict": true,
        "strictFunctionTypes": false,
        "skipLibCheck": true
    }
}

再将工程环境配置提取到 tsconfig.json 文件中,并使用 extends 继承 tsconfig.base.json 的所有配置:

{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "outDir": "lib",
    "rootDir": "./packages",
  },
  "include": [
    "packages/**/*.ts",
    "packages/**/*.tsx"
  ],
  "exclude": [
    "packages/**/__test__/*.test.ts",
  ]
}

declaration: true 选项会自动从项目中的 ts 和 js 文件生成 .d.ts 声明文件。

rootDir: './packages' 指定了根目录,该目录下的文件结构会在打包的目录中得到保留,后续所有开发的 utils 都要放在这个文件夹中。

outDir: 'lib' 指定了输出目录,编译后的 js.d.ts 声明文件等都会打包到 lib 文件夹内。

include 指定需要编译处理的文件列表,解析路径相对于当前项目的 tsconfig.json 文件位置。

exclude 指定在解析 include 时应跳过的文件,所以该配置项只会对 include 包含的文件有影响。

FAQ

1. This syntax requires an imported helper but module 'tslib' cannot be found

这是因为 tsconfig 配置了 importHelpers: true,开启该选项,一些低版本降级操作会从 tslib 中导入。如果你的 target 编译目标使用的是 ES5 这种较低版本,但语法中出现了 ES6 或更新的语法,那么该报错就会出现。

所以,你可以通过执行 npm install -D tslib@latest 来解决这个问题。tslib 是把一系列的降级代码(函数)抽离并合并导出的库。目的是降低编译后代码的数量,起到压缩代码体积的作用。

配置 Rollup

安装 Rollup 及其相关插件

npm i rollup @rollup/plugin-node-resolve @rollup/plugin-typescript @rollup/plugin-commonjs @rollup/plugin-terser rollup-plugin-postcss -D

插件作用分别如下:

包名作用
@rollup/plugin-node-resolve处理路径
@rollup/plugin-typescript支持 TS
@rollup/plugin-commonjs处理 CommonJS
@rollup/plugin-terser压缩 UMD 规范的输出文件
rollup-plugin-postcss处理 CSS

配置 rollup.config.js

const resolve = require('@rollup/plugin-node-resolve');
const typescript = require('@rollup/plugin-typescript');
const commonjs = require('@rollup/plugin-commonjs');
const terser = require('@rollup/plugin-terser');
const postcss = require('rollup-plugin-postcss');

module.exports = [
    {
        input: './packages/index.ts',
        output: [
            {
                dir: 'lib',
                format: 'cjs',
                entryFileNames: '[name].cjs.js',
                sourcemap: false, // 是否输出sourcemap
            },
            {
                dir: 'lib',
                format: 'esm',
                entryFileNames: '[name].esm.js',
                sourcemap: false, // 是否输出sourcemap
            },
            {
                dir: 'lib',
                format: 'umd',
                entryFileNames: '[name].umd.js',
                name: '$utils', // umd 模块名称,相当于一个命名空间,会自动挂载到window下面
                sourcemap: false,
                plugins: [terser()],
            },
        ],
        plugins: [
            postcss({
                minimize: true,
                extensions: ['.css'],
                extract: true,
            }),
            resolve(),
            commonjs(),
            typescript({
                tsconfig: './tsconfig.json',
                compilerOptions: {
                  incremental: false,
                },
            }),
        ],
    },
];

其中,@rollup/plugin-typescript 插件会先从 tsconfig.json 中加载所有配置项作为其初始值。传递新的配置可以覆盖这些选项。你也可以设置 tsconfig 的值为文件路径来指定配置文件。

修改 package.json

我们修改其中部分配置:

{
    "main": "lib/index.cjs.js",
    "module": "lib/index.esm.js",
    "jsnext:main": "lib/index.esm.js",
    "browser": "lib/index.umd.js",
    "scripts": {
        "build": "rollup -c",
    },
    "files": ["lib"],
    "types": "lib/index.d.ts",
}

配置说明如下:

配置项说明
mainBrowser 和 Node 环境中指定的项目入口文件
module指定 ESModule 模块的入口文件
jsnext:main同上,不过这个是社区规范,上面是官方规范
browserUMD 规范,当直接在浏览器中开发时,可下载 release 包并在浏览器中使用 script 导入
typesTS 类型声明文件路径
files约定 NPM 发包时包含的文件和文件夹

执行 npm run build 就会生成一份 lib 文件夹,里面会有 cjsesmumd 三种规范的 js 文件,以供不同方式引入。除此之外,还有自动生成的 .d.ts 类型声明文件,是不是很方便~

配置 Jest

安装测试框架 Jest 及其相关插件

npm i jest jest-environment-jsdom ts-jest @types/jest -D

配置 jest.config.js

执行 npx jest --init 命令,并进行配置选择:

jest --init

✔ Would you like to use Jest when running "test" script in "package.json"? … yes
✔ Would you like to use Typescript for the configuration file? … no
✔ Choose the test environment that will be used for testing › jsdom (browser-like)
✔ Do you want Jest to add coverage reports? … yes
✔ Which provider should be used to instrument code for coverage? › babel
✔ Automatically clear mock calls and instances between every test? … no

此时会生成一份 jest.config.js 文件。

配置 Babel

执行以下命令安装 Babel 相关插件。

npm i babel-jest @babel/core @babel/preset-env -D

在项目的根目录下创建 babel.config.js 注意不是 .babelrc ,通过配置 Babel 使其能够兼容当前的 Node 版本。

// only used by jest 不应该影响业务代码构建!
module.exports = {
    presets: [
        '@babel/preset-env'
    ],
};

接着修改 jest.config.js 部分配置:

const config = {
    coverageProvider: 'babel',
    testEnvironment: 'jsdom',
    testEnvironmentOptions: {
        userAgent:
            'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36',
    },
    transform: {
        '^.+\\.(js|jsx)$': 'babel-jest',
    },
}

module.exports = config;

最后在 package.json 中添加两条命令

{
    "scripts": {
        "test": "jest",
        "coveralls": "jest --coverage",
    }
}

test 会执行编写的单元测试,而 --coverage 则会在项目根目录下生成一份 coverage 文件夹,里面包含完整的测试报告

从零开发自己的工具库(一)配置 TS + Rollup + Jest

我们可以通过打开 index.html 在浏览器中查看报告:

从零开发自己的工具库(一)配置 TS + Rollup + Jest

coverage 文件夹无需上传,记得在 .gitignore 中忽略此文件。

编写单元测试

我们先在 packages/is 目录下写一个判断是否为 null 的方法:

// packages/is/index.ts
export const isNull = (val: any) => val === null;

在入口中导入导出:

// packages/index.ts
export { isNull } from './is';

packages/is 下新建一个 __test__ 测试目录,创建 index.test.ts 文件(注意有 .test 后缀)编写以下单元测试:

// packages/__test__/index.ts
import { isNull } from '../index';

describe('isNull', () => {
  it('should return true if the value is null', () => {
    const result = isNull(null);
    expect(result).toBe(true);
  });

  it('should return false if the value is not null', () => {
    const result = isNull('some value');
    expect(result).toBe(false);
  });

});

执行 npm run test,测试通过会有类似如下的提示:

从零开发自己的工具库(一)配置 TS + Rollup + Jest

FAQ

1. Error: Test environment jest-environment-jsdom cannot be found

从 jest 28.0.0 往后,jest-environment-jsdom 不再随 jest 内置,所以需要单独安装以支持 DOM 或 BOM 操作。

安装后,你可以在使用到 DOM 或 BOM 对象的测试文件的顶部加上一行注释即可运行:

/**
 * @jest-environment jsdom
 */

或者在配置文件中修改运行环境为 testEnvironment: 'jsdom'

module.exports = {
    // 支持测试环境访问dom
    testEnvironment: 'jsdom',

    // 配置测试环境 ua
    testEnvironmentOptions: {
        userAgent:
        'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36',
    },
}

2. Cannot use import statement outside a module

Jest 本身支持 ESM,即单测可以如此写:

export const isNull = /** ...... */

但是当导入的文件本身也包含 ESM 模式的导入导出,就不会被 Jest 识别,此时就需要 Babel 进行编译,参见上述的有关 Babel 的配置。

3. SyntaxError: Unexpected token 'export'

如果你的文件中引入了三方依赖(比如 lodash-es),可能会出现以下错误:

从零开发自己的工具库(一)配置 TS + Rollup + Jest

Jest 默认情况下并不转换 node_modules ,但是 lodash-es 专门提供了 ESM 规范,所以在执行测试时,这些模块就需要 Jest 通过 Babel 进行转换。

我们可以通过 transformIgnorePatterns 配置项来配置 Jest 转换白名单,被匹配到的文件将会跳过转换:

module.exports = {
    transformIgnorePatterns: ['<rootDir>/node_modules/(?!lodash-es)'],
}

4. Cannot find module '@/xxxx/xxxx'

项目中经常配置文件别名 alis 来简化路径,优化开发体验,但是 Jest 却无法识别这些代号。此时就需要配置 moduleNameMapper,通过正则表达式建立别名到模块的映射。

module.exports = {
    moduleNameMapper: {
        '^@/(.*)': '<rootDir>/packages/$1',
    },
}

5. No tests found, exiting with code 1 Run with --passWithNoTests to exit with code 0

Jest 默认只会匹配 __tests__ 文件夹下或中间带有 .test.spec 的 js 或 ts 文件。所以,请查看你的文件名是否有误。如果想自定义 Jest 匹配文件,可以配置如下属性:

module.exports = {
    // The glob patterns Jest uses to detect test files
    testMatch: [
        // 默认的:
        // '**/__tests__/**/*.[jt]s?(x)',
        // "**/?(*.)+(spec|test).[tj]s?(x)"
        
        // 指定自己的匹配规则:
        '**/__demo__/**/*.[jt]s?(x)',
    ],
}

总结

  1. TypeScript 的配置;
  2. Rollup 的配置;
  3. Jest 的配置;
  4. 单元测试的编写;
  5. 搭建问题的总结。

下篇我们将围绕开发中的代码质量,使用 ESLint 、Prettier、Husky、Commitlint 等工具对代码编写、提交进行规范约束。

系列文章

参考资料

使用Typescript和Rollup从零开发一个工具库

typescript-syntax-requires-imported-helper-but-module-tslib-cannot

stackoverflow | This syntax requires an imported helper but module 'tslib' cannot be found

test-environment-jest-environment-jsdom-cannot-be-found

Jest setup "SyntaxError: Unexpected token export"