如何在Yarn Workspaces的TypeScript共享包中使用相对路径
问题场景
使用Yarn Workspaces管理多包项目,其中apps/server依赖packages/shared模块。在server中尝试导入shared/interfaces时,编译后Node.js会报错ERR_MODULE_NOT_FOUND——因为它直接查找项目根目录下的interfaces/index.js,而非编译后的dist/interfaces/index.js。但直接导入shared(主入口)却能正常工作。
项目结构:
tsconfig.json package.json packages/ shared/ index.ts package.json tsconfig.json interfaces/ index.ts apps/ server/ index.ts package.json tsconfig.json
编译后node_modules中的shared包结构:
dist/ index.js index.d.ts interfaces/ index.js index.d.ts interfaces/ index.ts index.ts index.js
相关文件内容
根目录package.json
{ "name": "supperclub", "version": "1.0.0", "main": "index.js", "license": "MIT", "private": true, "workspaces": ["apps/*", "packages/*"], "devDependencies": { } }
根目录tsconfig.json
{ "references": [ { "path": "apps/server" }, { "path": "packages/shared" } ], "compilerOptions": { "target": "es2016", "module": "commonjs", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true } }
Shared包package.json
{ "name": "shared", "version": "1.0.0", "main": "dist/index.js", "types": "dist/index.d.ts", "license": "MIT", "scripts": { "compile": "tsc --build" }, "files": ["dist/"], "module": "CommonJS" }
Shared包tsconfig.json
{ "compilerOptions": { "composite": true, "target": "es2016", "module": "commonjs", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true, "outDir": "./dist", "declaration": true, "declarationMap": true, "sourceMap": true } }
Server包package.json
{ "name": "server", "version": "1.0.0", "main": "dist/index.js", "license": "MIT", "type": "module", "engines": { "node": ">=14.20.1 <19" }, "scripts": { "dev": "nodemon index.js" }, "devDependencies": { "typescript": "^4.5.4" }, "dependencies": { "supper-club-shared": "*" }, "references": [ { "path": "../../packages/supper-club-shared" } ] }
Server包tsconfig.json
{ "compilerOptions": { "target": "es2017", "module": "es6", "moduleResolution": "node", "baseUrl": "./", "resolveJsonModule": true, "sourceMap": true, "outDir": "./dist", "esModuleInterop": true, "strict": true, "skipLibCheck": true, "composite": true }, "references": [{"path": "../../packages/supper-club-shared"}], "include": ["src/**/*"] }
解决方法
方法1:通过主入口暴露子模块
在packages/shared/index.ts中统一导出子模块内容:
// packages/shared/index.ts export * from './interfaces';
之后在server中直接通过import { X } from 'shared'导入,Node.js会自动指向dist目录下的主入口文件。
方法2:为Shared包添加exports字段(推荐)
在shared/package.json中添加exports字段,明确子路径到dist文件的映射,同时配置类型文件的映射:
{ "name": "shared", // ... 原有字段 "exports": { ".": "./dist/index.js", "./interfaces": "./dist/interfaces/index.js", "./interfaces/*": "./dist/interfaces/*.js" }, "typesVersions": { "*": { "interfaces": ["./dist/interfaces/index.d.ts"], "interfaces/*": ["./dist/interfaces/*.d.ts"] } } }
exports告诉Node.js如何解析包的所有子路径typesVersions确保TypeScript能正确找到对应的类型定义
方法3:调整Shared包的源码目录结构
将Shared包的所有TS源码移至src目录:
packages/shared/ src/ index.ts interfaces/ index.ts package.json tsconfig.json
更新shared/tsconfig.json的include字段:
{ "compilerOptions": { // ... 原有配置 }, "include": ["src/**/*"] }
配合原有的files: ["dist/"]配置,Yarn Workspaces链接包时只会暴露dist目录,Node.js自然只会查找编译后的文件。
方法4:在Server中配置路径别名
在server/tsconfig.json中添加路径别名,将shared/*映射到dist目录:
{ "compilerOptions": { // ... 原有配置 "paths": { "shared/*": ["../packages/shared/dist/*"] } } }
如果使用ESM模块,还需额外配置package.json的imports字段,或使用ts-node/esbuild等工具处理别名解析。
内容的提问来源于stack exchange,提问作者Pelayo Martinez

