发布于 2026-01-06 2 阅读
0

使用 PNPM 工作区搭建 Monorepo,并使用 Nx 加速它!

使用 PNPM 工作区搭建 Monorepo,并使用 Nx 加速它!

重要提示 -虽然本文仍然有效,但我们已大幅改进了使用 PNPM 工作区的体验。我们现在提供关于 PNPM 工作区和 Nx 的更新版免费课程,并且最近宣布了在 NPM/PNPM 工作区中使用 Nx 的全新体验


本文将深入探讨如何使用PNPM 工作区搭建一个新的单体仓库,该仓库将托管一个 Remix 应用和一个基于 React 的库。我们将学习如何使用 PNPM 运行命令,如何并行运行命令,最后还会添加 Nx 来实现更复杂的任务调度,包括命令缓存等功能。

重要提示:如果您已经熟悉新的 PNPM 工作区的设置和配置,请直接跳到本文后面添加 Nx 的部分。

更喜欢视频演示?

目录


想跟着一起做吗?这是 Git 仓库:
https://github.com/nrwl/nx-recipes/tree/main/pnpm-workspace


初始化一个新的PNPM工作区

首先,请确保您已安装 PNPM。官方文档中有详细的安装说明。如果您需要处理多个不同版本的 NPM/PNPM 和 Node.js,我特别推荐使用Volta之类的工具。

让我们创建一个名为 `<folder_name>` 的新文件夹pnpm-mono,进入该文件夹,然后运行 ​​`npm run pnpm initbuild` 生成一个顶级目录。这将是我们 PNPM 单体仓库的package.json根目录。package.json

mkdir pnpm-monocd pnpm-mono
❯ pnpm init
Enter fullscreen mode Exit fullscreen mode

初始化一个新的 Git 仓库可能也很有用,这样我们就可以在设置过程中提交和备份内容:

❯ git init
Enter fullscreen mode Exit fullscreen mode

此时,我们还可以创建一个.gitignore文件,立即排除诸如node_modules常见的构建输出文件夹之类的内容。

# .gitignore
node_modules
dist
build
Enter fullscreen mode Exit fullscreen mode

设置 Monorepo 结构

单体仓库的结构可能因用途而异。单体仓库通常分为两种类型:

  • 以包为中心的仓库用于开发和发布一组可重用的包。这在开源领域是一种常见的架构,例如AngularReactVue等许多项目的仓库都采用了这种架构。这些仓库通常包含一个文件夹,并会被发布到NPMpackages等公共注册表中
  • 以应用为中心的代码仓库主要用于开发应用程序和产品。这在公司中很常见。这类代码仓库的特点是包含一个 ` <app-build>` 文件夹apps和一个packages`<app-build>`libs文件夹。` <app-build>`apps文件夹包含可构建和可部署的应用程序,而packages`<app-build>`libs文件夹包含特定于一个或多个正在该单体仓库中开发的应用程序的库。您仍然可以将其中一些库发布到公共注册表。

在本文中,我们将采用“以应用为中心”的方法,来演示如何创建一个能够从 monorepo 内部使用软件包的应用。

在以下位置创建apps文件packagespnpm-mono

mkdir apps packages
Enter fullscreen mode Exit fullscreen mode

现在我们来配置 PNPM,使其能够正确识别 monorepo 工作区。基本上,我们需要pnpm-workspace.yaml在仓库根目录创建一个文件,定义我们的 monorepo 结构:

# pnpm-workspace.yaml
packages:
  # executable/launchable applications
  - 'apps/*'
  # all packages in subdirs of packages/ and components/
  - 'packages/*'
Enter fullscreen mode Exit fullscreen mode

添加 Remix 应用程序

现在我们应该可以添加第一个应用程序了。在这个例子中我选择了Remix,但实际上你可以在这里托管任何类型的应用程序,这并不重要。

信息:我们在这里使用标准的Remix 安装和设置程序,您可以在他们的文档页面上找到该程序。

由于我们希望将应用程序放在该apps文件夹中,因此我们需要cd进入该文件夹:

❯ cd apps
❯ npx create-remix@latest
Enter fullscreen mode Exit fullscreen mode

系统会要求您输入应用名称。我们这里就用“my-remix-app”吧,本文后续部分都会使用这个名称。当然,您也可以使用其他名称。此外,Remix 设置过程还会询问您几个问题,用于自定义设置。这些具体选项与本文关系不大,您可以根据自己的需求选择最合适的选项。

现在你应该在文件夹(或其他你选择的名称)中看到一个名为 Remix 的应用apps/my-remix-app。Remix 已经package.json配置好了相应的脚本:

{
  "private": true,
  "sideEffects": false,
  "scripts": {
    "build": "remix build",
    "dev": "remix dev",
    "start": "remix-serve build"
  },
  ...
}
Enter fullscreen mode Exit fullscreen mode

通常,在单体仓库中,为了避免频繁切换文件夹,你会希望从仓库根目录运行命令。PNPM 工作区提供了一种实现方式,通过传递filter参数,例如:

❯ pnpm --filter <package-name> <command>
Enter fullscreen mode Exit fullscreen mode

现在(在撰写本文时),Remix 的默认设置中package.json没有name定义 PNPM 运行该包所需的属性。因此,让我们在 Remix 中定义一个apps/my-remix-app/package.json

{
  "name": "my-remix-app",
  "private": true,
  "sideEffects": false,
  ...
}
Enter fullscreen mode Exit fullscreen mode

现在您应该可以使用以下命令在开发模式下运行您的 Remix 应用:

❯ pnpm --filter my-remix-app dev 
Enter fullscreen mode Exit fullscreen mode

在 PNPM 工作区中运行的 Remix 应用

创建共享 UI 库

现在我们的应用程序已经设置好了,让我们创建一个可供我们的应用程序使用的库包。

cd packagesmkdir shared-ui
Enter fullscreen mode Exit fullscreen mode

接下来,我们创建一个package.json包含以下内容的文本框(您也可以使用pnpm init并进行调整):

{
  "private": true,
  "name": "shared-ui",
  "description": "Shared UI components",
  "scripts": {},
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
  },
  "devDependencies": {
  }
}
Enter fullscreen mode Exit fullscreen mode

请注意,我们这样声明是private因为我们不想将其发布到 NPM 或其他地方,而只是想在本地工作区中引用和使用它。我还移除了该version属性,因为它没有被使用。

技术栈方面,我选择了React(这样我们就可以在 Remix 中导入它)和TypeScript(因为它现在几乎可以算作标准)。让我们从工作区的根目录安装这些依赖项:

❯ pnpm add --filter shared-ui react
❯ pnpm add --filter shared-ui typescript -D  
Enter fullscreen mode Exit fullscreen mode

通过向--filter shared-ui安装命令传递参数,我们将这些 NPM 包安装到shared-ui库的本地位置。

注意:如果库包和使用者(例如我们的应用)使用的 React/TypeScript 版本不同,则可能会导致版本冲突。采用单一版本策略,将包移至 monoreopo 的根目录,是解决此问题的一种可能方案。

我们的第一个组件将是一个非常简单的Button组件。那么,让我们来创建一个吧:

// packages/shared-ui/Button.tsx
import * as React from 'react';

export function Button(props: any) {
  return <button onClick={() => props.onClick()}>{props.children}</button>;
}

export default Button;
Enter fullscreen mode Exit fullscreen mode

shared-ui我们还希望提供一个公共 API,用于导出组件以便在我们的软件包外部使用:

// packages/shared-ui/index.tsx
export * from './Button';
Enter fullscreen mode Exit fullscreen mode

为了简单起见,我们直接使用 TypeScript 编译器来编译我们的包。当然,我们也可以使用Rollup或其他你喜欢的工具来打包多个文件等等,但这超出了本文的讨论范围。

要创建所需的编译输出,请创建一个packages/shared-ui/tsconfig.json具有以下配置的文件。

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "allowJs": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "module": "commonjs",
    "outDir": "./dist"
  },
  "include": ["."],
  "exclude": ["dist", "node_modules", "**/*.spec.ts"]
}
Enter fullscreen mode Exit fullscreen mode

在单体仓库中,最佳实践是将通用配置部分提取到一个更高级别的配置中(例如根目录),然后在各个项目中进行扩展。这样做是为了避免单体仓库中各个包之间出现大量重复配置。为了简单起见,我在这里将所有配置都放在了一个地方。

如您所见,它outDir指向一个包本地文件夹。因此,我们应该在dist中添加一个主入口点shared-uipackage.json

{
  "private": true,
  "name": "shared-ui",
  "main": "dist/index.js",
}
Enter fullscreen mode Exit fullscreen mode

最后,实际构建过程包括删除先前输出中的一些残留文件夹,然后调用 TypeScript 编译器(tsc)。以下是完整的packages/shared-ui/package.json文件:

{
  "private": true,
  "name": "shared-ui",
  "description": "Shared UI components",
  "main": "dist/index.js",
  "scripts": {
    "build": "rm -rf dist && tsc"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^17.0.2"
  },
  "devDependencies": {
    "typescript": "^4.6.4"
  }
}
Enter fullscreen mode Exit fullscreen mode

使用以下命令从 PNPM 工作区的根目录运行构建:

❯ pnpm --filter shared-ui build
Enter fullscreen mode Exit fullscreen mode

如果编译成功,您应该会在packages/shared-ui/dist文件夹中看到编译后的输出文件。

从 Remix 应用中使用我们的共享 UI 包

我们的shared-ui库已经准备就绪,可以在apps仓库文件夹中托管的 Remix 应用中使用它。我们可以手动将依赖项添加到 Remix 中package.json,也可以使用 PNPM 添加:

❯ pnpm add shared-ui --filter my-remix-app --workspace
Enter fullscreen mode Exit fullscreen mode

这会将其添加到依赖项中apps/my-remix-app/package.json

{
  "name": "my-remix-app",
  "private": true,
  "sideEffects": false,
  ...
  "dependencies": {
    ...
    "shared-ui": "workspace:*"
  },
  ...
}

Enter fullscreen mode Exit fullscreen mode

workspace:*这表示该包是在本地工作区解析的,而不是从远程注册表(例如NPM)解析的。这*仅仅表明我们希望依赖该包的最新版本,而不是某个特定版本。只有在使用外部 NPM 包时,使用特定版本才有意义。

要使用我们的Button组件,现在我们需要从某个 Remix 路由导入它。请将内容替换apps/my-remix-app/app/routes/index.tsx为以下内容:

// apps/my-remix-app/app/routes/index.tsx
import { Button } from 'shared-ui';

export default function Index() {
  return (
    <div>
      <Button onClick={() => console.log('clicked')}>Click me</Button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

现在再次运行 Remix 应用,你应该会看到按钮渲染出来。

❯ pnpm --filter my-remix-app dev 
Enter fullscreen mode Exit fullscreen mode

如果您遇到以下错误,则是因为您需要shared-ui先进行构建。

Error: Cannot find module '/Users/juri/nrwl/content/pnpm-demos/pnpm-mono/apps/my-remix-app/node_modules/shared-ui/dist/index.js'. Please verify that the package.json has a valid "main" entry  
    at tryPackage (node:internal/modules/cjs/loader:353:19)  
    at Function.Module._findPath (node:internal/modules/cjs/loader:566:18)  
    at Function.Module._resolveFilename (node:internal/modules/cjs/loader:919:27)  
    at Function.Module._load (node:internal/modules/cjs/loader:778:27)  
    at Module.require (node:internal/modules/cjs/loader:1005:19)  
    at require (node:internal/modules/cjs/helpers:102:18)  
    at Object.<anonymous> (/Users/juri/nrwl/content/pnpm-demos/pnpm-mono/apps/my-remix-app/app/routes/index.tsx:1:24)  
    at Module._compile (node:internal/modules/cjs/loader:1105:14)  
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1159:10)  
    at Module.load (node:internal/modules/cjs/loader:981:32)
Enter fullscreen mode Exit fullscreen mode

要构建它,请运行

❯ pnpm --filter shared-ui build
Enter fullscreen mode Exit fullscreen mode

为什么?这是因为 PNPM 会创建符号链接来引用和解析本地依赖项。通过shared-ui: "workspace:*"向 Remix添加配置,package.json您指示 PNPM 添加一个指向 Remixnode_modules文件夹的符号链接。

PNPM 如何本地链接软件包

使用 PNPM 运行命令

PNPM 提供了一些便捷的功能,可以在单体仓库工作区内运行命令。我们已经了解了如何使用--filter`:`将命令的作用域限定在单个包上。

❯ pnpm --filter my-remix-app dev
Enter fullscreen mode Exit fullscreen mode

您还可以使用该标志对工作区中的所有包递归运行命令-r。例如,想象一下对所有项目运行构建。

❯ pnpm run -r build

Scope: 2 of 3 workspace projects
packages/shared-ui build$ rm -rf dist && tsc
└─ Done in 603ms
apps/my-remix-app build$ remix build
│ Building Remix app in production mode...
│ The path "shared-ui" is imported in app/routes/index.tsx but shared-ui is not listed in your package.json 
│ Built in 156ms
└─ Done in 547ms
Enter fullscreen mode Exit fullscreen mode

同样,您可以使用以下方法并行化运行:--parallel

❯ pnpm run --parallel -r build

Scope: 2 of 3 workspace projects
apps/my-remix-app build$ remix build
packages/shared-ui build$ rm -rf dist && tsc
apps/my-remix-app build: Building Remix app in production mode...
apps/my-remix-app build: The path "shared-ui" is imported in app/routes/index.tsx but shared-ui is not listed in your package.json dependencies. Did you forget to install it?
apps/my-remix-app build: Built in 176ms
apps/my-remix-app build: Done
packages/shared-ui build: Done
Enter fullscreen mode Exit fullscreen mode

使用 Nx 加速

PNPM 工作区提供了一些基本功能,可以对 monorepo 包执行任务,甚至可以并行执行。随着 monorepo 的增长,您可能需要更复杂的方法,以便能够……

  • 仅对已更改的软件包运行任务
  • 基于文件内容的高级缓存机制,避免重复执行之前已经计算过的内容。
  • 远程分布式缓存可加速您的持续集成 (CI)。

这正是Nx 的用武之地。它针对单体仓库场景进行了优化,并配备了先进的任务调度机制。我们仍然依赖 PNPM 工作区提供的包安装和链接机制,但使用 Nx 来以最高效的方式运行任务。

安装 Nx

由于Nx将用于在整个 monorepo 工作区中运行操作,我们将把它安装在根级别package.json

❯ pnpm add nx -D -w
Enter fullscreen mode Exit fullscreen mode

就是这样。

使用 Nx 运行任务

Nx 使用以下形式运行您的命令:

❯ npx nx <target> <project>
Enter fullscreen mode Exit fullscreen mode

target在本例中,您要执行的 NPM 脚本是什么?

让我们尝试shared-ui使用以下命令运行软件包的构建:

❯ npx nx build shared-ui
Enter fullscreen mode Exit fullscreen mode

这将产生以下输出

> nx run shared-ui:build


> shared-ui@ build /Users/juri/nrwl/content/pnpm-demos/pnpm-mono/packages/shared-ui
> rm -rf dist && tsc


 >  NX   Successfully ran target build for project shared-ui (1s)
Enter fullscreen mode Exit fullscreen mode

Nx 会自动查找shared-ui并运行在 .build中定义的脚本packages/shared-ui/package.json

同样,要启动我们的 Remix 应用,请运行npx nx dev my-remix-app

我们还可以使用以下命令在多个项目中并行运行命令:

❯ npx nx run-many --target=build --all

    ✔  nx run my-remix-app:build (1s)
    ✔  nx run shared-ui:build (1s)

 >  NX   Successfully ran target build for 2 projects (1s)
Enter fullscreen mode Exit fullscreen mode

或者有选择地指定项目

❯ npx nx run-many --target=build --projects=my-remix-app,shared-ui

    ✔  nx run my-remix-app:build (1s)
    ✔  nx run shared-ui:build (1s)

 >  NX   Successfully ran target build for 2 projects (1s)
Enter fullscreen mode Exit fullscreen mode

请注意,我在命令前添加了 `--nx` 前缀,npx这会运行文件夹中的 Nx 可执行文件node_modules。这样我就不必nx全局安装。如果您更喜欢全局安装,也可以这样做。

配置缓存

将 Nx 添加到我们的 PNPM 工作区的主要优势之一是通过缓存提升速度计算缓存功能会收集不同的输入(源文件、环境变量、命令标志等),并计算哈希值并将其存储在本地文件夹中。下次运行命令时,Nx 会查找匹配的哈希值,如果找到,则直接恢复。这包括恢复终端输出以及构建产物(例如dist文件夹中的 JS 文件)。

并非所有操作都可缓存,只有无副作用的操作才可缓存。例如,如果使用相同的输入运行一个操作,它必须始终可靠地产生相同的输出。如果在该操作中调用了某个 API,那么该操作就无法缓存,因为即使输入参数相同,该 API 的结果也可能不同。

为了启用缓存,我们需要配置可缓存的操作。为此,我们nx.json在工作区的根目录创建一个包含以下内容的文件。

// nx.json
{
  "tasksRunnerOptions": {
    "default": {
      "runner": "nx/tasks-runners/default",
      "options": {
        "cacheableOperations": ["build", "test"]
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

注意cacheableOperations我们指定 ` buildand` 和 `.`的数组test。您可以添加更多内容,例如代码检查。

启用此功能后,如果我们第一次运行 Remix 应用构建,它将像往常一样执行,我们会看到它大约需要 1 秒。

❯ npx nx build my-remix-app

> nx run my-remix-app:build


> my-remix-app@ build /Users/juri/nrwl/content/pnpm-demos/pnpm-mono/apps/my-remix-app
> remix build

Building Remix app in production mode...
The path "shared-ui" is imported in app/routes/index.tsx but shared-ui is not listed in your package.json dependencies. Did you forget to install it?
Built in 163ms


 >  NX   Successfully ran target build for project my-remix-app (1s)
Enter fullscreen mode Exit fullscreen mode

如果重新运行相同的命令,它将从缓存中取出,只需几毫秒即可完成。

❯ npx nx build my-remix-app

> nx run my-remix-app:build  [existing outputs match the cache, left as is]


> my-remix-app@ build /Users/juri/nrwl/content/pnpm-demos/pnpm-mono/apps/my-remix-app
> remix build

Building Remix app in production mode...
The path "shared-ui" is imported in app/routes/index.tsx but shared-ui is not listed in your package.json dependencies. Did you forget to install it?
Built in 163ms


 >  NX   Successfully ran target build for project my-remix-app (9ms)

Nx read the output from the cache instead of running the command for 1 out of 1 tasks.
Enter fullscreen mode Exit fullscreen mode

您还可以从终端输出中看到“现有输出与缓存匹配,保持不变”,以及在结尾处“Nx 从缓存中读取输出,而不是对 1 个任务运行命令”。

启用缓存可以显著提升命令执行速度。如果缓存能够远程分布,以便与持续集成 (CI) 系统和其他开发人员的机器共享,其优势将更加明显。对于 Nx 而言,可以通过启用Nx Cloud 来实现这一点,Nx Cloud提供每月 500 小时的免费缓存时长(无需信用卡),开源项目则可享受无限时长。

优化缓存

默认情况下,缓存机制会将所有项目级文件作为输入build。但是,我们可能需要根据执行的目标来区分哪些文件会被纳入考虑。例如:如果只有单元测试的规范文件发生了更改,您可能不希望使该任务的缓存失效。

为了说明这一点,请运行npx nx build my-remix-app两次命令,以便激活缓存。接下来,更改README.mdRemix 项目的 README 文件(apps/my-remix-app/README.md)。如果您重新运行 Remix 应用构建,由于 README 文件的更改,缓存将失效。这显然不是一个理想的操作。

targetDefaults我们可以通过添加一个节点来微调缓存,并定义目标nx.json默认值应排除文件。inputbuild*.md

{
  "tasksRunnerOptions": {
    "default": {
      "runner": "nx/tasks-runners/default",
      "options": {
        "cacheableOperations": ["build", "test"]
      }
    }
  },
  "targetDefaults": {
    "build": {
      "inputs": ["!{projectRoot}/**/*.md"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

通过这一更改,在运行任务时,MD 文件将不再被视为缓存输入的一部分build

请注意,所有路径通配符均相对于工作区的根目录。这样可以避免混淆,因为输入也可以在项目级别定义package.json更多信息请参见此处)。您可以使用插值变量{projectRoot},并{workspaceRoot}区分路径指向的是项目特定文件还是工作区文件。

重用缓存输入通配符

你还可以更进一步,将此通配符用​​于排除其他目标平台的 Markdown 文件test。你可以通过将通配符提取到一个namedInputs属性中来实现这一点:

{
  "tasksRunnerOptions": {
      ...
  },
  "namedInputs": {
    "noMarkdown": ["!{projectRoot}/**/*.md"]
  },
  "targetDefaults": {
    "build": {
      "inputs": ["noMarkdown", "^noMarkdown"]
    },
    "test": {
      "inputs": ["noMarkdown", "^noMarkdown"]
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

^在前面加上,namedInput我们表示这也适用于项目的任何依赖项的更改。

定义任务依赖关系(又称构建管道)

我们之前已经发现,当我们运行 Remix 开发服务器,但没有shared-ui先编译依赖包时,运行 Remix 应用程序时会出错。

Error: Cannot find module '/Users/juri/nrwl/content/pnpm-demos/pnpm-mono/apps/my-remix-app/node_modules/shared-ui/dist/index.js'. Please verify that the package.json has a valid "main" entry  
    at tryPackage (node:internal/modules/cjs/loader:353:19)  
    at Function.Module._findPath (node:internal/modules/cjs/loader:566:18)  
    at Function.Module._resolveFilename (node:internal/modules/cjs/loader:919:27)  
    at Function.Module._load (node:internal/modules/cjs/loader:778:27)  
    at Module.require (node:internal/modules/cjs/loader:1005:19)  
    at require (node:internal/modules/cjs/helpers:102:18)  
    at Object.<anonymous> (/Users/juri/nrwl/content/pnpm-demos/pnpm-mono/apps/my-remix-app/app/routes/index.tsx:1:24)  
    at Module._compile (node:internal/modules/cjs/loader:1105:14)  
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1159:10)  
    at Module.load (node:internal/modules/cjs/loader:981:32)
Enter fullscreen mode Exit fullscreen mode

为了解决这个问题,我们不得不shared-ui先手动构建。通常情况下,你会想要避免这种情况,而这正是 Nx 附带targetDefaults定义(通常也称为“构建管道”)的原因。

我们可以在nx.json工作区根目录的targetDefaults属性中定义此类任务依赖关系。

首先,我们需要定义一个依赖关系:当build在某个项目上运行目标时,build其所有依赖项目的目标都应该首先执行。我们可以通过dependsOn在任务定义中添加一个额外的属性来表达这一点build

{
  "tasksRunnerOptions": {
      ...
  },
  ...
  "targetDefaults": {
    "build": {
      ...
      "dependsOn": ["^build"]
    }
  },
}
Enter fullscreen mode Exit fullscreen mode

与我们在定义中看到的类似inputs^这里的 `--target` 表示目标应该在所有依赖项目上运行。如果移除 `--target` ,则目标将在同一项目上运行。如果您有一个始终需要执行的步骤,^这将非常有用。prebuild

接下来,我们还想为 Remixdev命令定义一个 targetDefault,以便首先build对所有依赖包(例如我们的shared-ui)运行。

{
  "tasksRunnerOptions": {
    ...
  },
  ...
  "targetDefaults": {
    "build": {
      ...
      "dependsOn": ["^build"]
    },
    "dev": {
      "dependsOn": ["^build"]
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

以下是整个nx.json文件,供您参考:

{
  "tasksRunnerOptions": {
    "default": {
      "runner": "nx/tasks-runners/default",
      "options": {
        "cacheableOperations": ["build", "test"]
      }
    }
  },
  "namedInputs": {
    "noMarkdown": ["!{projectRoot}/**/*.md"]
  },
  "targetDefaults": {
    "build": {
      "inputs": ["noMarkdown", "^noMarkdown"],
      "dependsOn": ["^build"]
    },
    "dev": {
      "dependsOn": ["^build"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

如果我们现在运行,npx nx build my-remix-app可以看到 Nx 首先在依赖项目上运行任务,然后才运行我们调用的命令。

任务依赖关系

运行结果究竟发生了哪些变化

除了提供缓存之外,Nx 还允许使用所谓的“受影响的命令”来运行给定分支相对于基础分支的更改

❯ npx nx affected:<target>
Enter fullscreen mode Exit fullscreen mode

您可以使用工作区中定义的任何目标。例如

  • npx nx affected:build
  • npx nx affected:test
  • npx nx affected:lint
  • npx nx affected:publish

这是如何实现的? Nx 会根据你的 monorepo 工作区中包的结构和依赖关系构建项目图。假设有以下示例图:

潜在项目图表可视化

每当我们在分支上运行受影响的命令时,Nx 都会将所有提交及其相对更改与基础分支进行比较。默认情况下,此操作是默认的main,但您可以在文件中进行微调nx.json

{
    "affected": {
        "defaultBase": "main"
    }
}
Enter fullscreen mode Exit fullscreen mode

如果lib2在我们的特性分支中发生了更改,使用针对工作区运行测试affected:test将只会运行针对lib2和的测试appB

受影响的项目已重点介绍

但请注意,如果我们运行命令affected:build,并且在命令中定义了依赖项nx.json,表明依赖项目也需要先构建(参见“定义任务依赖项”部分),那么affected:build将会构建

  • lib3
  • lib2
  • appB

它不会建造lib1,或者appA尽管如此。

附加功能

除了速度和任务调度方面的改进之外,将 Nx 添加到 PNPM 工作区还能带来一些额外的功能。让我们一起来了解一下:

想要实现软件包创建的自动化?

一旦你为一个软件包配置好了,显然在创建新软件包时,你也希望复制这种配置。通常的做法是:复制粘贴,然后删除所有不需要的内容。

这既繁琐又容易出错。Nx 有一个“生成器”的概念,本质上就是代码脚手架,它允许你在单体仓库中生成新的包,而不是复制粘贴旧包。

如果这听起来很有趣,以下是详细步骤:

动态终端输出

使用 PNPM 并行运行任务会导致终端输出非常混乱。由于并行执行的不同命令的消息交错出现,日志难以解析。

❯ pnpm run --parallel -r build

Scope: 2 of 3 workspace projects
apps/my-remix-app build$ remix build
packages/shared-ui build$ rm -rf dist && tsc
apps/my-remix-app build: Building Remix app in production mode...
apps/my-remix-app build: The path "shared-ui" is imported in app/routes/index.tsx but shared-ui is not listed in your package.json dependencies. Did you forget to install it?
apps/my-remix-app build: Built in 176ms
apps/my-remix-app build: Done
packages/shared-ui build: Done
Enter fullscreen mode Exit fullscreen mode

使用 Nx 运行任务时,你会看到一个动态终端,它只会显示与当前执行的命令最相关且必要的信息。使用 Nx 运行同一个并行构建任务会产生以下输出:

启用 Nx 时的终端输出

项目图表可视化

❯ npx nx graph
Enter fullscreen mode Exit fullscreen mode

这将启动工作区项目图的交互式可视化,并提供一些高级筛选、调试工作区结构等功能。

由 Nx 提供支持的项目图表可视化

补充说明:即使没有安装 Nx,你也可以在任何 PNPM 工作区上运行项目图。运行npx nx graph应该没问题。

结论

我们成功了!以下是我们涵盖的部分内容:

  • 如何搭建基于 PNPM 的单体仓库工作区
  • 在 PNPM 单体仓库中创建 Remix 和共享 React 库
  • 如何使用PNPM运行不同的命令
  • 如何在单体仓库中添加 Nx 并逐步采用它
  • 将 Nx 添加到 PNPM 工作区所带来的优势和功能

您可以在 Nx Recipe GitHub 仓库中找到此类设置的示例:
https://github.com/nrwl/nx-recipes/tree/main/pnpm-workspace


了解更多

另外,如果你喜欢这篇文章,请点击❤️,并务必在Twitter上关注JuriNx,获取更多内容!

# nx

文章来源:https://dev.to/nx/setup-a-monorepo-with-pnpm-workspaces-and-speed-it-up-with-nx-1eem