Monorepo 中 ES 模块管理核心是统一模块解析策略:所有包需明确声明 "type": "module",TS 配置用 module: "node16"/"nodenext" + moduleResolution: "bundler",构建工具须识别 workspace 路径;跨包导入必须走 package name(如 @myorg/utils)而非相对路径;ESLint 和 Prettier 通过根级配置统一校验与格式化,确保类型、解析、构建、运行时行为一致。

在 Monorepo 中管理 ES 模块,核心不是“怎么写 import”,而是怎么让所有包在类型、解析、构建、运行时都一致地识别和处理 ES 模块语义。尤其当项目混合了应用(apps)、库(packages)和工具(configs),ES 模块行为稍有不一致,就会出现 Cannot use import statement outside a module、类型丢失、Tree-shaking 失效、或 IDE 报错但实际能跑等典型问题。
以下三点是落地关键:
统一模块解析策略
pnpm workspace 下,子包之间用 workspace:* 引用,但 ES 模块能否正确加载,取决于三处配置是否协同:
-
package.json中每个包必须明确声明"type": "module"(纯 ESM)或保留"type": "commonjs"(需配合.mjs后缀); - TypeScript 编译目标(
tsconfig.json的module和moduleResolution)要与运行时一致:推荐module: "node16"或"nodenext"+moduleResolution: "bundler"; - 构建工具(如 Vite、Rollup、esbuild)的输入配置需识别 workspace 路径——例如 Vite 里
resolve.alias可映射@myorg/utils到packages/utils/src,避免相对路径污染。
跨包导入必须走 package name,不走相对路径
禁止在 apps/web/src/index.ts 里写 import { foo } from '../../packages/utils/src'。正确方式是:
- 在
packages/utils/package.json中定义"name": "@myorg/utils"和"exports"字段(支持 ESM/CJS 双模式); - 在
apps/web/package.json中以workspace:*声明依赖:"@myorg/utils": "workspace:*"; - 导入时直接用
import { foo } from '@myorg/utils'。
这样既能享受 pnpm 的符号链接优势,又能让 TS 类型检查、VSC 自动补全、打包器 Tree-shaking 全链路生效。
ESLint 和 Prettier 不干涉模块语法,但要守住边界
ESLint 本身不决定模块是否合法,但它能帮你提前发现隐患:
- 启用
eslint-plugin-import并配置import/resolver: { node: { extensions: ['.ts', '.tsx', '.js', '.jsx', '.mjs'] } },确保它能解析.mjs和exports字段; - 禁用
no-unused-vars对具名导入的误报(常见于import { something } from 'pkg'但只用了一部分); - Prettier 不处理模块逻辑,但要确保
.prettierrc中endOfLine: 'lf'和semi: false等风格设置,在所有包中通过根级配置统一生效,避免因格式差异导致git diff混淆模块变更。
本质上,Monorepo 里的 ES 模块管理,是一套「声明 → 解析 → 检查 → 构建」的闭环。不是每个包自己搞定,而是靠根目录的 tsconfig.base.json、eslint.config.js、pnpm-workspace.yaml 三层联动,把模块行为锁死。


















