# Internals — sub-plugins and what they work around This doc is for people hacking on the plugin (or on vinext / Payload compatibility). Using Payload on vinext requires none of it — the [README](../README.md) covers that. `vinextPayload()` composes a set of sub-plugins, each working around a specific upstream bug. They are not exported individually — the split exists purely for readability and maintenance. See [`upstream-bugs.md`](upstream-bugs.md) for the full write-up of each bug, including what Next.js does differently. ## Sub-plugins | Plugin | Owner bug | What it does | | -------------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `payloadUseClientBarrel` | Payload | Auto-detects `@payloadcms/*` barrel files that re-export from `'use client'` modules and excludes them from RSC pre-bundling (pre-bundling strips the directive, breaking client references) | | `payloadServerExternals` | Vite | Externalizes packages from both `ssr` and `rsc` environments. Only build tools and native addons are externalized (workerd can't resolve externals at runtime). Uses `configEnvironment` because `ssr.external` only applies to the `ssr` environment, and writes to `build.rolldownOptions.external` because `@cloudflare/vite-plugin` rejects `resolve.external` | | `payloadWorkerdCompat` | workerd / Rolldown | Four module-resolution / bundle-time fixes needed before code can evaluate inside workerd: (1) `resolveId` fallback for `node:*` CJS requires that bypass `@cloudflare/vite-plugin`'s filtered hook, (2) try-catch wrapper for undici's `detectRuntimeFeatureByExportedProperty` which crashes due to Rolldown's CJS→ESM interop returning void, (3) `import.meta.url ?? "file:///"` guards for `fileURLToPath` / `createRequire` patterns that crash in bundled workerd asset chunks, (4) `console.createTask` polyfill — where workerd exposes the method (e.g. when `node:console` is in the module graph) it throws "not implemented", breaking React 19 dev mode's async stack traces. Injected via both Vite transform and optimizeDeps rolldown plugin to cover pre-bundled deps | | `payloadWorkerdEntry` | Rolldown / @cloudflare/vite-plugin | `generateBundle` hook that re-wraps the RSC entry default export in `{ fetch }` if Rolldown inlines vinext's Workers handler wrapper into a bare function (regression of [workers-sdk#10213](https://github.com/cloudflare/workers-sdk/issues/10213) on Vite 8/Rolldown). Defensive on vinext 1.0.0-beta.2: a verified real build already emits a `{ fetch }` object there, and the injected wrapper passes non-functions through untouched | | `payloadHtmlDiffExportFix` | @vitejs/plugin-rsc / Rolldown | Patches `@payloadcms/ui/dist/exports/rsc/index.js` at build start to stabilize `getHTMLDiffComponents` export when RSC/Rolldown reports it as missing in Payload templates | | `payloadOptimizeDeps` | vinext | Per-environment optimizeDeps: excludes problematic packages, force-includes CJS transitive deps for the client. Auto-discovers all `next/*` alias specifiers from the resolved config so the optimizer doesn't discover them at runtime (which causes a full page reload) | | `payloadCjsTransform` | Vite | Fixes `this` → `globalThis` in UMD/CJS wrappers and wraps `module.exports` with ESM scaffolding (skips React/ReactDOM which Vite 8 handles natively) | | `payloadCliStubs` | Payload | Stubs packages not needed at web runtime (`console-table-printer`, `json-schema-to-typescript`, `esbuild-register`, `ws`, `wrangler`). The `wrangler` stub is what keeps payload.config's Node-only `getPlatformProxy` fallback from dragging wrangler's entire CLI — including the unresolvable `blake3-wasm` — into workerd bundles | | `payloadNavComponentFix` | Payload | Patches `DefaultNavClient` and `DocumentTabLink` to not switch element types (`` vs `
`) based on `usePathname()`/`useParams()` — prevents React 19 tree-destroying hydration mismatches (AST-based via ast-grep) | | `payloadRscExportFix` | @vitejs/plugin-rsc | Fixes `@vitejs/plugin-rsc`'s CSS export transform dropping exports after sourcemap comments | | `payloadRscRuntime` | vinext / workerd / pnpm | RSC environment patches: stubs `file-type` (a direct dep of `payload` itself) and `drizzle-kit/api` (via `@payloadcms/db-d1-sqlite`), and patches the RSC serializer to silently drop non-serializable values (functions, RegExps) at the server/client boundary. Next.js prod throws the same serialization error — dropping is a deliberate divergence that keeps Payload's admin form flows working | | `payloadServerActionFix` | vinext | Prevents data-returning server actions (like `getFormState`) from triggering a re-render that resets Payload's form state. Shape depends on vinext version: on ≤0.0.46, moves `getReactRoot().render()` after the `returnValue` check in `app-browser-entry`; on 0.0.47–0.0.55, gates the one-line visible-commit dispatch (`dispatchApprovedVisibleCommit` in 0.0.47–0.0.49, renamed to `dispatchSynchronousVisibleCommit` in 0.0.50) in `app-browser-navigation-controller` on `!returnValue`; on ≥0.1.0, where that dispatch moved into a block body, wraps the bare dispatch call as `if (!returnValue) …`. Also rewrites the browser entry's relative shim import to use the pre-bundled alias (AST-based via ast-grep) | | `cjsInterop` | Vite | Fixes CJS default export interop for packages like `bson-objectid` (via [vite-plugin-cjs-interop](https://github.com/cyco130/vite-plugin-cjs-interop)) | ## Known compatibility issues These all work fine on Next.js — they exist because vinext reimplements Next.js's framework layer on Vite. | Issue | Owner | Our workaround | | ----------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | Barrel exports missing `'use client'` directive | Payload | Auto-exclude affected packages from RSC optimizeDeps | | RSC export transform drops exports after sourcemap comments | @vitejs/plugin-rsc | Post-transform newline insertion | | `getHTMLDiffComponents` missing export in RSC build | @vitejs/plugin-rsc / Rolldown | Patch `@payloadcms/ui/dist/exports/rsc/index.js` at build start | | `console.createTask` throws "not implemented" | workerd | Try/catch polyfill | | `node:*` CJS requires bypass cloudflare plugin's resolveId filter | Rolldown / @cloudflare/vite-plugin | Filterless resolveId fallback routing to unenv polyfills | | undici `detectRuntimeFeatureByExportedProperty` crashes on void | Rolldown | Try-catch wrapper around detection function | | `import.meta.url` undefined in bundled workerd asset chunks | workerd | `?? "file:///"` fallback guard on `fileURLToPath` and `createRequire` | | `ssr.external` only applies to "ssr" environment, not RSC | Vite | Use `build.rolldownOptions.external` via `configEnvironment` for both ssr/rsc | | `file-type` / `drizzle-kit/api` unresolvable in workerd | pnpm + Vite | Stub modules for RSC | | payload.config's `getPlatformProxy` fallback drags wrangler (and unresolvable `blake3-wasm`) into workerd bundles | Payload template / Rolldown | Stub `wrangler` in Vite environments; the fallback only runs under plain Node (payload CLI) | | Browser entry imports shims via relative paths → optimizer reload + duplicate React | vinext | Rewrite import to aliased specifier; auto-include all `next/*` aliases in optimizeDeps | | `render()` called before `returnValue` check → form state reset | vinext | AST transform to reorder render after returnValue | | Components switch element types based on pathname/params → tree-destroying hydration mismatch | Payload | AST transform to force consistent element types | | Non-serializable values (functions, RegExps) throw at RSC boundary | vinext | Patch serializer throws to `return undefined` (Next.js prod also throws; we deliberately drop instead) | | Rolldown inlines Workers entry wrapper into bare function | Rolldown / @cloudflare/vite-plugin | `generateBundle` hook re-wraps default export in `{ fetch }` ([workers-sdk#10213](https://github.com/cloudflare/workers-sdk/issues/10213)) | | CJS default export interop (e.g. `bson-objectid`) breaks named-import desugaring | Vite | `vite-plugin-cjs-interop` for the curated package list | ## Drift detection The patches above match patterns in vinext's shipped `dist`. When vinext restructures, a pattern can stop matching and the patch silently becomes a no-op — the build still succeeds and unit tests (which use synthetic fixtures) still pass. `test/patch-targets.test.ts` reads the _real_ installed vinext and asserts each patch target still exists; a failure there means "re-verify this workaround against the new vinext".