feat: add TanStack Start framework adapter - #16139
Conversation
|
Hey, I am excited about this work! I had experimented with a similar approach a few months ago. I'm sure you might be aware that TanStack Start is planning to add RSC support soon (there is an draft blog post here, not sure how up to date it is with their current plans). I was wondering if this would make a difference in the approach to the framework adapter pattern, and if RSCs in Start make this significantly easier. If you haven't already, it might be good to have some communication with the TanStack team about this. Thanks for your work on this! |
I'm aware, currently I want to see if it is possible so we don't rely on whether a framework supports RSC or not (while still maintaining 100% the same approach in Next.js). This is a bit more complex indeed but would allow room for any React framework (or even a custom one on top of Vite), not just Next/Tanstack. In this case - when Tanstack will add RSC support and if we want to use it - the only place we'd have modify is the adapter itself. |
edd5952 to
22b4583
Compare
📦 esbuild Bundle Analysis for payloadThis analysis was generated by esbuild-bundle-analyzer. 🤖
Largest pathsThese visualization shows top 20 largest paths in the bundle.Meta file: packages/next/meta_index.json, Out file: esbuild/index.js
Meta file: packages/payload/meta_index.json, Out file: esbuild/index.js
Meta file: packages/payload/meta_shared.json, Out file: esbuild/exports/shared.js
Meta file: packages/richtext-lexical/meta_client.json, Out file: esbuild/exports/client_optimized/index.js
Meta file: packages/ui/meta_client.json, Out file: esbuild/exports/client_optimized/index.js
Meta file: packages/ui/meta_shared.json, Out file: esbuild/exports/shared_optimized/index.js
DetailsNext to the size is how much the size has increased or decreased compared with the base branch of this PR.
|
🧪 E2E Test Results — TanStack Start AdapterRun: 24249234229 · 2026-04-10 Pass RatesSuites
Individual Tests
TanStack — Suites With 0% Pass Rate (completely broken)22 suites ran at least one test and passed nothing:
TanStack — Suites Cancelled (45-min timeout, 0 tests ran)
TanStack — Partial Results (some tests pass)33 suites with mixed pass/fail
TanStack — Fully Passing Suites (9)
Next.js — Regressions6 failing Next.js suites (11 individual tests):
|
|
Vinxi is dead. It's just vite + @tanstack/react-start/server (this uses H3 under the hood, but that's irrelevant). If there's anything else you'd like us to review, please come join our discord. I think we have a private payload channel but no one is there yet. |
7ef54a3 to
b741714
Compare
|
@tannerlinsley I removed all the references to vinxi! thank you for looking at this so much! 🧪 E2E Test Results — TanStack Start Adapter (Updated)Run: 24477822883 · 2026-04-15 Suite-level pass rates
Individual test-level pass rates (estimated)
Progress vs April 10 run
|
🧪 E2E Test Results — TanStack Start Adapter (Updated — April 21)Run: 24743389080 · 2026-04-21 Suite-level pass rates
Individual test-level pass rates
Progress vs April 15 run
|
Introduce framework-agnostic type contracts that decouple the admin panel from Next.js-specific APIs, enabling alternative framework adapters. Key changes: - Add `admin/adapters.ts` with RouterAdapter, ServerAdapter, ComponentRenderer, DevReloadStrategy, and LinkAdapter type contracts - Replace Next.js `Metadata` type with framework-agnostic `AdminMeta` - Add `Plugin` type extensions (slug, order, options) and `PluginsMap` - Add `renderComponent` to `ServerProps` for adapter-injected rendering - Add `SidebarTab` type for extensible sidebar tab system - Replace hardcoded Next.js HMR WebSocket with pluggable `DevReloadStrategy` - Add `isRSCEnabled()` utility for RSC feature detection - Add `ServerFunctionMode` to server function types - Extend shared exports with `extractJWT`, error types, `canAccessAdmin` - Update `loadEnv.ts` and `resolveImportMapFilePath` for adapter flexibility
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
bfc2491 to
8fd93c0
Compare
Introduce PAYLOAD_FRAMEWORK env variable and switch dispatch in test/dev.ts so the e2e and integration test runners can boot against pluggable framework adapters. Extract the existing Next.js boot logic into test/adapters/nextDevServer.ts behind a shared DevServerResult contract, add the @payloadcms/ui/server path mapping, and add the framework adapter pattern plan document. - Add test/adapters/nextDevServer.ts (extracted from test/dev.ts) - Rewrite test/dev.ts to dispatch on PAYLOAD_FRAMEWORK - Add @payloadcms/ui/server path mapping in tsconfig.base.json - Add docs/plans/framework-adapter-pattern.md
Phase 2 of the framework adapter pattern. Replace direct next/navigation and next/link imports with a framework-agnostic RouterAdapterContext defined in packages/ui, and provide the Next.js implementation in packages/next. - Add packages/ui/src/providers/RouterAdapter: framework-agnostic context exposing useRouter / useSearchParams / usePathname / Link primitives - Add packages/next/src/elements/RouterAdapter: NextRouterAdapter that wires next/navigation hooks and next/link into the context Subsequent commits will replace next/* imports across packages/ui with this adapter and drop next from peerDependencies.
…I to packages/ui Phase 3 of the framework adapter pattern. Move framework-agnostic admin panel UI from packages/next to packages/ui and reduce packages/next to a thin Next.js adapter. Replace next/* imports in packages/ui with the RouterAdapter context introduced in the previous commit. Major moves (packages/next -> packages/ui): - elements/Nav, DocumentHeader, FormHeader, Logo - templates/Default, templates/Minimal - views/Login, ResetPassword, ForgotPassword, Unauthorized, Logout, Verify, CreateFirstUser (orchestrators) - views/API, Account sub-components - views/NotFound, Version, Versions client components - widgets/CollectionCards (sync parts) Other Phase 3 additions in packages/ui: - utilities/routeResolution: framework-agnostic route matching + custom view resolution extracted from packages/next - utilities/serverFunctionRegistry: shared registry of server function handlers consumable by any adapter - New @payloadcms/ui/views/* exports for moved views packages/next changes: - Original element/template/view files reduced to thin re-exports of the packages/ui implementations - handleServerFunctions uses the shared registry Subsequent commit adds Phase 4 work (RSC abstraction, data-first pattern, RenderServerComponent split, ComponentRenderer threading).
Phase 4 (partial) of the framework adapter pattern. Move the RSC flight-path renderer out of packages/ui, extract data fetchers from view components, and thread a pluggable ComponentRenderer through serverProps so non-RSC adapters can render the same view tree with their own rendering primitive. Adapter boundary: - Move RenderServerComponent's canonical implementation from packages/ui to packages/next/src/elements/RenderServerComponent - Retain @payloadcms/ui/elements/RenderServerComponent as a deprecated re-export and add @payloadcms/ui/elements/RenderServerComponent/clientOnly exporting the framework-agnostic RenderClientComponent - Add packages/ui/src/exports/server.ts entrypoint for server-only utils - Add @payloadcms/next/elements/RenderServerComponent export Data-first view pattern (extract async data fetchers from packages/ui views and pair them with index.client.tsx client components — orchestrators in packages/next call the fetcher then render the client component): - Root, Dashboard, Account, Login, Document, List, Version, Versions, Verify, CreateFirstUser data fetchers - Nav.getNavData, CollectionCards data fetcher - ModularDashboard data fetcher + client component Server-function data-only path: - Add packages/ui/src/utilities/dataOnlyHandlers/* and dataOnlyServerFunctions.ts so non-RSC adapters can call render-document, render-list, render-widget, render-field, render-document-slots, getDefaultLayout and receive JSON (no React flight payload) ImportMap as a client provider: - Move ImportMap into packages/ui/src/providers/ImportMap so adapters hydrate the import map through a framework-agnostic context richtext-lexical: - Adopt the injected ComponentRenderer in rscEntry and generateImportMap - Add browser-safe rsc.browser entry and clientEntry for non-RSC adapters
The Phase 1 commit replaced @next/env with dotenv + dotenv-expand in loadEnv.ts but did not update package.json. Drop @next/env, add the dotenv packages, and refresh the lockfile.
Reconcile main's UI4 css migrations and view redesigns (NotFound, CreateFirstUser, Login, Unauthorized) with Phase 3 element/template/view moves from packages/next to packages/ui. Pull main's css into the moved ui locations, drop the original scss files, and keep main's redesigns as the canonical implementation behind thin next-side re-exports. Take main's Document/index.tsx orchestrator as-is to avoid regressing Next.js behavior; the data-first refactor stays available in packages/ui for non-Next adapters but does not back the Next view.
When buildFormState was called without an explicit renderComponent (the Next.js path), the fallback was RenderClientComponent which strips serverProps from RSC components. This dropped clientField on RSC field renderers (e.g. lexical's RscEntryLexicalField), producing 'Initialized lexical RSC field without a field name'. Default to the RSC-aware RenderServerComponent so Next.js form-state builds keep working; non-Next adapters still inject their own ComponentRenderer explicitly. Also flip @payloadcms/ui exports from index.scss to index.css for the view paths Phase 3 moved next->ui (Login, LoginForm, CreateFirstUser, Unauthorized) to match main's UI4 css conversions.
Phase 3 auto-created next-side stubs that re-export from @payloadcms/ui for every moved element/template/view. 64 of these are never imported from anywhere — neither internally in packages/next, nor from other packages, nor via @payloadcms/next/* package.json exports. Delete them. Kept: thin re-exports that are still imported by next-side orchestrators (Document/index.tsx, Account/index.tsx, etc.) via relative paths. Removing those would require adding new @payloadcms/ui/* package.json exports for each (e.g. ./views/Document/getDocPreferences); deferred. Breaking change for external consumers doing deep imports under @payloadcms/next/elements, /templates, or /views — none of these paths are in packages/next/package.json's exports field, so they were unsupported deep imports.
… directly
Replace 34 thin re-export files in packages/next/src with direct
@payloadcms/ui/* imports in the orchestrators and exports/* barrels.
Add the 8 missing @payloadcms/ui package.json exports needed for these
direct imports (Document/getDocPreferences|getDocumentData|getIsLocked
|getVersions, List/enrichDocsWithVersionStatus|resolveAllFilterOptions
|transformColumnsToSelect, elements/DocumentHeader).
Net effect: packages/next contains only orchestrators + adapter-specific
files (RouterAdapter, RenderServerComponent). All framework-agnostic
elements/templates/views are consumed from @payloadcms/ui directly.
Breaking change for deep imports under @payloadcms/next/{elements,
templates,views} — those paths were never in package.json exports
field, so they were already unsupported.
…loadcms/payload into experiment/framework-adapter-pattern
Adds
@payloadcms/tanstack-start— the first non-Next.js framework adapter for Payload's admin panel.Background
The framework adapter pattern already landed across four PRs:
This pushed every Next-specific concern (routing, request init, server functions, HMR) behind typed contracts in
payloadand made@payloadcms/uiframework-agnostic.This PR is the payoff: a working adapter built entirely on that abstraction, proving the admin panel renders on a non-Next stack with no forks of the UI.
Motivation
@payloadcms/uicarries no hidden Next.js assumptions.@payloadcms/uicomponents and data fetchers. There is no TanStack fork of the admin panel — only a thin adapter package that satisfies the contracts.How it differs from the Next.js adapter
Same UI, different plumbing behind the contracts:
'use server'actionscreateServerFnnext/headers@tanstack/react-start/serverIntegration touch points
Wiring Payload into a TanStack Start app is a handful of file routes, similar to the Next.js app dir shipped since Payload v3:
Root shell — This is the highest level touchpoint that affects your app. In your root route file, add the
withPayloadRootshell component:For all other file contents, see the
app-tanstackdirectory in the monorepo (subject to change). Docs to be provided in the future.Status
Experimental. Ships as a new package alongside the Next.js adapter; nothing in the existing Next path changes at runtime.