本页内容
工作区和 Monorepo
Deno 支持工作区,也称为“monorepos”,它允许您同时管理多个相关和相互依赖的包。
“工作区”是包含 deno.json
或 package.json
配置文件的文件夹集合。根目录下的 deno.json
文件定义了工作区。
{
"workspace": ["./add", "./subtract"]
}
这会配置一个包含 add
和 subtract
成员的工作区,这些目录应包含 deno.json(c)
和/或 package.json
文件。
Deno 使用 workspace
而非 npm 的 workspaces
来表示一个具有多个成员的单一工作区。
示例 Jump to heading
让我们扩展 deno.json
工作区示例并查看其功能。文件层次结构如下:
/
├── deno.json
├── main.ts
├── add/
│ ├── deno.json
│ └── mod.ts
└── subtract/
├── deno.json
└── mod.ts
有两个工作区成员(add 和 subtract),每个都包含 mod.ts
文件。此外,还有一个根目录 deno.json
和一个 main.ts
。
顶层 deno.json
配置文件定义了工作区和应用于所有成员的顶层导入映射
{
"workspace": ["./add", "./subtract"],
"imports": {
"chalk": "npm:chalk@5"
}
}
根目录下的 main.ts
文件使用导入映射中的 chalk
裸指定符,并从工作区成员中导入 add
和 subtract
函数。请注意,它使用 @scope/add
和 @scope/subtract
导入它们,即使这些不是正确的 URL 并且不在导入映射中。它们是如何解析的?
import chalk from "chalk";
import { add } from "@scope/add";
import { subtract } from "@scope/subtract";
console.log("1 + 2 =", chalk.green(add(1, 2)));
console.log("2 - 4 =", chalk.red(subtract(2, 4)));
在 add/
子目录中,我们定义了一个带有 "name"
字段的 deno.json
,这对于引用工作区成员很重要。deno.json
文件还包含示例配置,例如在使用 deno fmt
时关闭分号。
{
"name": "@scope/add",
"version": "0.1.0",
"exports": "./mod.ts",
"fmt": {
"semiColons": false
}
}
export function add(a: number, b: number): number {
return a + b;
}
subtract/
子目录类似,但没有相同的 deno fmt
配置。
{
"name": "@scope/subtract",
"version": "0.3.0",
"exports": "./mod.ts"
}
import { add } from "@scope/add";
export function subtract(a: number, b: number): number {
return add(a, b * -1);
}
让我们运行它
> deno run main.ts
1 + 2 = 3
2 - 4 = -2
这里有很多值得探讨的地方,展示了 Deno 工作区的一些特性
-
这个 monorepo 由两个包组成,分别放置在
./add
和./subtract
目录中。 -
通过在成员的
deno.json
文件中使用name
和version
选项,可以在整个工作区中使用“裸指定符”引用它们。在此示例中,这些包命名为@scope/add
和@scope/subtract
,其中scope
是您可以选择的“scope”名称。有了这两个选项,就不必在导入语句中使用冗长且相对的文件路径。 -
npm:chalk@5
包是整个工作区的共享依赖项。工作区成员“继承”工作区根目录的imports
,从而可以轻松地在整个代码库中管理单个版本的依赖项。 -
add
子目录在其deno.json
中指定deno fmt
在格式化代码时不应应用分号。这使得现有项目的过渡更加顺畅,无需一次性更改数十或数百个文件。
Deno 工作区是灵活的,可以与 Node 包一起使用。为了使现有 Node.js 项目的迁移更容易,您可以在单个工作区中同时拥有 Deno 优先和 Node 优先的包。
工作区路径模式 Jump to heading
Deno 支持工作区成员文件夹的模式匹配,从而更轻松地管理具有许多成员或特定目录结构的工作区。您可以使用通配符模式一次性包含多个目录
{
"workspace": [
"some-dir/*",
"other-dir/*/*"
]
}
模式匹配语法遵循关于文件夹深度的特定规则
some-path/*
匹配 some-path
中直接的文件和目录(仅限第一级缩进)。例如,对于 packages/*
,这包括 packages/foo
和 packages/bar
,但不包括 packages/foo/subpackage
。
some-path/*/*
匹配位于 some-path
子目录中的文件和目录(第二级缩进)。它不匹配 some-path
中直接的项目。例如,对于 examples/*/*
,这包括 examples/basic/demo
和 examples/advanced/sample
,但不包括 examples/basic
。
模式中的每个 /*
片段都对应于相对于基本路径的特定文件夹深度。这允许精确地定位目录结构中不同级别的工作区成员。
Deno 如何解析工作区依赖项 Jump to heading
当在工作区中运行一个项目(该项目从另一个工作区成员导入)时,Deno 遵循以下步骤来解析依赖项
- Deno 从执行项目(例如,项目 A)的目录开始
- 它在父目录中查找根
deno.json
文件 - 如果找到,它会检查该文件中的
workspace
属性 - 对于项目 A 中的每个 import 语句,Deno 检查该 import 是否与任何工作区成员的
deno.json
中定义的包名称匹配 - 如果找到匹配的包名称,Deno 会验证包含该包的目录是否列在根工作区配置中
- 然后使用工作区成员的
deno.json
中的exports
字段将 import 解析到正确的文件
例如,给定此结构
/
├── deno.json # workspace: ["./project-a", "./project-b"]
├── project-a/
│ ├── deno.json # name: "@scope/project-a"
│ └── mod.ts # imports from "@scope/project-b"
└── project-b/
├── deno.json # name: "@scope/project-b"
└── mod.ts
当 project-a/mod.ts
从 "@scope/project-b"
导入时,Deno 会
- 查看 import 语句
- 检查父目录的
deno.json
- 在 workspace 数组中找到
project-b
- 验证
project-b/deno.json
是否存在且具有匹配的包名称 - 使用
project-b
的 exports 解析 import
容器化的重要注意事项 Jump to heading
当对依赖于其他工作区成员的工作区成员进行容器化时,您必须包含
- 根目录
deno.json
文件 - 所有依赖的工作区包
- 与开发环境相同的目录结构
例如,如果 dockerizing 上面的 project-a
,您的 Dockerfile 应该
COPY deno.json /app/deno.json
COPY project-a/ /app/project-a/
COPY project-b/ /app/project-b/
这保留了 Deno 用于查找和导入工作区依赖项的工作区解析机制。
多个包入口 Jump to heading
exports
属性详细说明了入口点,并公开了您的包应允许用户导入哪些模块。
到目前为止,我们的包只有一个入口。这对于简单的包来说很好,但通常您会希望有多个入口,将包的相关方面分组。这可以通过将 object
而不是 string
传递给 exports
来实现
{
"name": "@scope/my-package",
"version": "0.3.0",
"exports": {
".": "./mod.ts",
"./foo": "./foo.ts",
"./other": "./dir/other.ts"
}
}
"."
入口是导入 @scope/my-package
时选择的默认入口。因此,上面的 deno.json
示例提供了以下入口
@scope/my-package
@scope/my-package/foo
@scope/my-package/other
将工作区包发布到注册表 Jump to heading
工作区使得将包发布到 JSR 或 NPM 等注册表变得容易。您可以发布单个工作区成员,同时在您的 monorepo 中保持它们的开发关联。
发布到 JSR Jump to heading
要将工作区包发布到 JSR,请遵循以下步骤
- 确保每个包在其
deno.json
中包含适当的元数据
{
"name": "@scope/my-package",
"version": "1.0.0",
"exports": "./mod.ts",
"publish": {
"exclude": ["tests/", "*.test.ts", "examples/"]
}
}
- 导航到特定包目录并发布
cd my-package
deno publish
管理相互依赖的包 Jump to heading
当发布具有相互依赖关系的工作区中的包时,请在相关包之间使用一致的版本控制方案。首先发布被依赖的包,然后发布依赖它们的包。发布后,验证已发布的包是否按预期工作
# Test a published package
deno add jsr:@scope/my-published-package
deno test integration-test.ts
当发布依赖于其他工作区成员的包时,Deno 将自动将已发布代码中的工作区引用替换为正确的注册表引用。
从 npm
工作区迁移 Jump to heading
Deno 工作区支持从现有 npm 包中使用 Deno 优先的包。在此示例中,我们混合使用了一个名为 @deno/hi
的 Deno 库,以及一个几年前开发的名为 @deno/log
的 Node.js 库。
我们需要在根目录中包含一个 deno.json
配置文件
{
"workspace": {
"members": ["hi"]
}
}
以及我们现有的 package.json 工作区
{
"workspaces": ["log"]
}
该工作区目前有一个 log npm 包
{
"name": "@deno/log",
"version": "0.5.0",
"type": "module",
"main": "index.js"
}
export function log(output) {
console.log(output);
}
让我们创建一个导入 @deno/log
的 @deno/hi
Deno 优先包
{
"name": "@deno/hi",
"version": "0.2.0",
"exports": "./mod.ts",
"imports": {
"log": "npm:@deno/log@^0.5"
}
}
import { log } from "log";
export function sayHiTo(name: string) {
log(`Hi, ${name}!`);
}
现在,我们可以编写一个导入并调用 hi
的 main.ts
文件
import { sayHiTo } from "@deno/hi";
sayHiTo("friend");
$ deno run main.ts
Hi, friend!
您甚至可以在现有的 Node.js 包中同时拥有 deno.json
和 package.json
。此外,您可以删除根目录中的 package.json,并在 deno.json 工作区成员中指定 npm 包。这允许您逐步迁移到 Deno,而无需进行大量前期工作。
例如,您可以添加 log/deno.json
来配置 Deno 的 linter 和 formatter
{
"fmt": {
"semiColons": false
},
"lint": {
"rules": {
"exclude": ["no-unused-vars"]
}
}
}
在工作区中运行 deno fmt
会将 log
包格式化为不包含任何分号,并且如果您在其中一个源文件中留下未使用的变量,deno lint
也不会报错。
配置内置 Deno 工具 Jump to heading
某些配置选项仅在工作区根目录中才有意义,例如,在其中一个成员中指定 nodeModulesDir
选项是不可用的,如果选项需要应用于工作区根目录,Deno 将发出警告。
这是工作区根目录及其成员中可用的各种 deno.json
选项的完整矩阵
选项 | 工作区 | 包 | 备注 |
---|---|---|---|
compilerOptions | ✅ | ✅ | |
importMap | ✅ | ❌ | 每个配置文件与 imports 和 scopes 互斥。此外,不支持在工作区配置中拥有 importMap,而在包配置中拥有 imports。 |
imports | ✅ | ✅ | 每个配置文件与 importMap 互斥。 |
scopes | ✅ | ❌ | 每个配置文件与 importMap 互斥。 |
exclude | ✅ | ✅ | |
lint.include | ✅ | ✅ | |
lint.exclude | ✅ | ✅ | |
lint.files | ⚠️ | ❌ | 已弃用 |
lint.rules.tags | ✅ | ✅ | 标签通过将包添加到工作区列表来合并。重复项将被忽略。 |
lint.rules.include | |||
lint.rules.exclude | ✅ | ✅ | 规则按包合并,包优先于工作区(包 include 强于工作区 exclude)。 |
lint.report | ✅ | ❌ | 一次只能激活一个报告器,因此如果文件跨多个包,允许每个工作区有不同的报告器将无法工作。 |
fmt.include | ✅ | ✅ | |
fmt.exclude | ✅ | ✅ | |
fmt.files | ⚠️ | ❌ | 已弃用 |
fmt.useTabs | ✅ | ✅ | 包优先于工作区。 |
fmt.indentWidth | ✅ | ✅ | 包优先于工作区。 |
fmt.singleQuote | ✅ | ✅ | 包优先于工作区。 |
fmt.proseWrap | ✅ | ✅ | 包优先于工作区。 |
fmt.semiColons | ✅ | ✅ | 包优先于工作区。 |
fmt.options.* | ⚠️ | ❌ | 已弃用 |
nodeModulesDir | ✅ | ❌ | 整个工作区的解析行为必须相同。 |
vendor | ✅ | ❌ | 整个工作区的解析行为必须相同。 |
tasks | ✅ | ✅ | 包任务优先于工作区。使用的 cwd 是任务所在配置文件的 cwd。 |
test.include | ✅ | ✅ | |
test.exclude | ✅ | ✅ | |
test.files | ⚠️ | ❌ | 已弃用 |
publish.include | ✅ | ✅ | |
publish.exclude | ✅ | ✅ | |
bench.include | ✅ | ✅ | |
bench.exclude | ✅ | ✅ | |
bench.files | ⚠️ | ❌ | 已弃用 |
lock | ✅ | ❌ | 每个解析器只能存在一个锁定文件,并且每个工作区只能存在一个解析器,因此按包条件启用锁定文件没有意义。 |
unstable | ✅ | ❌ | 为简单起见,我们不允许不稳定的标志,因为许多 CLI 假设不稳定的标志是不可变的且对整个进程全局。也与 DENO_UNSTABLE_* 标志存在奇怪的交互。 |
名称 | ❌ | ✅ | |
version | ❌ | ✅ | |
exports | ❌ | ✅ | |
workspace | ✅ | ❌ | 不支持嵌套工作区。 |
跨工作区运行命令 Jump to heading
Deno 提供了多种方法来跨所有或特定工作区成员运行命令
运行测试 Jump to heading
要跨所有工作区成员运行测试,只需从工作区根目录执行 deno test
deno test
这将根据其各自的测试配置在所有工作区成员中运行测试。
要为特定工作区成员运行测试,您可以选择
- 切换到该成员的目录并运行测试命令
cd my-directory
deno test
- 或者从工作区根目录指定路径
deno test my-directory/
格式化和 Linting Jump to heading
与测试类似,格式化和 linting 命令默认在所有工作区成员中运行
deno fmt
deno lint
每个工作区成员都遵循其 deno.json
文件中定义的自己的格式化和 linting 规则,其中一些设置继承自上表中所示的根配置。
使用工作区任务 Jump to heading
您可以在工作区根目录和各个工作区成员中定义任务
{
"workspace": ["./add", "./subtract"],
"tasks": {
"build": "echo 'Building all packages'",
"test:all": "deno test"
}
}
{
"name": "@scope/add",
"version": "0.1.0",
"exports": "./mod.ts",
"tasks": {
"build": "echo 'Building add package'",
"test": "deno test"
}
}
要运行在特定包中定义的任务
deno task --cwd=add build
共享和管理依赖项 Jump to heading
工作区提供了强大的方式来共享和管理项目之间的依赖项
共享开发依赖项 Jump to heading
常见的开发依赖项(如测试库)可以在工作区根目录中定义
{
"workspace": ["./add", "./subtract"],
"imports": {
"@std/testing/": "jsr:@std/testing@^0.218.0/",
"chai": "npm:chai@^4.3.7"
}
}
这使得所有工作区成员都可以使用这些依赖项,而无需重新定义它们。
管理版本冲突 Jump to heading
在解析依赖项时,工作区成员可以覆盖根目录中定义的依赖项。如果根目录和成员都指定了相同依赖项的不同版本,则在解析该成员的文件夹内时,将使用成员的版本。这允许单个包在需要时使用特定的依赖项版本。
但是,成员特定的依赖项仅限于该成员的文件夹。在成员文件夹之外,或在处理工作区根级别的文件时,将使用工作区根目录的导入映射来解析依赖项(包括 JSR 和 HTTPS 依赖项)。
相互依赖的工作区成员 Jump to heading
如前面 add
和 subtract
模块的示例所示,工作区成员可以相互依赖。这实现了职责的清晰分离,同时保持了共同开发和测试相互依赖模块的能力。
subtract
模块从 add
模块导入功能,展示了工作区成员如何相互构建
import { add } from "@scope/add";
export function subtract(a: number, b: number): number {
return add(a, b * -1);
}
这种方法允许您
- 将复杂项目分解为可管理、单一用途的包
- 在不发布到注册表的情况下在包之间共享代码
- 共同测试和开发相互依赖的模块
- 逐步将整体式代码库迁移到模块化架构
在 package.json 中使用 workspace 协议 Jump to heading
Deno 支持 package.json
文件中的 workspace 协议指定符。当您有依赖于工作区内其他包的 npm 包时,这些指定符很有用
{
"name": "my-npm-package",
"dependencies": {
"another-workspace-package": "workspace:*"
}
}
支持以下 workspace 协议指定符
workspace:*
- 使用工作区中可用的最新版本workspace:~
- 使用工作区版本,仅限补丁级别更改workspace:^
- 使用工作区版本,兼容 semver 更改
npm 和 pnpm 工作区兼容性 Jump to heading
Deno 与 package.json
中定义的标准 npm 工作区无缝协作
{
"workspaces": ["packages/*"]
}
对于 pnpm 用户,Deno 支持典型的 pnpm 工作区配置。但是,如果您使用 pnpm-workspace.yaml
文件,则需要迁移到 deno.json
工作区配置
packages:
- "packages/*"
应转换为
{
"workspace": ["packages/*"]
}
这允许 Deno 和 npm/pnpm 生态系统在迁移或混合项目中平滑集成。
有关项目配置的更多信息,请查看使用 deno.json 进行配置教程。