feat: add TanStack Start adapter#16717
Conversation
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
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.
TS2882 from tsc declaration emit on packages with side-effect CSS/SCSS imports. Added `declare module '*.css'` + '*.scss' shims to next, plugin-ecommerce, plugin-import-export, plugin-multi-tenant, and richtext-lexical.
TS2307 from tsc declaration emit on `next/font/google` / `next/font/local` in CI. Follows same pattern as css/scss shim.
TS2882 from tsc declaration emit on MetaTitleComponent side-effect SCSS import. Same pattern as dd8fa14.
Auto-fixed via `eslint --fix`. No behavior changes.
Regression from packages/next adapter restructure: getDocumentView call in renderDocument lost defaultViews arg, so FallbackVersionsView, FallbackVersionView, and FallbackEditView resolved to null/defaults. For the Versions and Version sub-routes the result was NotFoundView -> "Nothing found" page, even though the underlying data fetch succeeded. Restore defaultViews wiring with EditView, VersionView, VersionsView from packages/next/views.
Refactor 4210a01 stripped the getHTMLDiffComponents wrapping from upload field diffs, which removed the .html-diff__diff-old / .html-diff__diff-new classnames that test selectors and SCSS rely on. Re-add the wrapping divs (without the ReactDOMServer roundtrip) so the From/To columns are addressable by the same classnames as text / textArea diffs.
renderDocumentSlots defaults its renderComponent arg to RenderClientComponent, which strips serverProps. When invoked from the Next.js Document view, slot components like EditMenuItems are RSCs and need server props (id, payload, user, etc.) to render correctly. Without this, custom editMenuItems server components received `props.id === undefined`. Mirrors the same regression class as 66a76ae (buildFormState renderComponent). Also switch the import from the deprecated @payloadcms/ui re-export to the @payloadcms/next-local RenderServerComponent.
The packages/next -> packages/ui Nav refactor dropped the `nav__link--selected` modifier from the active link element, leaving only the `__link-indicator` child div. E2E tests assert presence of the selected modifier on the link itself for active-state checks (also useful for styling hooks that match the link element directly).
The packages/next -> packages/ui Account view move kept the ToggleHighContrast component in packages/next but the moved Settings component (now in packages/ui) only renders ToggleTheme. The /admin/account view was missing the high-contrast checkbox entirely. Move ToggleHighContrast into packages/ui alongside ToggleTheme (it already depends only on the framework-agnostic useTheme provider) and render it from Settings. Drop the now-unused packages/next copy.
…sertion - QueryPresetBar: after the document drawer saves the active preset, call router.refresh() so the RSC re-fetches the updated preset doc. handlePresetChange only mutates URL params, so when only non-URL fields change (e.g. title) the existing flow leaves the cached activePreset prop stale and the trigger button keeps showing the old title. - clearGroupBy helper / preset reset spec: when a query preset is active, sanitizeQuery intentionally preserves an empty `groupBy=` param as the user's clear-override marker (see 594d3c8bf4). Relax the assertions from "groupBy must be absent" to "groupBy must not carry a value" so they pass in both preset-active and preset-absent contexts.
The packages/next -> packages/ui Nav refactor extracted getNavData and made it read `req.payload.config`, but several legitimate callers (notably custom root views built on top of DefaultTemplate, e.g. CustomDefaultView in the admin test suite) render DefaultTemplate without forwarding a `req`. Before the refactor the equivalent inline code read `payload.config` directly and only `getNavPrefs(req)` touched req, so the path tolerated a missing req. Take `payload` explicitly, treat `req` as optional, and fall back to an empty NavPreferences when req isn't available. Update the packages/next DefaultNav adapter to forward the already-destructured payload.
Two regressions from the packages/next -> packages/ui Nav refactor: 1. The ui DefaultNav drops the SidebarTabs wrapper. Any feature that adds a sidebar tab via admin.components.sidebar.tabs (hierarchy, plugins) used to surface as a tab in the nav; after the refactor only the collections list rendered. Rebuild the next DefaultNav to wrap DefaultNavClient in SidebarTabs the same way the original next-side Nav did, while keeping the framework-agnostic ui Nav as the fallback for non-next adapters. 2. The simplified ui getNavPrefs returned null when no preference doc exists; SidebarTabs then dereferences `.activeTab` on the undefined navPreferences and crashes the server render. Restore the original behaviour: always resolve to a NavPreferences object with defaults and merge in the separately-stored sidebar active tab preference.
The packages/next -> packages/ui renderDocumentSlots refactor lost the BeforeDocumentMeta slot wiring. Features that inject components via admin.components.edit.BeforeDocumentMeta (most visibly the hierarchy plugin's folder header button injected by injectHierarchyButton) never mount, so the doc header is missing the folder picker entirely and any e2e that expects to click "find child folder" times out. Re-emit BeforeDocumentMeta from the RSC renderer alongside BeforeDocumentControls, mirroring the original next-side implementation, and add the same slot to the data-only handler / DocumentSlotConfigs.
The Nav rewrite stopped importing the ui DefaultNav component (which side-effect-imported its scss) and didn't replace it with an equivalent side-effect import for packages/next/src/elements/Nav/index.css. As a result .nav__scroll lost its height/overflow rules and .nav__wrap / .nav__controls lost their flex layout, so the settings menu button rendered far below the viewport and was unclickable in tests. Re-add the local index.css side-effect import.
The CustomListDrawer e2e test waits for the success toast as its synchronization point, then closes the document drawer and asserts the outer list drawer now shows two rows. The original handler fired the toast immediately after the POST and only then awaited refresh(), so in slower environments the test could close the drawer and check rows before refresh() resolved. Reorder so refresh() resolves first; toast then acts as a deterministic signal that the outer list view is up to date.
This reverts commit 31bf24a.
The ListDrawer refresh path previously cleared the currently rendered list view (setListView(null)) when the server function returned an unexpected shape, and force-closed the drawer outright when it threw. Both behaviors are fine for the very first load, when there is no list to preserve and the drawer cannot show without data. They are too destructive for refreshes triggered by user actions (e.g. the "create new" button from a nested CustomListDrawer in a doc drawer): a transient server-function error or empty payload would silently nuke the open list view (and any nested document drawer inside it). Track whether the drawer has ever successfully rendered a list view via hasLoadedRef. After the first success, treat missing/error results as a no-op and let the existing view remain visible.
Switching the admin language dispatched `switch-language` through the generic Payload server-function registry, but the TanStack dispatcher never registered a handler for it (only Next.js did), so the call failed with "Unknown Server Function: switch-language" — the language cookie was never written and the UI never re-rendered in the selected language (e.g. the RTL admin-panel lexical test). Register a framework-agnostic `switch-language` handler that writes the `<cookiePrefix>-lng` cookie through `req.server`, and wire the `tanstackServerAdapter` onto `req.server` in `initReq` (mirroring the Next.js adapter, which passes its `ServerAdapter`). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…er race
In CI cold starts, Vite discovered several transitive deps only after the
initial crawl — @floating-ui/dom + @floating-ui/core (via react-select),
react-is (via prop-types), and the default date-fns locale (loaded through
a dynamic `date-fns/locale/${key}` import). Each late discovery triggered a
full dep re-optimization mid-session, which 404s every in-flight
`.vite/deps/*` chunk ("Pre-transform error: file does not exist in the
optimize deps directory") and broke the admin UI before tests could run.
Add them to `optimizeDeps.include` so the first optimization pass is
complete and no mid-session re-optimization is triggered.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reconcile the TanStack adapter branch with PR #16803 (feat!: admin view adapter), which landed a parallel, Next-only implementation of the same admin-view migration on main. Resolution: adopt main's view-split architecture wholesale (renderRoot + defaultAdminViews + AdminViewAdapter, unified generatePageMetadata, the adapter-ready RootProvider/RouterAdapter hooks) and re-graft the TanStack adapter on top of it: - Reset packages/ui/src, packages/next/src, and the admin view tree to main; delete the branch's superseded parallel design (renderAdminPage/ renderAdminView, *ViewRSC, getRootViewData, routeResolution/, branch view data loaders, duplicate Nav/template files). - Keep TanStack-only additive code (packages/tanstack-start, serverFunctionRegistry, CollectionCards widget, ui server export) and repoint serverFunctionRegistry at main's views/{List,Document}/handleServerFunction handlers. - Rewire tanstack-app's admin page onto renderRoot: inject a TanStack initReq + error-contract ServerAdapter (string nav contract caught at the loader boundary), and resolve page metadata via main's shared generatePageMetadata. - Revert payload core MetaConfig to main's (DeepClone<Metadata>); keep the additive ServerProps.renderComponent / optional server fields. - Restore the css/scss tsc shim in @payloadcms/ui; regenerate ui package exports (main base + TanStack-only entries) and pnpm-lock. build:core passes (47/47 packages). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…trailing-slash redirect
After merging onto main's shared `renderRoot`, the admin-page server function
passed `segments: []` for the admin root (`/admin`). `renderRoot` builds
`currentRoute` via `formatAdminURL({ path: Array.isArray(segments) ? `/${segments.join('/')}` : null })`,
so an empty array produced `/admin/` (trailing slash) instead of `/admin`.
That no longer equals `adminRoute`, so `handleAuthRedirect` appended
`?redirect=/admin/` and the unauthenticated redirect landed on
`/admin/login?redirect=%2Fadmin%2F` instead of a clean `/admin/login`.
Next's optional catch-all passes `undefined` for the root; match that by
passing `undefined` when there are no splat segments. Restores the clean
`/admin -> /admin/login` redirect.
Verified: TanStack `auth` e2e suite now passes 12/12 (1 intentionally skipped),
up from a beforeAll hook timeout that blocked the entire suite.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… views
`renderRoot` (and main's view components) navigate via `req.server.notFound()`
/ `redirect()` not only during orchestration but also deep inside streamed
view components — e.g. `DocumentView` access checks and `LoginView`
already-authenticated. Those throws happen during `renderServerComponent`, where
the RSC stream swallows them (surfacing as a client `CatchBoundary` error)
instead of rejecting the render promise, so the admin-page loader never saw
them: a restricted doc rendered a broken tree instead of the NotFound view, and
a disallowed user was never redirected.
- Replace the static error-contract adapter with `createPageRenderServerAdapter(nav)`,
which records the navigation intent on a per-request holder *and* throws the
string contract. The loader inspects `nav` after `renderServerComponent`
resolves (the render buffers), then:
- redirect → returns the `_redirect` sentinel (route loader re-throws native
TanStack redirect);
- notFound → renders Payload's `renderNotFoundPage` and returns its payload
(so `.not-found` shows), matching the Next adapter which serves
`renderNotFoundPage` for `req.server.notFound()`. The broken aborted-render
payload is discarded.
- Whitelist the adapter's `not-found` / `redirect:` nav control-flow strings in
`initPageConsoleErrorCatch`, mirroring the existing NEXT_NOT_FOUND /
NEXT_REDIRECT entries (these are expected control flow, not errors).
Verified: TanStack `access-control` e2e 99→101 passing. The two fully-restricted
notFound cases and the unauthorized-user redirect now pass. The lone remaining
failure (817, logout-success toast) is an unrelated Logout-flow issue.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- adminPageRSC: drop redundant `as string[]` segment casts (the app's tsconfig is non-strict, so eslint flags them as unnecessary) - tanstack-start exports + ui serverFunctionRegistry: fix import/export sort order (perfectionist) Resolves the CI `lint` job failures. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`ignoreDeprecations: "6.0"` (added alongside a restored `baseUrl`) is only a valid value on TypeScript 6.0; tstyche runs the type tests across TS 5.7–6.0, and 5.7/5.8/5.9 reject it with ts(5103) `Invalid value for '--ignoreDeprecations'`, failing the `tests-types` job before any assertions run (596/596 passed on 6.0). `baseUrl` was only restored to back the `__helpers/*` path alias, but `paths` resolve relative to the defining tsconfig without `baseUrl` (since TS 4.1), so dropping `baseUrl` removes the deprecation entirely — no `ignoreDeprecations` needed — and works across all tstyche target versions. The `__helpers/*` and `@payload-config` aliases are retained and still resolve. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
getAdminMeta only emitted charSet/viewport/title/description, dropping every
other tag main's shared generatePageMetadata produces, so the admin pages were
missing OpenGraph, Twitter, favicon and robots tags under TanStack.
- toAdminPageMetadata: flatten the resolved MetaConfig (Next Metadata shape)
into a serializable AdminPageMetadata (title, description, robots, keywords,
openGraph {title, description, images, siteName}, icons), dropping the
non-serializable metadataBase/functions.
- getAdminMeta: render the full set as TanStack head() meta + links — including
og:*, og:image(:width/height/alt), twitter:* (inherited from openGraph, as
Next does) and link[rel=icon] favicons.
- Route head() now spreads { meta, links }.
Verified: TanStack admin__e2e__general metadata tests 10/18 → 18/18.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…otFound The notFound rendering (Payload's `.not-found` view via renderNotFoundPage) was only wired for notFound thrown deep inside a streamed view component; notFound thrown during `renderRoot` orchestration (e.g. an unknown root route like `/admin/1234`) hit the loader catch and returned the bare `_notFound` sentinel, which renders TanStack's generic notFound component instead of `.not-found`. Extract a `renderNotFound()` helper and use it from both the post-render holder branch and the catch branch, and attempt a 404 document status via `setResponseStatus`. NotFound now consistently renders Payload's view. Verified: TanStack admin__e2e__general routing 5/7 → 6/7 (the custom-view and not-found content cases pass; the remaining failure is the strict `response.status() === 404` doc-status assertion, a TanStack SSR limitation where a server function cannot set the document response status). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`assertNetworkRequests` accepts `/_serverFn/` requests on tanstack-start so that
tests counting form-state requests (which Next posts to the admin/document URL,
tanstack dispatches via `createServerFn` to `/_serverFn/...`) work cross-framework.
But this applied to *every* asserted URL, so tests asserting a framework-agnostic
REST endpoint — e.g. `/api/<collection>/access/<id>` — also counted unrelated
form-state RPCs fired during the same window, reporting 4 requests where 2 were
expected.
Only apply the `/_serverFn` substitution when the asserted URL is not an
`/api/...` REST endpoint (i.e. it's the admin/document URL where Next routes form
state). REST endpoints are identical across adapters and need no substitution.
Verified: TanStack form-state e2e 12 → 14 passing ("fetch new doc permissions
after save" and "autosave should not fetch permissions on every autosave" now
pass; the adapter was already firing the correct 2 requests).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ate transport The "optimistic rows should not disappear between pending network requests" test counted/awaited/blocked form-state requests by matching `postsUrl.create` (where Next posts RSC form state). TanStack dispatches form state via the `createServerFn` RPC at `/_serverFn/<id>`, so `requestCount` stayed 0 and the test failed at `expect.poll(() => requestCount).toBe(1)` before ever exercising the optimistic behavior. - Add an adapter-aware `isFormStatePOST` matcher: the document URL on Next, or a `/_serverFn/` POST whose body carries the `form-state` name on tanstack (the RPC multiplexes all server functions, so the name disambiguates). - Use it for the request counter, the response wait, and the request-blocking route. - Clean up with `page.unrouteAll()` — `unroute()` with a function matcher does not reliably remove the route, which otherwise leaked the abort into the next test (its form-state requests were aborted, so server-rendered fields never appeared). Verified: the optimistic behavior is correct on tanstack (the row stays visible); form-state e2e 14 → 15 passing with no regression to the following nested-components test. Next behavior is unchanged (the document-URL match path is preserved). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ady-to-edit Two form-state tests assert Next.js-specific behavior that TanStack Start legitimately does differently (verified — not adapter bugs), so gate them to `framework: 'next'` via the framework-aware test wrapper, with comments: - `should disable fields during initialization`: Next runs a client-side form-state init during which fields are disabled; tanstack serves the form already-initialized in the RSC payload, so there's no disabled phase. Added a `framework: 'tanstack-start'` companion test (`should render the create form ready to edit`) asserting the form is immediately enabled and editable. - `should send lastRenderedPath only when necessary`: inspects the form state embedded in Next's RSC request body; tanstack dispatches form state via the createServerFn RPC (seroval-encoded body), so this transport-specific assertion doesn't apply. The optimization is framework-agnostic shared logic exercised by the other form-state tests. These still run on Next (coverage preserved). TanStack form-state e2e: 0 failures (16 passed / 6 skipped). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The addBlock / addBlockBelow / duplicateBlock helpers measured `numberOfPrevRows` immediately after navigation. TanStack Start renders the document form client-side from the RSC payload (Next.js SSRs it into the initial HTML), so on tanstack the field's default rows had not rendered yet when the count was taken — it read 0 instead of 4, then the post-add/duplicate `+ 1` assertion saw all 5 rows and failed (`Expected 1, Received 5`). Wait for `[data-form-ready="true"]` before measuring. Framework-agnostic: a fast no-op on Next (form already present), and gates the client render on tanstack. Verified: TanStack fields__collections__Blocks e2e 0 failures (36 passed), up from 7 failures — all were this same timing race. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ck-adapter # Conflicts: # test/__helpers/e2e/filters/openListFilters.ts
🧪 E2E Test Results — TanStack Start AdapterRun: 27029215870 · 2026-06-05 Suite-level pass ratesCounts CI jobs (one shard = one job).
Individual test-level pass rates
TanStack failures by suite
|
…-warning silencer, regen importMap Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…m/server The version-diff views call renderToStaticMarkup from react-dom/server while rendering a Server Component. Under @vitejs/plugin-rsc the RSC environment activates the react-server export condition, where every react-dom/server* subpath resolves to a stub that throws "react-dom/server is not supported in React Server Components", aborting the entire versions e2e suite. - Add reactDomServerInRsc: pre-bundles a self-contained react-dom/server with client React inlined (esbuild, without the react-server condition) and redirects react-dom/server to it in the RSC graph only. - Extend stripDistStyleImports to also strip CSS side-effect imports from payload-package workspace src (/packages/*/src/), not just dist. CheckIcon's './index.css' was otherwise a render-time suspending resource under renderToStaticMarkup, failing all diff-view tests via the console-error catch. versions shard 3/3 prod e2e: ~1 passed/30+ aborted -> 41 passed/0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…admin Two fixes for the versions suite's status pill showing the wrong value: - getVersions: when readVersions access is denied, a missing doc (the create view, before first save) no longer reports hasPublishedDoc=true via `undefined !== 'draft'`. A brand-new draft now correctly shows 'Draft'. - tanstack admin route: key the rendered RSC subtree by a loader-derived routeKey (the splat) instead of location.pathname. During a navigation transition the pathname updates before useLoaderData(), so a pathname key remounted with the previous payload then reconciled the fresh payload in place, leaving DocumentInfo providers holding stale useState from the prior document (e.g. a duplicated draft showing the source's 'Published' status). routeKey changes in lockstep with rscPayload; search params are excluded so list-view filtering still reconciles in place. Fixes versions e2e: readVersions-permission status tests, duplicate-to-draft, and publish-specific-locale. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The schedule-publish drawer passes a `TZDate` (@date-fns/tz, a Date subclass)
to the `schedulePublish` server function. It is `instanceof Date` but has a
different `constructor`, so TanStack Start's seroval serializer rejects it
("The value [object Date] ... cannot be parsed/serialized"), the server
function never runs, and no scheduled job/upcoming row is created.
Normalize any Date instance to a plain `Date` in the args sanitizer so the
value serializes and the server still receives a real Date.
Fixes versions e2e: Scheduled publish > correctly sets a UTC date.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🧪 E2E Test Results — TanStack Start AdapterRun: 27204482979 · 2026-06-09 Suite-level pass ratesCounts CI jobs (one shard = one job).
Individual test-level pass rates
TanStack failures by suiteSorted by impact (failed + did-not-run). shared = the same suite also fails on
TanStack adapter is at ~93.7% test-level / 63.8% suite-level — up from the prior run (~91.5% / 60.0%). The versions suite (all 3 shards) is now fully green. |
`parseNestedLabel` did `label.includes(' > ')`, assuming the label is always
a string. A field can supply a React element (custom `Label`) or a non-string
translation object, which threw `label.includes is not a function` and blanked
the list view's column selector. Accept `React.ReactNode` and treat non-strings
as a single, non-splittable segment.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…izeForRsc beforeInput/afterInput field component arrays were serialized element-by-element through renderServerComponent, which drops each element's React key. The client then rendered them as unkeyed array children, tripping React's "unique key prop" warning and failing the JSON, joins, and bulk-edit e2e suites (the shared console error catcher throws on it). Render such all-element arrays as a single Fragment-backed RSC handle instead, keeping per-element keys inside one Flight payload. Verified: JSON 8/8, joins 31/31, bulk-edit 22/22 now green.
…provider inheritance In production builds, RichTextViewContext was duplicated across vite chunks (the package ships an esbuild-bundled `/client` copy of RichTextViewProvider plus the granular `dist/field/RichTextViewProvider.js`), so the provider and the editor's `useRichTextView()` consumer resolved different context objects. Nested richtext editors then never inherited `currentView` from a parent `RichTextViewProvider` — `data-lexical-view` stayed "default" and the view selector showed when it should be hidden. Dev passed (single source module); only prod bundling duplicated it, which is why CI (prod e2e) caught it. Adding `@payloadcms/richtext-lexical` to `resolve.dedupe` (next to the existing `@payloadcms/ui`) forces a single module instance. Verified in prod: LexicalViewsProvider 0/5 -> 5/5, LexicalViewsFrontend -> 10/10.
… the RSC env
ssrStripDistStyleImports stripped `import './x.css'` from all server
environments (SSR + RSC) to keep Node's ESM loader from crashing on `.css`.
But server components — e.g. the admin `Nav` (a non-'use client' component that
`import './index.css'`) — render in the RSC graph, and `@vitejs/plugin-rsc`
must SEE their CSS imports there to collect them as client stylesheets.
Stripping in the RSC env meant that CSS (`.nav__scroll { overflow-y:auto }`,
`.nav__wrap`, etc.) was never emitted, so the admin nav couldn't scroll and its
items rendered off-viewport — breaking the admin__general nav/settings tests.
Skip the RSC environment in both `resolveId` and `transform`. The Node-side
`.css` no-op is already handled by `cssLoader.mjs` (dev) and Vite's CSS
extraction (build), so the crash this plugin guards against doesn't require
touching the RSC env. SSR stripping is unchanged.
Verified in prod (`test:e2e:prod admin__e2e__general`): build succeeds, no
crash, suite 83 passed / 4 failed (was ~11 failed) — nav navigation, settings
menu, and custom nav components (beforeNav/afterNav) all pass. Remaining 4
(custom providers, custom CSS, 404 view) are unrelated.
…ation mismatch The dev-time Vite HMR + React Refresh preamble was injected at the start of <head>. React 19 treats <meta>/<link>/<style>/<title> as hoistable resources (key-matched, position-tolerant), but the non-hoistable inline <script type="module"> placed before React's first rendered head node makes hydration position-match that script against a <style>/<meta> and throw. React then discards and regenerates the whole tree client-side, which aborts in-flight server-function fetches (TypeError: Failed to fetch in serverFnFetcher) and intermittently breaks drawers, forms, and navigation. Inject the preamble before </head> instead, after React's hoistable head content, so it stays out of that positional comparison while still running before the body's app-entry module. Verified on fields__collections__Relationship shard 2/2 under CI conditions (CI=true, cold .vite cache): hydration mismatches 22 -> 0, 16/16 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🧪 E2E Test Results — TanStack Start AdapterRun: 27347575163 · 2026-06-11 Suite-level pass ratesCounts CI jobs (one shard = one job).
Individual test-level pass ratesRate = passed / (passed + failed). "Total ran" = passed + failed + skipped.
TanStack failures by suiteSorted by impact (failed + did-not-run). shared = the same suite also fails on
TanStack adapter is at ~95.1% test-level / 72.4% suite-level — up from the prior run (~93.7% / 63.8%, run 27204482979). Remaining failure concentration is unchanged: |
Squashed replay of the tanstack work previously developed on
experiment/framework-adapter-pattern, rebased onto refactor/admin-adapter-types.
Includes:
server functions, server-function dispatcher, vite plugins, layouts/views)
(extractAccessFromPermission, combineQueries, logError), payload "browser"
conditional export pointing at exports/shared.ts, srvx-safe header/signal
extraction in handleEndpoints, ASSET shims in packages/tanstack-start/src/https://github.com/types
For all shared files that both branches refactored differently
(packages/ui, packages/next, packages/payload), refactor/admin-adapter-types
is preserved; only genuinely tanstack-specific changes are layered on top.