Skip to content

feat: add TanStack Start adapter#16717

Draft
r1tsuu wants to merge 79 commits into
mainfrom
feat/tanstack-adapter
Draft

feat: add TanStack Start adapter#16717
r1tsuu wants to merge 79 commits into
mainfrom
feat/tanstack-adapter

Conversation

@r1tsuu

@r1tsuu r1tsuu commented May 22, 2026

Copy link
Copy Markdown
Member

Squashed replay of the tanstack work previously developed on
experiment/framework-adapter-pattern, rebased onto refactor/admin-adapter-types.

Includes:

  • @payloadcms/tanstack-start package (RouterAdapter, ServerAdapter, auth
    server functions, server-function dispatcher, vite plugins, layouts/views)
  • tanstack-app test harness and e2e infrastructure
  • test/adapters/tanstackStartDevServer.ts + test/dev.ts dispatch case
  • Shared additions used by tanstack: payload/shared exports
    (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
  • Hydration-wait helpers and { framework: 'rsc' } test gating for e2e specs
  • Tanstack vite import-protection callback updated to match vite 7.x typings

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.

r1tsuu and others added 30 commits May 11, 2026 17:58
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.
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.
r1tsuu and others added 15 commits June 4, 2026 15:12
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
@r1tsuu

r1tsuu commented Jun 8, 2026

Copy link
Copy Markdown
Member Author

🧪 E2E Test Results — TanStack Start Adapter

Run: 27029215870 · 2026-06-05
Commit: b60066c · feat: add TanStack Start adapter
Branch: feat/tanstack-adapter (PR #16717)


Suite-level pass rates

Counts CI jobs (one shard = one job).

Framework ✅ Passing ❌ Failing ⏱ Cancelled Total Rate
TanStack Start 63 41 1 105 60.0%
Next.js 100 5 0 105 95.2%

Individual test-level pass rates

Framework ✅ Passed ❌ Failed ⏭ Skipped ⛔ Did not run 🔁 Flaky Total ran Rate
TanStack Start 1,311 121 142 92 21 1,574 91.5%
Next.js 1,582 3 105 0 17 1,690 99.8%

TanStack failures by suite

Suite ❌ Failed ⛔ Did not run ✅ Passed 🔁 Flaky Notes
versions (3/3) 30 10 1 0 suite mostly aborted
plugin-import-export 30 6 8 1 suite mostly aborted
lexical__LexicalViewsProvider 5 0 0 0 full suite failed
versions (2/3) 4 0 37 0
lexical__LexicalViewsFrontend 4 0 6 0
storage-s3__client-uploads 3 0 0 1
fields__JSON 3 0 4 1
storage-vercel-blob__client-uploads 3 0 1 0
admin__e2e__general (2/3) 3 0 26 0
joins 2 0 29 0
trash (2/2) 2 0 18 0
locked-documents 2 0 45 0
localization (2/2) 2 0 15 3
admin__e2e__general (1/3) 2 0 27 0
versions (1/3) 2 0 34 2
plugin-multi-tenant#conditionalProvider 1 37 0 0 suite aborted early
plugin-multi-tenant (2/2) 1 18 0 0 suite aborted early
plugin-multi-tenant (1/2) 1 18 0 0 suite aborted early
queues 1 1 0 0
server-url 1 1 0 0
sort 1 1 0 0
uploads (2/3) 1 0 26 0
uploads (1/3) 1 0 26 0
lexical__FullyFeatured__db 1 0 5 0
lexical__blocks (1/2) 1 0 6 1 shared w/ next
lexical__blocks (2/2) 1 0 10 1
lexical__blocks#blockreferences (1/2) 1 0 7 0 shared w/ next
lexical__blocks#blockreferences (2/2) 1 0 11 0
storage-azure__client-uploads 1 0 0 0
bulk-edit (2/2) 1 0 10 0
admin__e2e__general (3/3) 1 0 28 0
admin__e2e__list-view (4/4) 1 0 21 0 shared w/ next
admin__e2e__list-view (2/4) 1 0 21 0
lexical__LexicalViewsProviderDefault 1 0 1 0
fields__UploadPoly 1 0 0 0
access-control (1/2) 1 0 50 0
query-presets 1 0 24 0
admin__e2e__document-view (3/3) 1 0 18 1
admin__e2e__document-view (1/3) 1 0 20 0
lexical__LexicalJSXConverter infra: no tests ran
lexical__LexicalHeadingFeature infra: no tests ran
dashboard cancelled mid-run

r1tsuu and others added 5 commits June 9, 2026 10:02
…-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>
@r1tsuu

r1tsuu commented Jun 9, 2026

Copy link
Copy Markdown
Member Author

🧪 E2E Test Results — TanStack Start Adapter

Run: 27204482979 · 2026-06-09
Commit: a9da5c0 · fix(tanstack-start): normalize Date subclasses in server-fn args
Branch: feat/tanstack-adapter (PR #16717)


Suite-level pass rates

Counts CI jobs (one shard = one job).

Framework ✅ Passing ❌ Failing ⏱ Cancelled Total Rate
TanStack Start 67 37 1 105 63.8%
Next.js 99 6 0 105 94.3%

Individual test-level pass rates

Framework ✅ Passed ❌ Failed ⏭ Skipped ⛔ Did not run 🔁 Flaky Total ran Rate
TanStack Start 1,344 91 142 82 28 1,577 93.7%
Next.js 1,588 4 105 0 10 1,697 99.7%

TanStack failures by suite

Sorted by impact (failed + did-not-run). shared = the same suite also fails on [next] (not adapter-specific).

Suite ❌ Failed ⛔ Did not run ✅ Passed 🔁 Flaky Notes
plugin-multi-tenant#conditionalProvider 1 37 0 0 suite mostly aborted
plugin-import-export 30 6 8 1 suite mostly aborted
plugin-multi-tenant (1/2) 1 18 0 0 suite mostly aborted
plugin-multi-tenant (2/2) 1 18 0 0 suite mostly aborted
admin__general (2/3) 10 0 19 0
lexical__LexicalViewsProvider 5 0 0 0
lexical__LexicalViewsFrontend 4 0 6 0
fields__JSON 3 0 5 0
storage-s3__client-uploads 3 0 1 0
storage-vercel-blob__client-uploads 3 0 1 0
admin__document-view (1/3) 2 0 19 0
hierarchy 2 0 17 0
joins 2 0 29 0
plugin-nested-docs 2 0 1 0
queues 1 1 0 0 suite mostly aborted
server-url 1 1 0 0 suite mostly aborted
sort 1 1 0 0 suite mostly aborted
trash (2/2) 2 0 18 0
access-control (1/2) 1 0 50 0
admin__general (1/3) 1 0 26 2
admin__list-view (2/4) 1 0 19 2
admin__list-view (4/4) 1 0 21 0 shared
bulk-edit (2/2) 1 0 10 0
lexical__LexicalViewsProviderDefault 1 0 1 0
lexical__Lexical__blocks (1/2) 1 0 6 1 shared
lexical__Lexical__blocks (2/2) 1 0 11 0
lexical__Lexical__blocks#blockreferences (1/2) 1 0 7 0 shared
lexical__Lexical__blocks#blockreferences (2/2) 1 0 11 0
lexical___LexicalFullyFeatured__db 1 0 5 0
live-preview (2/2) 1 0 12 0
locked-documents 1 0 46 0
query-presets 1 0 24 0
storage-azure__client-uploads 1 0 0 0
uploads (1/3) 1 0 26 0
uploads (2/3) 1 0 26 0
dashboard 0 0 0 0 cancelled
lexical__LexicalHeadingFeature 0 0 0 0 shared; infra: missing lexical (both frameworks)
lexical__LexicalJSXConverter 0 0 0 0 shared; infra: missing lexical (both frameworks)

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.

r1tsuu and others added 6 commits June 9, 2026 15:34
`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>
@r1tsuu

r1tsuu commented Jun 11, 2026

Copy link
Copy Markdown
Member Author

🧪 E2E Test Results — TanStack Start Adapter

Run: 27347575163 · 2026-06-11
Commit: 40de3ec · fix(tanstack-start): inject dev preamble before </head> to avoid hydration mismatch
Branch: experiment/framework-adapter-pattern (PR #16717)


Suite-level pass rates

Counts CI jobs (one shard = one job).

Framework ✅ Passing ❌ Failing ⏱ Cancelled Total Rate
TanStack Start 76 27 2 105 72.4%
Next.js 98 7 0 105 93.3%

Individual test-level pass rates

Rate = passed / (passed + failed). "Total ran" = passed + failed + skipped.

Framework ✅ Passed ❌ Failed ⏭ Skipped ⛔ Did not run 🔁 Flaky Total ran Rate
TanStack Start 1,329 69 142 82 25 1,540 95.1%
Next.js 1,585 6 105 0 12 1,696 99.6%

TanStack failures by suite

Sorted by impact (failed + did-not-run). shared = the same suite also fails on [next] (not adapter-specific).

Suite ❌ Failed ⛔ Did not run ✅ Passed 🔁 Flaky Notes
plugin-multi-tenant#conditionalProvider 1 37 0 0 suite mostly aborted
plugin-import-export 30 6 9 0
plugin-multi-tenant (1/2) 1 18 0 0 suite mostly aborted
plugin-multi-tenant (2/2) 1 18 0 0 suite mostly aborted
fields__Upload 6 0 2 0
plugin-form-builder 5 0 13 0
admin__general (2/3) 3 0 26 0
storage-s3__client-uploads 3 0 1 0
storage-vercel-blob__client-uploads 3 0 1 0
queues 1 1 0 0 suite mostly aborted
server-url 1 1 0 0 suite mostly aborted
sort 1 1 0 0 suite mostly aborted
access-control (1/2) 1 0 50 0
admin__general (1/3) 1 0 26 2
admin__list-view (2/4) 1 0 21 0
admin__list-view (4/4) 1 0 21 0 shared
fields__Collapsible 1 0 2 0
lexical__Lexical__blocks (1/2) 1 0 6 1 shared
lexical__Lexical__blocks#blockreferences (1/2) 1 0 7 0 shared
lexical___LexicalFullyFeatured__db 1 0 5 0
locked-documents 1 0 46 0
query-presets 1 0 24 0
storage-azure__client-uploads 1 0 0 0
uploads (1/3) 1 0 26 0
uploads (2/3) 1 0 26 0
dashboard 0 0 0 0 cancelled
lexical__LexicalHeadingFeature 0 0 0 0 shared; infra: missing lexical (both frameworks)
lexical__LexicalJSXConverter 0 0 0 0 shared; infra: missing lexical (both frameworks)
versions (3/3) 0 0 0 0 cancelled

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: plugin-import-export and the three plugin-multi-tenant shards account for the bulk of failed + did-not-run tests (mostly aborted suites). The lexical heading/JSX-converter suites fail with Cannot find package 'lexical' on both frameworks (test-infra issue, not adapter-specific).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants