Nx 仓库 Dogfood 实战:examples/react/basic 独立 pnpm 工作区如何用 link: 依赖驱动本地插件端到端验证
2026/9/12 16:30:51 网站建设 项目流程

Nx 仓库 Dogfood 实战:examples/react/basic 独立 pnpm 工作区如何用 link: 依赖驱动本地插件端到端验证

【免费下载链接】nxThe Monorepo Platform that amplifies both developers and AI agents. Nx optimizes your builds, scales your CI, and fixes failed PRs automatically. Ship in half the time.项目地址: https://gitcode.com/GitHub_Trending/nx/nx

导读

本文以 Nx 仓库中的 examples/react/basic 为蓝本,完整拆解一个"独立 pnpm 工作区 + 本地插件link:依赖 + Nx target 推断"的 Dogfood(吃自己的狗粮)示例:它本身是一个由Vite 构建、Vitest 单测、Playwright E2E、ESLint/Oxlint 静态检查的 React 19 应用,同时通过link:../../../packages/*直接消费仓库内未发布的本地@nx/*插件构建产物。读完本文,你将掌握:link:依赖如何跨 pnpm workspace 边界生效、插件注册与 target 推断的完整链路、postinstall自动构建依赖包的机制,以及一个可用于任何 monorepo 的"本地插件回归验证"工程模板。


一、这个示例是什么:技术栈与目标

examples/react/basic是一个刻意保持"小而完整"的 React 应用,其核心目标并非演示业务功能,而是为 Nx 仓库自身的插件做持续回归验证(dogfooding)

  • Vite构建前端,产物输出到dist/
  • Vitest + jsdom + Testing Library做单元测试;
  • Playwright(Chromium)做端到端测试;
  • ESLint(配套@nx/eslint-plugin)做静态检查,同时本示例的 nx.json 还注册了@nx/oxlint用于 lint target 推断;
  • @nx/*依赖全部通过link:指向仓库根目录下的packages/*,因此target 推断与运行时辅助函数都来自本地构建而非已发布的 npm 包

页面本身非常简单:一个<h1>渲染"Hello there, Welcome examples-react-basic 👋",源码见 src/app/app.tsx。麻雀虽小,但它串起了 Nx 工作区中"本地开发插件 → 示例应用消费"的最短链路。

二、目录拓扑:一个被"隔离"的独立工作区

examples/react/basic不是仓库根工作区的一部分,而是一个自带完整 pnpm 工作区语义的独立目录

examples/react/basic/ ├── e2e/ # Playwright 端到端测试 │ └── app.spec.ts ├── src/ │ ├── app/ │ │ ├── app.spec.tsx # Vitest 单元测试 │ │ └── app.tsx # 应用组件 │ ├── main.tsx # React 入口 │ └── styles.css ├── index.html ├── nx.json # 本工作区的插件注册与 namedInputs ├── package.json # link: 依赖 + postinstall 构建链 ├── playwright.config.ts # 使用本地 @nx/playwright 的 nxE2EPreset ├── pnpm-lock.yaml # 独立的、已提交的锁文件 ├── pnpm-workspace.yaml # 独立 pnpm 工作区标记 ├── tsconfig*.json # 应用 / spec / base 三份 TS 配置 └── vite.config.mts # Vite + Vitest 一体化配置

pnpm-workspace.yaml 的注释直接说明了设计意图:

Marks this example as a standalone pnpm workspace: it gets its own pnpm-lock.yaml and install, separate from the repo root workspace. The local @nx/* packages are wired in via link: dependencies, which work across workspace boundaries.

即:

  • 该目录拥有自己提交的pnpm-lock.yaml,与仓库根工作区解耦,互不污染依赖图;
  • 它同时被排除在仓库根工作区之外,并从根 Nx graph 中被.nxignore忽略——根 Nx 不知道这个子项目的存在,避免两个 graph 相互干扰;
  • 本地@nx/*包通过link:依赖接入(详见下一节)。

三、link: 依赖:跨工作区边界的本地包接入

这是整个示例最关键的一环。package.json 的devDependencies中可以看到:

"devDependencies": { "@nx/eslint-plugin": "link:../../../packages/eslint-plugin", "@nx/js": "link:../../../packages/js", "@nx/oxlint": "link:../../../packages/oxlint", "@nx/playwright": "link:../../../packages/playwright", "@nx/react": "link:../../../packages/react", "@nx/vite": "link:../../../packages/vite", "nx": "link:../../../packages/nx", ... }

原理拆解:

  1. pnpm 的link:协议允许目录指向工作区之外——../../../packages/*从本目录出发正好落到仓库根下的packages/
  2. pnpm 安装时会为这些条目创建指向本地源码目录的符号链接,因此node_modules/@nx/vite实际就是packages/vite本身;
  3. 由于nx本体同样来自link:../../../packages/nx,本工作区执行的nx命令就是当前仓库的本地构建,而不是发布版;
  4. 本目录的 nx.json 注册插件后,插件解析从本工作区的node_modules出发,于是找到的是本地插件构建产物——target 推断与运行时辅助函数全部来自本地代码。

需要特别注意:编辑本地packages/*源码不会自动触发重建,因为本工作区的 Nx graph 看不到仓库根下的这些包。修改插件源码后,需要手动重跑 postinstall(见第五节)。

四、插件注册与 Target 推断

nx.json 是本工作区的 Nx 配置核心:

{ "$schema": "../../../node_modules/nx/schemas/nx-schema.json", "namedInputs": { "default": ["{projectRoot}/**/*", "sharedGlobals"], "production": [ "default", "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", "!{projectRoot}/tsconfig.spec.json", "!{projectRoot}/.oxlintrc.json" ], "sharedGlobals": [] }, "plugins": [ "@nx/vite/plugin", "@nx/vitest", "@nx/playwright/plugin", "@nx/oxlint" ], "analytics": false, "neverConnectToCloud": true }

plugins数组是 target 推断的源头。所有 target 都不是手写在project.json里的,而是插件基于项目文件结构动态推断出来的:

Target由哪个插件推断具体行为
build@nx/vite/plugin执行vite build,产物输出到dist/
serve@nx/vite/plugin启动 Vite dev server,端口 4301
preview@nx/vite/plugin预览生产构建产物
test@nx/vitest运行 Vitest 单元测试
e2e@nx/playwright/plugin运行 Playwright 端到端测试
lint@nx/eslint/plugin/@nx/oxlint对项目做静态检查

从仓库源码看,@nx/playwright/plugin的推断逻辑实现在 packages/playwright/src/plugins/plugin.ts(通过createNodesV2创建 e2e target),配套的 packages/playwright/src/plugins/plugin.spec.ts 用大量用例验证了 target 推断、命令行解析与 dependsOn 推导等行为——本示例正是这些推断逻辑的"活体测试"。

此外namedInputs中的production输入把*.spec/test文件、tsconfig.spec.json.oxlintrc.json排除在生产输入之外,保证缓存计算只依赖真正影响产物源码。

五、一条命令完成安装:postinstall 自动构建链

这是示例最精妙的设计——全新 clone 后只需pnpm install即可获得可运行环境。其机制是 package.json 中的 postinstall 钩子:

"scripts": { "postinstall": "cd ../../.. && pnpm nx run-many -t build -p nx js vite react playwright @nx/oxlint eslint-plugin", "validate": "nx run-many -t lint,test,build,e2e" }

执行顺序:

  1. pnpm install先完成依赖安装与link:符号链接创建;
  2. 随后 postinstall 回到仓库根目录(cd ../../..),用根工作区的 Nx 对本地包批量构建:nx run-many -t build -p nx js vite react playwright @nx/oxlint eslint-plugin
  3. 构建完成后,本工作区的node_modules符号链接立即指向新鲜构建的本地包

validate脚本则是全量验证入口:nx run-many -t lint,test,build,e2e一次性跑完所有阶段,适合 CI 或提交前自查。

日常使用时的常用命令(在本目录内执行):

pnpm install # 安装依赖,并触发本地插件构建 nx build # Vite 生产构建 → dist/ nx test # Vitest 单元测试 nx serve # 开发服务器,端口 4301 nx e2e # Playwright 端到端测试 nx lint # 静态检查

Playwright 首次运行前需要安装浏览器:npx playwright install chromium

六、关键配置文件逐项解读

6.1 vite.config.mts:Vite 与 Vitest 一体化

vite.config.mts 在同一份配置里同时承载构建与测试:

export default defineConfig(() => ({ root: import.meta.dirname, cacheDir: '../../../node_modules/.vite/examples/react/basic', server: { port: 4301, host: 'localhost', }, preview: { port: 4300, host: 'localhost', }, plugins: [react()], build: { outDir: './dist', emptyOutDir: true, reportCompressedSize: true, commonjsOptions: { transformMixedEsModules: true, }, }, test: { name: 'examples-react-basic', watch: false, globals: true, environment: 'jsdom', include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], reporters: ['default'], coverage: { reportsDirectory: './test-output/vitest/coverage', provider: 'v8' as const, }, }, }));

要点:

  • cacheDir指向仓库根node_modules/.vite,与根工作区共享缓存目录,但按示例名隔离子目录;
  • server.port: 4301对应 README 中 serve target 的端口;
  • test块即 Vitest 配置:jsdom环境 +globals: true+ 对src/tests*.{test,spec}.*的匹配规则,覆盖 v8 报告输出到test-output/vitest/coverage
  • 注意vite.config.mts顶部/// <reference types='vitest' />,使 Vitest 的test配置获得类型提示。

6.2 playwright.config.ts:nxE2EPreset 消费本地预设

playwright.config.ts 展示了如何用本地@nx/playwright提供的预设驱动 E2E:

import { defineConfig, devices } from '@playwright/test'; import { nxE2EPreset } from '@nx/playwright/preset'; import { workspaceRoot } from '@nx/devkit'; const baseURL = process.env['BASE_URL'] || 'http://localhost:4200'; export default defineConfig({ ...nxE2EPreset(__filename, { testDir: './e2e' }), use: { baseURL, trace: 'on-first-retry', }, webServer: { command: 'npx nx run examples-react-basic:serve', url: baseURL, reuseExistingServer: true, cwd: __dirname, }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, // 可取消注释以启用 Firefox / WebKit ], });

几个值得注意的实现细节:

  • nxE2EPreset定义在 packages/playwright/src/utils/preset.ts:它接收pathToConfig(本示例是手写的 CJS 风格.ts配置,因此传__filename),自动派生测试报告与结果输出目录;当在 CI 中(process.env.CI)且 Playwright 版本满足要求时,会自动追加blobreporter 用于产物合并;
  • webServer.command使用npx nx run examples-react-basic:serve而非直接vite:这样@nx/playwright/plugin可以从命令行推断出dependsOn(e2e 依赖 serve),Nx 会先启动开发服务器再跑测试;
  • BASE_URL语义:配置注释明确说明,回退值http://localhost:4200并非 serve 端口(serve 在 4301),测试只有配合 e2e task env 文件中设置的正确地址才会通过——这是示例在刻意演练"通过 Nx 注入环境变量"这一能力;
  • trace: 'on-first-retry'在测试失败重试时自动收集 trace,便于排障。

6.3 测试用例:单元与端到端的断言

单元测试 src/app/app.spec.tsx 基于 Testing Library + jsdom,验证组件渲染与欢迎文案:

describe('App', () => { it('should render successfully', () => { const { baseElement } = render(<App />); expect(baseElement).toBeTruthy(); }); it('should have a welcome message', () => { const { getByText } = render(<App />); expect(getByText(/Welcome examples-react-basic/i)).toBeTruthy(); }); });

E2E 测试 e2e/app.spec.ts 在真实浏览器中导航到/并断言<h1>内容包含 "Welcome":

test('has welcome heading', async ({ page }) => { await page.goto('/'); expect(await page.locator('h1').innerText()).toContain('Welcome'); });

七、修改本地插件后的重建注意点

由于本工作区的 Nx graph看不到仓库根的packages/*,编辑插件源码不会触发任何自动重构建。README 明确给出两条补救路径(二选一):

# 路径一:在本目录重跑 postinstall(会回到仓库根构建本地包) pnpm install # 路径二:直接在仓库根执行批量构建 pnpm nx run-many -t build -p vite react playwright eslint eslint-plugin

这一约束其实是 Dogfood 工程刻意保留的"真实现状"——它如实暴露了本地插件开发者在迭代时必经的构建环节,也让 CI 能够以最朴素的方式验证"源码变更 → 构建 → 示例消费"全链路。

八、小结:它为什么值得作为模板

examples/react/basic以约二十个文件,浓缩了 Nx 工作区的三个核心机制:

  1. 独立 pnpm 工作区 +link:依赖:用 pnpm 原生的跨工作区符号链接,把本地未发布的插件"伪装"成普通依赖,无需任何发布流程;
  2. 插件驱动的 target 推断:所有 target 来自nx.json中注册的插件(Vite/Vitest/Playwright/Oxlint),项目本身零project.json手写 target;
  3. postinstall 自动构建链:让"全新 clone → 一键可跑可测"成为可能,pnpm install一步打通。

对于任何维护自有 Nx 插件、或希望复用 Nx 插件机制的团队,这份示例都是一份高信噪比的参考实现:源码、配置与测试彼此印证,packages/playwright/src/plugins/plugin.ts、packages/playwright/src/plugins/plugin.spec.ts 与 packages/playwright/src/utils/preset.ts 分别展示了推断逻辑、验证用例与运行时预设的实际面貌,值得顺着本文的路径逐一细读。

【免费下载链接】nxThe Monorepo Platform that amplifies both developers and AI agents. Nx optimizes your builds, scales your CI, and fixes failed PRs automatically. Ship in half the time.项目地址: https://gitcode.com/GitHub_Trending/nx/nx

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询