monorepo 如何解决别名问题

1,892 阅读3分钟

得益于 pnpm 软链接实现现在 monorepo 项目已经十分成熟了,不过在写项目时候还是遇到很多痛点,例如:

  1. 如果我有多个 packages 项目并且使用 TypeScript,那么我肯定希望是写的都是 ts,在需要调用的时候通过工具来完成这一过程的转译,不过在实际开发中可能使用了 vite 这样的工具,导致遇到问题只能通过其它方式来绕过去;
  2. 如果写一个后端项目,那么数据库定义的模型描述肯定希望在 web 项目也可以复用,不过在多入口中可能还需要通过 @new-house/src/xxxx.ts 来引用,这个 src/xxx 也太不简洁了。

所以这篇文章重点就是解决问题 2 ,问题 1 如果有机会再单独开一个篇幅来说,在正式讲解之前需要介绍一下 exports 这个属性

exports

这个属性添加于 v12.7.0,最简单的使用方式如下

{
  // package.json
  "exports": "./index.js"
}

它的优先级高于 main,除此之外更多是作为不同环境导入文件来使用,例如开发了一个 utils 的包希望它可以在 node 的 cjs 和 ems 环境下工作,那么它就可以发挥作用了。

{
  "exports": {
    ".": {
      "import": "./feature-node.mjs",
      "require": "./feature-node.cjs"
    },
  }
}

除此之外还可以解决目录别名问题,这也是为什么介绍它的原因

{
  "name": "my-package",
  "exports": {
    ".": "./lib/index.js",
    "./lib": "./lib/index.js",
    "./lib/*": "./lib/*.js",
    "./lib/*.js": "./lib/*.js",
    "./feature": "./feature/index.js",
    "./feature/*": "./feature/*.js",
    "./feature/*.js": "./feature/*.js",
    "./package.json": "./package.json"
  }
}

上面的 "./feature/*": "./feature/*.js", 以及 "./feature/*.js": "./feature/*.js", 等都可以通过 my-package/feature/xxx.js 来完成调用

exports 就介绍到这里,了解到它可以适配不同环境以及用于解决目录别名,下面就是项目实战。

TypeScript 下使用

为了方便,这里我已经搭建好了一个 pnpm monorepo 项目,它的文件结构如下

test
├─ node_modules
├─ package.json
├─ packages
│    ├─ utils
│    │    ├─ package.json
│    │    ├─ src
│    │    │    ├─ addition.ts
│    │    │    └─ subtraction.ts
│    │    └─ tsconfig.json
│    └─ web
│           ├─ babel.config.js
│           ├─ dist
│           │    └─ index.js
│           ├─ node_modules
│           ├─ package.json
│           ├─ src
│           │    └─ index.ts
│           ├─ tsconfig.json
│           └─ webpack.config.js
├─ pnpm-lock.yaml
├─ pnpm-workspace.yaml
└─ tsconfig.json

utils 这个项目用于给 web 项目使用,它的 package.json 内容如下

{
  "name": "@test/utils",
  "version": "1.0.0",
  "exports": {
    "./*": "./src/*.ts"
  }
}

这里设置 "./*" 是提示这个为一个子目录别名,同理你也可以设置 "test/*" 表示以 test 为开头,因为 utils 这个项目都是 ts 我并不想花时间每次都 build 一遍成 js 所以只能写成 ./src/*.ts

这里提示一下,如果是 ts 项目必须这个结尾,否则会提示找不到类型文件

第二步就是在 web 项目中引用

// web/src/index.ts
import addition from '@test/utils/addition';
import subtraction from '@test/utils/subtraction';

console.log(addition(1, 1));
console.log(subtraction(1, 1));

如果 TypeScript 提示说不到模块之类的错误,那么你需要调整一下 tsconfig.json 的配置

{
  "compilerOptions": {
    "moduleResolution": "NodeNext" // node 16也可以
  }
}

这样就消除了闹心的 src 目录

其它问题

TypeScript 使用别名如何定义类型文件

这个可以参考官方文档的实现

// package.json
{
  "name": "my-package",
  "type": "module",
  "exports": {
    ".": {
      // Entry-point for TypeScript resolution - must occur first!
      "types": "./types/index.d.ts",
      // Entry-point for `import "my-package"` in ESM
      "import": "./esm/index.js",
      // Entry-point for `require("my-package") in CJS
      "require": "./commonjs/index.cjs"
    }
  },
  // CJS fall-back for older versions of Node.js
  "main": "./commonjs/index.cjs",
  // Fall-back for older versions of TypeScript
  "types": "./types/index.d.ts"
}

额外在 exports 中添加 types 属性,相关讨论链接 github.com/microsoft/T…

设置了 moduleResolution 导致导入其它模块必须以.js 结尾

以.js 结尾是规范的规定,如果你使用 webpack5 来打包项目可以在配置文件中添加以下内容

{
  resolve: {
    extensionAlias: {
      ".js": [".ts", ".js"],
      ".cjs": [".cts", ".cjs"],
      ".mjs": [".mts", ".mjs"],
    }
    // ...
  }
  // ...
}

相关问题链接 Support TypeScript module resolution node16 · Issue #12625 · facebook/create-react-app (github.com)

参考链接