使用 Lerna、Rollup、TypeScript 打造自己的 JavaScript 工具包

目标

  1. 使用 RollupTypeScript 构建三个工具包,分别为:仅支持浏览器环境(使用了 BOM、DOM)、仅支持 nodejs 环境(使用了 nodejs API)、纯 JavaScript 环境;
  2. 自动生成 *.d.ts 文件,使用 @microsoft/api-documenter、 @microsoft/api-extractor 生成 API Doc;
  3. 加入 Jest 测试工具,测试覆盖率达标后方可发包;
  4. 使用 Lerna 进行多 package 管理与 npm 发布。

初始化

创建 Lerna repo

1
2
3
npm install --global lerna
git init lerna-repo && cd lerna-repo
lerna init -i # -i – 使用独立版本控制模式。

目录如下:

1
2
3
4
lerna-repo/
packages/
package.json
lerna.json

lerna.json

1
2
3
4
5
6
{
"packages": [
"packages/*"
],
"version": "independent"
}

package.json

1
2
3
4
5
6
7
{
"name": "root",
"private": true,
"devDependencies": {
"lerna": "^4.0.0"
}
}

启用 Yarn Workspaces

package.json

1
2
3
4
5
6
7
{
...
"workspaces": [
"packages/*"
]
...
}

lerna.json

1
2
3
4
5
6
{
...
"npmClient": "yarn",
"useWorkspaces": true,
...
}

创建 package

1
2
3
lerna create tool
lerna create tool-browser
lerna create tool-node

安装依赖

  1. 安装 typescript 及相关工具

    1
    2
    3
    4
    lerna add typescript --dev
    lerna add tslib --dev
    lerna add @microsoft/api-extractor --dev
    lerna add @microsoft/api-documenter --dev
  2. 安装 babel 及相关插件

    1
    2
    3
    4
    5
    # nodejs 环境无需 babel
    lerna add @babel/core packages/tool packages/tool-browser --dev
    lerna add @babel/preset-env packages/tool packages/tool-browser --dev
    lerna add @babel/preset-typescript packages/tool packages/tool-browser --dev
    lerna add @babel/plugin-transform-runtime packages/tool packages/tool-browser --dev
  3. 安装 rollup 及相关插件

    1
    2
    3
    4
    5
    6
    lerna add rollup --dev
    lerna add @rollup/plugin-babel packages/tool packages/tool-browser --dev
    lerna add @rollup/plugin-commonjs --dev
    lerna add @rollup/plugin-node-resolve --dev
    lerna add @rollup/plugin-typescript --dev
    lerna add rollup-plugin-clear --dev
  4. 安装 eslint 、prettier 及相关插件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    lerna add eslint --dev
    lerna add prettier --dev
    lerna add eslint-config-airbnb-base --dev
    lerna add eslint-config-airbnb-typescript --dev
    lerna add eslint-config-prettier --dev
    lerna add eslint-plugin-import --dev
    lerna add eslint-plugin-prettier --dev
    lerna add @typescript-eslint/eslint-plugin --dev
    lerna add @typescript-eslint/parser --dev

生成/编写相关配置文件

  1. 生成 tsconfig.json 文件,并添加自定义配置

    1
    ./node_modules/.bin/tsc --init

    配置如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    {
    "compilerOptions": {
    "target": "ES5", // tool-node: ES2021
    "module": "ESNext",
    "moduleResolution": "node",
    "declaration": true,
    "sourceMap": true,
    "outDir": "dist",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
    },
    "include": [
    "src"
    ],
    "exclude": [
    "dist"
    ]
    }
  2. 编写 .eslintrc.js 文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    module.exports = {
    env: {
    browser: true, // tool-browser 启用
    es2021: true,
    node: true, // tool-node 启用
    },
    extends: [
    'airbnb-base',
    'airbnb-typescript/base',
    'prettier',
    ],
    parser: '@typescript-eslint/parser',
    parserOptions: {
    project: './tsconfig.json',
    },
    ignorePatterns: ['.eslintrc.js','rollup.config.js','package.json','tsconfig.json','node_modules'],
    plugins: [
    '@typescript-eslint',
    'prettier',
    ],
    rules: {
    },
    };

  3. 编写 .eslintignore 文件

    1
    2
    3
    4
    dist/
    ems/
    node_modules/
    config/
  4. 编写 rollup.config.js 文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    import resolve from '@rollup/plugin-node-resolve'
    import commonjs from '@rollup/plugin-commonjs'
    import typescript from '@rollup/plugin-typescript'
    import babel from '@rollup/plugin-babel' // tool-node 无需
    import clear from 'rollup-plugin-clear'

    const shareConfig = {
    input: 'src/index.ts',
    external: [
    /@babel\/runtime/,
    ], // tool-node 无需
    plugins: [
    clear({
    targets: ['dist', 'esm'],
    }),
    resolve(),
    commonjs(),
    ],
    }

    export default [
    {
    ...shareConfig,
    plugins: [
    ...shareConfig.plugins,
    typescript({
    outDir: 'esm',
    }),
    babel({
    babelHelpers: 'runtime',
    extensions: ['.ts']
    })
    ],
    output: [
    {
    dir: 'esm',
    format: 'esm',
    sourcemap: true,
    preserveModules: true,
    exports: 'auto',
    },
    ]
    },
    {
    ...shareConfig,
    plugins: [
    ...shareConfig.plugins,
    typescript({
    outDir: 'lib',
    }),
    babel({
    babelHelpers: 'runtime',
    extensions: ['.ts']
    })
    ],
    output: [
    {
    dir: 'dist',
    format: 'cjs',
    sourcemap: true,
    preserveModules: true,
    exports: 'auto',
    // tool-node 启用 generatedCode
    // generatedCode: {
    // arrowFunctions: true,
    // constBindings: true,
    // objectShorthand: true,
    // preset: 'es2015',
    // }
    },
    ]
    }
    ]

  5. 编写 .babelrc

    tool-node 无需

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    {
    "presets": [
    [
    "@babel/preset-env",
    {
    "modules": false
    }
    ],
    "@babel/preset-typescript"
    ],
    "plugins": [
    "@babel/plugin-transform-runtime"
    ]
    }
  6. 生成 api-extractor.json

    1
    npx api-extractor init
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    {
    "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
    "mainEntryPointFilePath": "<projectFolder>/dist/index.d.ts",
    "bundledPackages": [],
    "compiler": {},
    "apiReport": {
    "enabled": false
    },
    "docModel": {
    "enabled": true,
    "apiJsonFilePath": "../../../temp/<unscopedPackageName>.api.json"
    },
    "dtsRollup": {
    "enabled": true,
    "untrimmedFilePath": "<projectFolder>/dist/<unscopedPackageName>.d.ts",
    "publicTrimmedFilePath": "<projectFolder>/esm/<unscopedPackageName>.d.ts"
    },
    "tsdocMetadata": {
    "enabled": false
    },
    "messages": {
    "compilerMessageReporting": {
    "default": {
    "logLevel": "warning"
    }
    },
    "extractorMessageReporting": {
    "default": {
    "logLevel": "warning"
    }
    },
    "tsdocMessageReporting": {
    "default": {
    "logLevel": "warning"
    }
    }
    }
    }

    api json 文件放在根目录下,方便生成 Doc

编写 npm script

package

1
2
3
4
5
6
7
8
9
10
11
12
{
...
"main": "dist/index.js",
"module": "esm/index.js",
"typings": "dist/index.d.ts",
"scripts": {
"build": "rollup -c",
"api": "api-extractor run",
"doc": "api-documenter markdown -i ../../temp -o ../../docs"
},
...
}

root

1
2
3
4
5
6
7
8
9
10
{
...
"scripts": {
"bootstrap": "npx lerna bootstrap",
"build": "lerna exec -- yarn build",
"build:api": "lerna exec -- yarn api",
"build:doc": "lerna exec -- yarn doc"
},
...
}

加入 Jest

  1. 安装 jest 及相关插件

    1
    2
    3
    4
    lerna add jest --dev
    lerna add @types/jest --dev
    lerna add ts-jest --dev
    lerna add babel-jest packages/tool packages/tool-browser --dev
  2. 编写 jest.config.js 文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    module.exports = {
    collectCoverage: true, // 是否收集测试时的覆盖率信息
    coverageDirectory: "coverage", // 覆盖率信息输出目录
    coverageProvider: "v8", // 用哪个提供程序来检测代码以进行覆盖。允许的值为babel(默认)或v8
    coverageThreshold: {
    global: {
    branches: 100,
    functions: 100,
    lines: 100,
    statements: 100,
    },
    }, // 配置覆盖结果的最小阈值强制执行
    preset: "ts-jest", // 用作 Jest 配置基础的预设
    testEnvironment: "jsdom", // 将用于测试的测试环境。 Jest 中的默认环境是 Node.js 环境。 如果你正在构建一个 web 应用程序,你可以通过 jsdom 来使用类似浏览器的环境
    verbose: true, // 是否应在运行期间报告每个单独的测试。
    };
  3. 编写测试文件

    type.test.ts
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    import { type } from "../../src";

    const { isUndefined, isNull } = type;

    describe("type:", () => {
    describe("isUndefined", () => {
    test(" undefined => true ", () => {
    expect(isUndefined(undefined)).toBe(true);
    });
    test(" null => false ", () => {
    expect(isUndefined(null)).toBe(false);
    });
    test(" 0 => false ", () => {
    expect(isUndefined(0)).toBe(false);
    });
    test(' "" => false ', () => {
    expect(isUndefined("")).toBe(false);
    });
    test(" {} => false ", () => {
    expect(isUndefined({})).toBe(false);
    });
    test(" [] => false ", () => {
    expect(isUndefined([])).toBe(false);
    });
    test(" () => false ", () => {
    expect(isUndefined(() => {})).toBe(false);
    });
    });
    });

  4. 配置 npm script
    package

    1
    2
    3
    4
    5
    {
    "scripts": {
    "test": "jest"
    }
    }

    root

    1
    2
    3
    4
    5
    {
    "scripts": {
    "test": "lerna exec -- yarn test"
    }
    }

配置 pre-commit git-hook

  1. 安装 husky

    1
    npx husky-init && yarn

    它将设置 husky,修改 package.json 并创建一个 pre-commit 的示例挂钩。默认情况下,它将 npm test 在您提交时运行。

  2. 安装相关依赖
    根目录安装即可

    1
    yarn add @commitlint/cli @commitlint/config-conventional commitizen lint-staged
  3. 配置相关文件

    1. commitizen 配置文件
      .czrc
      1
      2
      3
      {
      "path": "cz-conventional-changelog"
      }
    2. commitlint 配置文件
      commitlint.config.js
      1
      2
      3
      module.exports = {
      extends: ['@commitlint/config-conventional']
      }
    3. lint-staged 配置文件(各 package 内)
      .lintstagedrc.json
      1
      2
      3
      4
      {
      "**/*.{js,ts}": "npm run lint-staged:js",
      "**/*.{js,ts,md,json}": ["prettier --write"]
      }
  4. 添加 husky hook
    已经存在的 pre-commit

    1
    2
    3
    4
    5
    #!/bin/sh
    . "$(dirname "$0")/_/husky.sh"

    yarn lint-staged
    yarn test

    新增 commit-msg

    1
    yarn husky add .husky/commit-msg 'yarn commitlint --edit $1'
  5. npm script
    package 添加

    1
    2
    3
    4
    5
    {
    "scripts": {
    "lint-staged:js": "eslint --ext .js,.ts"
    }
    }

    root 添加

    1
    2
    3
    4
    5
    {
    "scripts": {
    "commit": "cz"
    }
    }

发布 npm

1
lerna publish

更多查看:lerna publish

完整的 npm script

package

1
2
3
4
5
6
7
8
9
10
11
{
...
"scripts": {
"build": "rollup -c",
"api": "api-extractor run",
"doc": "api-documenter markdown -i ../../temp -o ../../docs",
"test": "jest",
"lint-staged:js": "eslint --ext .js,.ts"
},
...
}

root

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
...
"scripts": {
"bootstrap": "npx lerna bootstrap",
"build": "lerna exec -- yarn build",
"build:api": "lerna exec -- yarn api",
"build:doc": "lerna exec -- yarn doc",
"test": "lerna exec -- yarn test",
"release": "yarn test && yarn build && yarn build:api && yarn build:doc && yarn publish",
"publish": "lerna publish",
"prepare": "husky install",
"commit": "cz"
},
...
}

项目地址

panacea