block65/vite-plugin-vinext-payload
GitHub: block65/vite-plugin-vinext-payload
该插件填补 Payload CMS 与 vinext/Cloudflare Workers 之间的运行时与构建兼容性差距,使 Payload CMS 能在边缘环境无修补运行。
Stars: 2 | Forks: 0
# vite-plugin-vinext-payload
一个让 [Payload CMS](https://payloadcms.com/) 运行在 [vinext](https://github.com/cloudflare/vinext) 下的 Vite 插件,支持 Cloudflare Workers。
## 快速开始
```
npm install -D vite-plugin-vinext-payload
```
```
// vite.config.ts
import { defineConfig } from "vite";
import vinext from "vinext";
import vinextPayload from "vite-plugin-vinext-payload";
export default defineConfig({
plugins: [vinext(), vinextPayload()],
});
```
```
npm run dev
```
就是这样。对于带有 RSC 的 Cloudflare Workers,请添加 Cloudflare 插件:
```
import { cloudflare } from "@cloudflare/vite-plugin";
import vinext from "vinext";
import { defineConfig } from "vite";
import vinextPayload from "vite-plugin-vinext-payload";
export default defineConfig({
plugins: [
cloudflare({
viteEnvironment: { name: "rsc", childEnvironments: ["ssr"] },
}),
vinext(),
vinextPayload(),
],
});
```
`cloudflare:workers` 会自动被外部化——无需通过 `ssrExternal` 传递。
有关 Cloudflare D1 项目,请参阅 **[Cloudflare D1 指南](docs/cloudflare-d1.md)**。
## 从 Next.js 迁移
已经有一个基于 Next.js 的 Payload CMS 项目?`init` 命令可以将其转换:
```
npm install -D vinext vite # Install vinext
npx vinext init # Convert Next.js → vinext
npm install -D vite-plugin-vinext-payload
npx vite-plugin-vinext-payload init # Apply Payload-specific fixes
npm run dev
```
`init` 是幂等的——多次运行是安全的。使用 `--dry-run` 来预览更改。它会:
- 将 `vinextPayload()` 添加到项目的 `vite.config.ts` 中
- 将 `layout.tsx` 中的内联 server function 提取到一个单独的 `'use server'` 模块中(这是 Vite 的 RSC 转换所必需的)
- 将 `normalizeParams` 添加到管理页面
- 如果存在 `wrangler.{jsonc,json,toml}`,还会将 `cloudflare()` 添加到 `vite.config.ts` 中,并将 `@cloudflare/vite-plugin` 添加到 `devDependencies` 中
## 两种模式
- **`vinextPayload()`** —— 完整的 Payload(管理 UI + REST/GraphQL)与基于 Vite 的 Next.js 重新实现版本 [vinext](https://github.com/cloudflare/vinext) 结合使用。这就是上面快速开始所使用的方式。
- **`vinextPayloadWorker()`** —— 无头 Payload 仅通过 `WorkerEntrypoint` RPC 暴露其 [Local API](https://payloadcms.com/docs/local-api/overview),没有管理 UI。可与任何基于 Vite 的前端框架(TanStack Start、SvelteKit、Remix、Nuxt)配对,作为父 Worker 运行。
### 无头 RPC Worker(无管理 UI)
将 Payload 作为一个独立的 Cloudflare 辅助 Worker 运行,通过 `WorkerEntrypoint` RPC 暴露其 Local API。父 Worker 通过 service binding 与其通信——无需 HTTP,无需管理 UI,无需 vinext。
```
// services/website/vite.config.ts (parent worker)
import { cloudflare } from "@cloudflare/vite-plugin";
import { vinextPayloadWorker } from "vite-plugin-vinext-payload";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
// ...the framework plugin (tanstackStart, sveltekit, etc.)
cloudflare({
viteEnvironment: { name: "ssr" },
auxiliaryWorkers: [
{
configPath: "../payload-cms/wrangler.jsonc",
config: { main: "../payload-cms/src/rpc-only.ts" },
},
],
}),
// `env` is the auxiliary worker's vite env name (the cloudflare
// plugin normalizes the worker's `name` from wrangler.jsonc:
// "payload-cms" → "payload_cms"). The `[vite] (...)` prefix in the
// dev log confirms it.
...vinextPayloadWorker({ env: "payload_cms" }),
],
});
```
```
// services/payload-cms/src/rpc-only.ts
import { WorkerEntrypoint } from "cloudflare:workers";
import { getPayload } from "payload";
import config from "./payload.config";
export class CmsEntrypoint extends WorkerEntrypoint {
async find(
args: Parameters>["find"]>[0],
) {
const payload = await getPayload({ config });
return payload.find(args);
}
// Expose whatever Local API surface the parent worker needs.
}
// Required so the worker module satisfies wrangler's `fetch` shape, but
// the parent calls this worker over the service binding, not via HTTP.
export default {
fetch: () => new Response("rpc-only", { status: 404 }),
};
```
然后在父级的 `wrangler.jsonc` 中,添加一个指向 `CmsEntrypoint` 的 service binding,并从父级的 loader / API 路由 / server function 中调用其方法。有关 binding 的结构,请参阅 Cloudflare 的 [WorkerEntrypoint 文档](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/)。
`vinextPayloadWorker` 组合了 `vinextPayload` 相同子插件的子集(workerd polyfill、server 外部依赖、optimize-deps 排除项、file-type / drizzle-kit/api 桩、CJS 互操作、CLI 桩)——这是让 Payload 的 Local API 在 workerd 内部执行所需的一切,但不包含任何针对管理 UI / RSC 的修复。
## 选项
```
// Full Payload + vinext
vinextPayload({
// Additional packages to externalize from SSR bundling
ssrExternal: ["some-native-package"],
// Additional packages to exclude from optimizeDeps
excludeFromOptimize: ["some-broken-package"],
// Additional CJS packages needing default export interop
cjsInteropDeps: ["some-cjs-dep"],
});
// Headless Payload-as-auxiliary-worker
vinextPayloadWorker({
// Required — the vite env name of the auxiliary worker (cloudflare
// plugin normalizes the wrangler `name` to a JS identifier).
env: "payload_cms",
// Optional — same shape as vinextPayload
ssrExternal: ["..."],
excludeFromOptimize: ["..."],
cjsInteropDeps: ["..."],
});
```
## 环境要求
- Node.js `>=24`
- Vite `^8.0.0`
- Payload CMS `^3.82.0`
- vinext `1.0.0-beta.2`(必须确切匹配 —— vinext 仍处于预发布阶段;每次版本更新都可能破坏兼容性)。可选 —— 仅在使用 `vinextPayload()` 时需要。`vinextPayloadWorker()` 不需要。
## 它的作用
Payload CMS 主要面向 Next.js;vinext 在 Vite 和 Cloudflare Workers 上重新实现了 Next.js 的框架层。两者之间的差距——RSC 预打包、workerd 的运行时接口、Rolldown 的输出格式、CJS 互操作——正是此插件所填补的。它应用了一系列变通方案,使得管理 UI、REST/GraphQL API、server action 和上传功能无需手动修补即可正常工作。
要使用此插件,不需要了解这些单独的修复;它们在 [`docs/internals.md`](docs/internals.md) 中列出,相关的底层 Bug 记录在 [`docs/upstream-bugs.md`](docs/upstream-bugs.md) 中。
### 构建时补丁
此插件在构建时会重写其他包的代码。每一次此类重写都作为数据声明([`src/main.ts`](src/main.ts) 中的 `PATCH_MANIFEST`);下表是由这些声明生成的,并且如果出现偏差,单元测试将会失败。
| 补丁 | 类型 | 重写内容 | 原因 | 移除条件 |
| ------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
| `use-client-barrel` | config | @payloadcms/* — 重新导出 'use client' 模块的子路径 barrel,从 rsc optimizeDeps 中排除 | 预打包会将 barrel 及其重新导出的模块合并,并剥离 'use client',导致 plugin-rsc 在服务器端执行客户端组件 | @vitejs/plugin-rsc 遵循重导出链来检测 'use client' 指令 |
| `server-externals` | config | esbuild, wrangler, miniflare, sharp — 从 server bundle 中外部化
cloudflare:workers — 在所有地方进行外部化 | 构建/部署工具和原生插件无法被 bundle,并且客户端环境不能尝试 bundle 仅限 workerd 的 cloudflare:workers 标识符 | 永不——构建工具和原生插件保持外部化 | | `workerd-console-createtask` | transform | react — 任何调用 console.createTask 的模块(开发模式) | workerd 的 node:console 定义了 console.createTask,但在调用时会抛出 'not implemented';React 19 开发模式为了异步堆栈跟踪会调用它 | workerd 将 console.createTask 设为空操作而不是抛出异常 | | `workerd-undici-feature-detect` | transform | undici — runtime-features 检测 | Rolldown 将 undici 的惰性 require('node:*') 转换为返回 void 的 ESM 初始化程序,导致其特征探测从 undefined 读取属性并抛出异常,而不是返回 false | Rolldown 为外部化的 node 内置模块保留 require() 语义,或者 undici 对其探测进行了保护 | | `workerd-import-meta-url-guard` | transform | 任何调用 fileURLToPath(import.meta.url) 或 createRequire(import.meta.url) 的服务器环境模块 | workerd 中 bundle 的资产模块可能会看到 import.meta.url 为 undefined,导致模块初始化崩溃 | workerd 为 bundle 的模块提供 import.meta.url | | `workerd-node-builtin-shims` | stub | workerd 环境中的 node:* 导入 → unenv/node/* | workerd 不提供 Node 内置模块;unenv 的 shim 保持传递依赖可加载 | workerd 的 Node 兼容性涵盖了 Payload 引入的内置模块 | | `workerd-entry` | transform | vinext — 构建的 rsc entry chunk 的默认导出 | 在 Vite 8/Rolldown 上,vinext 的 app-router-entry 的 { fetch } 包装器可能会被内联为裸函数,而默认导出没有 fetch 方法的 Worker 将无法启动
目前处于防御性状态:预期不会重写任何内容。
https://github.com/cloudflare/workers-sdk/issues/10213
https://github.com/cloudflare/workers-sdk/pull/10544
https://github.com/rolldown/rolldown/issues/3500
https://github.com/rolldown/rolldown/issues/6449 | Rolldown 对此内联情况强制执行 preserveEntrySignatures: 'strict' | | `html-diff-export-fix` | file-write | @payloadcms/ui — dist/exports/rsc/index.js,在磁盘上重写 | 在 vinext/Rolldown 构建中,HTMLDiff 的重新导出解析为一个缺少 getHTMLDiffComponents 的模块,导致 version-diff 视图崩溃;该导出被替换为本地实现 | @payloadcms/ui 对 getHTMLDiffComponents 的 rsc 导出在 Rolldown 下能正确解析 | | `optimize-deps` | config | file-type, blake3-wasm, wrangler, @payloadcms/next — 在所有环境中从 optimizeDeps 中排除
payload > ajv, payload > bson-objectid, react/compiler-runtime, @payloadcms/ui 及发现的 next/* 别名 — 强制包含在客户端 optimizeDeps 中
payload, @payloadcms/richtext-lexical 和 @payloadcms/ui 的传递依赖 — 强制包含在 rsc optimizeDeps 中 | 这些包在 esbuild/Rolldown 预打包期间由于 webpack 原生处理的结构原因而崩溃,被排除的父级失去了对其子级的 CJS 自动发现,并且留给运行时发现的依赖在冷启动时都会触发串行重新优化 + module-runner 重载;每个条目的详细说明位于 payload-packages.ts 中
https://github.com/cloudflare/vinext/issues/538 | payload-packages.ts 中的各条目条件——列表会逐个条目地缩减 | | `cjs-transform` | transform | 除 react, react-dom, react-server-dom-webpack 和 scheduler 之外的任何 node_modules CJS/UMD 文件 | 通过 /@fs/ 原始提供的文件在浏览器(module.exports)和 Vite 的严格 ESM module runner 中会崩溃,因为在其中 UMD 包装器和 TS CJS 辅助函数的模块级 `this` 是 undefined | Vite 为在 optimizeDeps 预打包之外提供的文件将 CJS 转换为 ESM | | `cli-stubs` | stub | console-table-printer, json-schema-to-typescript, esbuild-register, ws, wrangler, pnpapi → 空操作桩 | 这些仅被 Payload CLI 命令或 Next 特定的代码路径触达,并且打包它们会将损坏的依赖项拖入图中(wrangler 的 CLI 引入了 Rolldown 无法解析的 blake3-wasm);ws 和 wrangler 在使用时会抛出异常,因此真正的 Node 端调用会保持明显报错 | payload 在动态导入后惰性加载其仅限 CLI 的依赖项 | | `nav-component-fix` | transform | @payloadcms/next — Nav/index.client.js (DefaultNavClient)
@payloadcms/next — DocumentHeader Tabs TabLink.js (DocumentTabLink) | vinext 的 usePathname()/useParams() 在 SSR 和客户端 hydrate 时有所不同,因此这些组件会渲染不同的元素类型,而 React 19 会丢弃服务器树,导致表单状态丢失 | vinext 的 navigation hook 像 Next.js 一样使用 React context,或者 Payload 移除了条件元素类型的渲染 | | `rsc-export-fix` | transform | @vitejs/plugin-rsc — 其 CSS 导出转换的输出,仅限 rsc 环境 | plugin-rsc 使用 MagicString 将 export 语句重定位到文件末尾;当源代码以没有尾随换行符的 sourcemap 注释结尾时,export 会落在注释内,导致 Rolldown 无法看到它
https://github.com/vitejs/vite-plugin-react (plugin-rsc) | plugin-rsc 的 transformWrapExport 在重定位的导出之前发出换行符 | | `rsc-runtime-stubs` | stub | file-type → 空操作桩(服务器环境)
drizzle-kit/api → 空操作桩,包括内联的 createRequire() 调用 | 两者都是在 RSC 渲染期间被传递引用但从未被调用的,并留下了 workerd module runner 无法解析的裸导入;各条目的详细说明位于 payload-packages.ts (RSC_STUBS) 中 | workerd 支持它们所需的 Node API,或者 payload 惰性加载它们 | | `rsc-serializer-throws` | transform | react-server-dom-webpack — 'Client Component' 序列化程序抛出异常 | RSC 序列化程序对于无法跨越服务器/客户端边界(Payload 字段配置中的访问函数、hooks、RegExps)的值会抛出异常;Next.js 在生产环境中会静默丢弃它们,而 vinext 不会,因此每个 Payload 页面都会失败 | vinext 的 RSC 流水线像 Next.js 一样不可序列化的配置值 | | `server-action-fix` | transform | vinext — server/app-browser-entry.js
vinext — server/app-browser-navigation-controller.js | vinext 在返回其数据之前应用了返回数据的 server action 的 RSC 树,重置了 Payload 表单状态并导致 getFormState 循环;其浏览器条目还通过相对路径导入了 navigation shim,绕过了预打包的 next/navigation 别名,并迫使运行时重新优化 | vinext 在其浏览器条目中使用别名的 next/navigation,并跳过对返回数据的 server action 的可见提交 | | `cjs-default-interop` | transform | pluralize, bson-objectid — 通过 vite-plugin-cjs-interop | 它们的 CJS 默认导出在没有互操作包装的情况下以 { default: fn } 的形式到达,因此像 pluralize('item') 这样的调用会在运行时中断 | 这些包提供原生 ESM | ## 许可证 MIT
cloudflare:workers — 在所有地方进行外部化 | 构建/部署工具和原生插件无法被 bundle,并且客户端环境不能尝试 bundle 仅限 workerd 的 cloudflare:workers 标识符 | 永不——构建工具和原生插件保持外部化 | | `workerd-console-createtask` | transform | react — 任何调用 console.createTask 的模块(开发模式) | workerd 的 node:console 定义了 console.createTask,但在调用时会抛出 'not implemented';React 19 开发模式为了异步堆栈跟踪会调用它 | workerd 将 console.createTask 设为空操作而不是抛出异常 | | `workerd-undici-feature-detect` | transform | undici — runtime-features 检测 | Rolldown 将 undici 的惰性 require('node:*') 转换为返回 void 的 ESM 初始化程序,导致其特征探测从 undefined 读取属性并抛出异常,而不是返回 false | Rolldown 为外部化的 node 内置模块保留 require() 语义,或者 undici 对其探测进行了保护 | | `workerd-import-meta-url-guard` | transform | 任何调用 fileURLToPath(import.meta.url) 或 createRequire(import.meta.url) 的服务器环境模块 | workerd 中 bundle 的资产模块可能会看到 import.meta.url 为 undefined,导致模块初始化崩溃 | workerd 为 bundle 的模块提供 import.meta.url | | `workerd-node-builtin-shims` | stub | workerd 环境中的 node:* 导入 → unenv/node/* | workerd 不提供 Node 内置模块;unenv 的 shim 保持传递依赖可加载 | workerd 的 Node 兼容性涵盖了 Payload 引入的内置模块 | | `workerd-entry` | transform | vinext — 构建的 rsc entry chunk 的默认导出 | 在 Vite 8/Rolldown 上,vinext 的 app-router-entry 的 { fetch } 包装器可能会被内联为裸函数,而默认导出没有 fetch 方法的 Worker 将无法启动
目前处于防御性状态:预期不会重写任何内容。
https://github.com/cloudflare/workers-sdk/issues/10213
https://github.com/cloudflare/workers-sdk/pull/10544
https://github.com/rolldown/rolldown/issues/3500
https://github.com/rolldown/rolldown/issues/6449 | Rolldown 对此内联情况强制执行 preserveEntrySignatures: 'strict' | | `html-diff-export-fix` | file-write | @payloadcms/ui — dist/exports/rsc/index.js,在磁盘上重写 | 在 vinext/Rolldown 构建中,HTMLDiff 的重新导出解析为一个缺少 getHTMLDiffComponents 的模块,导致 version-diff 视图崩溃;该导出被替换为本地实现 | @payloadcms/ui 对 getHTMLDiffComponents 的 rsc 导出在 Rolldown 下能正确解析 | | `optimize-deps` | config | file-type, blake3-wasm, wrangler, @payloadcms/next — 在所有环境中从 optimizeDeps 中排除
payload > ajv, payload > bson-objectid, react/compiler-runtime, @payloadcms/ui 及发现的 next/* 别名 — 强制包含在客户端 optimizeDeps 中
payload, @payloadcms/richtext-lexical 和 @payloadcms/ui 的传递依赖 — 强制包含在 rsc optimizeDeps 中 | 这些包在 esbuild/Rolldown 预打包期间由于 webpack 原生处理的结构原因而崩溃,被排除的父级失去了对其子级的 CJS 自动发现,并且留给运行时发现的依赖在冷启动时都会触发串行重新优化 + module-runner 重载;每个条目的详细说明位于 payload-packages.ts 中
https://github.com/cloudflare/vinext/issues/538 | payload-packages.ts 中的各条目条件——列表会逐个条目地缩减 | | `cjs-transform` | transform | 除 react, react-dom, react-server-dom-webpack 和 scheduler 之外的任何 node_modules CJS/UMD 文件 | 通过 /@fs/ 原始提供的文件在浏览器(module.exports)和 Vite 的严格 ESM module runner 中会崩溃,因为在其中 UMD 包装器和 TS CJS 辅助函数的模块级 `this` 是 undefined | Vite 为在 optimizeDeps 预打包之外提供的文件将 CJS 转换为 ESM | | `cli-stubs` | stub | console-table-printer, json-schema-to-typescript, esbuild-register, ws, wrangler, pnpapi → 空操作桩 | 这些仅被 Payload CLI 命令或 Next 特定的代码路径触达,并且打包它们会将损坏的依赖项拖入图中(wrangler 的 CLI 引入了 Rolldown 无法解析的 blake3-wasm);ws 和 wrangler 在使用时会抛出异常,因此真正的 Node 端调用会保持明显报错 | payload 在动态导入后惰性加载其仅限 CLI 的依赖项 | | `nav-component-fix` | transform | @payloadcms/next — Nav/index.client.js (DefaultNavClient)
@payloadcms/next — DocumentHeader Tabs TabLink.js (DocumentTabLink) | vinext 的 usePathname()/useParams() 在 SSR 和客户端 hydrate 时有所不同,因此这些组件会渲染不同的元素类型,而 React 19 会丢弃服务器树,导致表单状态丢失 | vinext 的 navigation hook 像 Next.js 一样使用 React context,或者 Payload 移除了条件元素类型的渲染 | | `rsc-export-fix` | transform | @vitejs/plugin-rsc — 其 CSS 导出转换的输出,仅限 rsc 环境 | plugin-rsc 使用 MagicString 将 export 语句重定位到文件末尾;当源代码以没有尾随换行符的 sourcemap 注释结尾时,export 会落在注释内,导致 Rolldown 无法看到它
https://github.com/vitejs/vite-plugin-react (plugin-rsc) | plugin-rsc 的 transformWrapExport 在重定位的导出之前发出换行符 | | `rsc-runtime-stubs` | stub | file-type → 空操作桩(服务器环境)
drizzle-kit/api → 空操作桩,包括内联的 createRequire() 调用 | 两者都是在 RSC 渲染期间被传递引用但从未被调用的,并留下了 workerd module runner 无法解析的裸导入;各条目的详细说明位于 payload-packages.ts (RSC_STUBS) 中 | workerd 支持它们所需的 Node API,或者 payload 惰性加载它们 | | `rsc-serializer-throws` | transform | react-server-dom-webpack — 'Client Component' 序列化程序抛出异常 | RSC 序列化程序对于无法跨越服务器/客户端边界(Payload 字段配置中的访问函数、hooks、RegExps)的值会抛出异常;Next.js 在生产环境中会静默丢弃它们,而 vinext 不会,因此每个 Payload 页面都会失败 | vinext 的 RSC 流水线像 Next.js 一样不可序列化的配置值 | | `server-action-fix` | transform | vinext — server/app-browser-entry.js
vinext — server/app-browser-navigation-controller.js | vinext 在返回其数据之前应用了返回数据的 server action 的 RSC 树,重置了 Payload 表单状态并导致 getFormState 循环;其浏览器条目还通过相对路径导入了 navigation shim,绕过了预打包的 next/navigation 别名,并迫使运行时重新优化 | vinext 在其浏览器条目中使用别名的 next/navigation,并跳过对返回数据的 server action 的可见提交 | | `cjs-default-interop` | transform | pluralize, bson-objectid — 通过 vite-plugin-cjs-interop | 它们的 CJS 默认导出在没有互操作包装的情况下以 { default: fn } 的形式到达,因此像 pluralize('item') 这样的调用会在运行时中断 | 这些包提供原生 ESM | ## 许可证 MIT
标签:Serverless, SOC Prime, Syscall, Vite 插件, Web开发, 内容管理系统, 前端工程化, 开发工具, 程序员工具, 自动化攻击