如何搭建含共享包的Deno与Node.js(Vite) Monorepo并解决类型问题?
Deno/Node.js(Vite) Monorepo 搭建方案(共享包双环境兼容+类型安全)
核心结论
完全可以用Deno的**dnt(deno-to-node)**替代tsc构建共享包,这是解决你当前前端类型丢失、配置繁琐问题的最优方案之一。dnt能自动将Deno风格的TypeScript代码转换为Node.js兼容的ES模块,并生成完整的类型定义文件,同时Deno后端仍可直接引用原始TS文件。
1. 调整目录结构(适配构建流程)
在现有基础上,给每个共享包新增构建输出目录,示例结构:
repo/ ├── packages/ │ ├── pkg-a/ # 共享包A │ │ ├── src/ # 源码目录 │ │ ├── dist/ # dnt构建输出(自动生成) │ │ └── build.ts # dnt构建脚本 │ └── pkg-b/ # 依赖pkg-a的共享包B │ ├── src/ │ ├── dist/ │ └── build.ts ├── backend/ │ ├── src/ │ └── deno.jsonc ├── frontend/ │ ├── src/ │ ├── vite.config.mts │ ├── tsconfig.json │ └── package.json └── scripts/ └── build-all-packages.ts # 批量构建脚本
2. 用dnt配置共享包构建
每个共享包的build.ts负责配置dnt的构建规则,以pkg-a为例:
// packages/pkg-a/build.ts import { build, emptyDir } from "https://deno.land/x/dnt@0.39.0/mod.ts"; await emptyDir("./dist"); await build({ entryPoints: ["./src/index.ts"], // 共享包入口文件 outDir: "./dist", shims: { // 根据需要添加Deno特有的API垫片(如Deno.env) deno: true, }, package: { // 生成的package.json配置 name: "@repo/pkg-a", version: "0.1.0", type: "module", exports: { ".": "./index.js", }, }, compilerOptions: { lib: ["ESNext"], strict: true, // 保证类型严格性 }, }); // 复制额外文件(如README、LICENSE)到dist目录 await Deno.copyFile("README.md", "./dist/README.md");
如果共享包之间有依赖(如pkg-b依赖pkg-a),只需在pkg-b的build.ts中添加本地依赖声明:
// packages/pkg-b/build.ts await build({ // ...其他配置 dependencies: { "@repo/pkg-a": "file:../pkg-a/dist", }, });
执行构建:在共享包目录下运行deno run -A build.ts,dist目录会生成Node兼容的.js文件和完整的.d.ts类型定义。
3. Deno后端配置:直接引用原始TS文件
通过import maps简化导入路径,避免冗长的相对路径,同时保持类型安全:
// backend/deno.jsonc { "imports": { "@repo/pkg-a/": "../packages/pkg-a/src/", "@repo/pkg-b/": "../packages/pkg-b/src/" }, "compilerOptions": { "strict": true } }
后端代码中直接导入:
// backend/src/main.ts import { foo } from "@repo/pkg-a/index.ts"; import { bar } from "@repo/pkg-b/utils.ts";
4. Vite前端配置:导入构建后的共享包
步骤1:添加本地依赖
在前端的package.json中添加共享包的本地路径依赖:
// frontend/package.json { "dependencies": { "@repo/pkg-a": "file:../packages/pkg-a/dist", "@repo/pkg-b": "file:../packages/pkg-b/dist" } }
步骤2:Vite配置(确保解析本地包)
// frontend/vite.config.mts import { defineConfig } from "vite"; export default defineConfig({ resolve: { // 如果有需要,添加别名映射(可选) alias: { "@repo": "../packages", }, }, });
步骤3:TS配置(保证类型识别)
// frontend/tsconfig.json { "compilerOptions": { "strict": true, "types": ["vite/client"], // 自动识别共享包的类型定义 "paths": { "@repo/pkg-a": ["../packages/pkg-a/dist/index.d.ts"], "@repo/pkg-b": ["../packages/pkg-b/dist/index.d.ts"] } }, "include": ["src/**/*", "../packages/**/dist/*.d.ts"] }
前端代码中直接导入:
// frontend/src/App.tsx import { foo } from "@repo/pkg-a"; import type { FooType } from "@repo/pkg-a"; // 类型导入也能正常识别
5. 自动化批量构建
为了避免逐个构建共享包,在根目录编写批量构建脚本:
// scripts/build-all-packages.ts import { walk } from "https://deno.land/std@0.208.0/fs/walk.ts"; async function buildPackages() { for await (const entry of walk("./packages", { includeFiles: ["build.ts"], })) { console.log(`Building ${entry.path}...`); // 执行每个共享包的构建脚本 await new Deno.Command(Deno.execPath(), { args: ["run", "-A", entry.path], cwd: entry.parent, stdout: "inherit", stderr: "inherit", }).output(); } } await buildPackages();
在根目录的deno.jsonc中添加任务:
{ "tasks": { "build:packages": "deno run -A scripts/build-all-packages.ts" } }
执行deno task build:packages即可一键构建所有共享包。
关键优势
- 类型安全:dnt生成的
.d.ts文件完整保留了TS类型约束,前端不会出现any类型丢失的问题。 - 双环境兼容:Deno后端直接用源码,前端用构建后的Node模块,无需修改共享包代码。
- 配置简洁:避免了Vite直接引用TS文件时的复杂配置,依赖管理更清晰。
内容的提问来源于stack exchange,提问作者andy-bc
相关产品推荐
相关产品推荐

