mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 10:55:06 +03:00
**Stack 2/6** of the TanStack Start migration (#46424). Stacked on **#47107** (S1) — review that first; this PR's diff is just the compat shims. > [!NOTE] > Purely additive. Next never imports these files — under TanStack they're wired in via Vite aliases (`next/*` → `@/compat/next/*`). No routes consume them yet (that begins in stack 3). ## What's in this PR `apps/studio/compat/next/*` — drop-in shims so the existing pages-router code runs unchanged under TanStack Start: - `link`, `router`, `navigation`, `head`, `image`, `legacy/image`, `script`, `dynamic`, `server`, `_router-events` — React/runtime shims over `@tanstack/react-router`. - `api.ts` — `toWebHandler`, which adapts a pages-router API handler `(req, res)` into a TanStack server-route Web `fetch` handler. ## Verification On top of S1: `studio` typecheck ✓, lint (0 errors) ✓. Next build is unaffected (nothing imports these under tsc). <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added broad Next.js compatibility support for routing, links, dynamic imports, images, scripts, head metadata, navigation hooks, server responses, and API handlers. * Improved handling of redirects, pathname/search params, base paths, and event callbacks for smoother app behavior. * **Tests** * Added coverage for URL resolution and dynamic route interpolation to verify Next-style routing behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com> Co-authored-by: Ivan Vasilov <vasilov.ivan@gmail.com>
139 lines
4.5 KiB
TypeScript
139 lines
4.5 KiB
TypeScript
// Adapter that exposes a Next pages-router `events` API on top of
|
|
// TanStack Router's `router.subscribe`. Handlers receive Next's
|
|
// signature: `(url: string, options: { shallow: boolean })`.
|
|
//
|
|
// TanStack's `RouterEvents` type (router-core/dist/esm/router.d.ts):
|
|
// - onBeforeNavigate — fires before the URL transition begins
|
|
// - onBeforeLoad — fires after navigation, before route loaders run
|
|
// - onLoad — fires while loaders run
|
|
// - onResolved — fires after route is fully resolved
|
|
// - onBeforeRouteMount
|
|
// - onRendered
|
|
//
|
|
// Each TanStack event payload carries
|
|
// { fromLocation, toLocation, pathChanged, hrefChanged, hashChanged }
|
|
// — we forward `toLocation.href` as the URL arg.
|
|
//
|
|
// Known gap: Next's `routeChangeStart` lets handlers throw to cancel the
|
|
// navigation. TanStack's `subscribe` is fire-and-forget; cancellation
|
|
// requires `useBlocker` instead. `usePreventNavigationOnUnsavedChanges`
|
|
// relies on the throw-to-cancel pattern and will need migrating to
|
|
// `useBlocker` separately.
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
type AnyRouter = any
|
|
type Handler = (url: string, options: { shallow: boolean }) => void
|
|
|
|
type NextEventName =
|
|
| 'routeChangeStart'
|
|
| 'routeChangeComplete'
|
|
| 'routeChangeError'
|
|
| 'beforeHistoryChange'
|
|
| 'hashChangeStart'
|
|
| 'hashChangeComplete'
|
|
|
|
type TanStackEventName =
|
|
| 'onBeforeNavigate'
|
|
| 'onBeforeLoad'
|
|
| 'onLoad'
|
|
| 'onResolved'
|
|
| 'onBeforeRouteMount'
|
|
| 'onRendered'
|
|
|
|
type TanStackNavigationEvent = {
|
|
type: TanStackEventName
|
|
fromLocation?: { href: string; pathname: string; hash: string }
|
|
toLocation: { href: string; pathname: string; hash: string }
|
|
pathChanged: boolean
|
|
hrefChanged: boolean
|
|
hashChanged: boolean
|
|
}
|
|
|
|
type Mapping = {
|
|
tsEvent: TanStackEventName
|
|
// Optional filter — only fire for events matching the predicate (used
|
|
// to scope `hashChange*` to hash-only navigations).
|
|
filter?: (event: TanStackNavigationEvent) => boolean
|
|
}
|
|
|
|
const EVENT_MAP: Record<NextEventName, Mapping | undefined> = {
|
|
// `onBeforeLoad` is closer to Next's `routeChangeStart` semantics than
|
|
// `onBeforeNavigate` — both fire after the URL is committed but before
|
|
// the page renders.
|
|
routeChangeStart: { tsEvent: 'onBeforeLoad' },
|
|
routeChangeComplete: { tsEvent: 'onResolved' },
|
|
// TanStack surfaces errors via router state rather than a dedicated
|
|
// lifecycle event. No-op for now; if a consumer needs this we can
|
|
// subscribe to `router.__store` instead.
|
|
routeChangeError: undefined,
|
|
// Next fires `beforeHistoryChange` between `routeChangeStart` and the
|
|
// pushState call — `onBeforeNavigate` is the closest TanStack stage.
|
|
beforeHistoryChange: { tsEvent: 'onBeforeNavigate' },
|
|
hashChangeStart: {
|
|
tsEvent: 'onBeforeNavigate',
|
|
filter: (e) => e.hashChanged && !e.pathChanged,
|
|
},
|
|
hashChangeComplete: {
|
|
tsEvent: 'onResolved',
|
|
filter: (e) => e.hashChanged && !e.pathChanged,
|
|
},
|
|
}
|
|
|
|
type EventsProxy = {
|
|
on(event: NextEventName, handler: Handler): void
|
|
off(event: NextEventName, handler: Handler): void
|
|
emit(event: NextEventName, ...args: unknown[]): void
|
|
}
|
|
|
|
const proxyCache = new WeakMap<object, EventsProxy>()
|
|
|
|
function createProxy(router: AnyRouter): EventsProxy {
|
|
// Tracked per (event, handler) so the same handler can subscribe to
|
|
// multiple Next events with independent unsubscribes.
|
|
const unsubs = new Map<NextEventName, Map<Handler, () => void>>()
|
|
|
|
return {
|
|
on(event, handler) {
|
|
const mapping = EVENT_MAP[event]
|
|
if (!mapping) return
|
|
|
|
const adapt = (e: TanStackNavigationEvent) => {
|
|
if (mapping.filter && !mapping.filter(e)) return
|
|
// Next handlers expect the destination URL string + a shallow
|
|
// flag. We don't model shallow routing under TanStack, so it's
|
|
// always `false`.
|
|
handler(e.toLocation.href, { shallow: false })
|
|
}
|
|
|
|
const unsub = router.subscribe(mapping.tsEvent, adapt)
|
|
let map = unsubs.get(event)
|
|
if (!map) {
|
|
map = new Map()
|
|
unsubs.set(event, map)
|
|
}
|
|
map.set(handler, unsub)
|
|
},
|
|
off(event, handler) {
|
|
const map = unsubs.get(event)
|
|
if (!map) return
|
|
const unsub = map.get(handler)
|
|
if (unsub) {
|
|
unsub()
|
|
map.delete(handler)
|
|
}
|
|
},
|
|
emit() {
|
|
// Next exposes `events.emit` but nothing in studio calls it.
|
|
},
|
|
}
|
|
}
|
|
|
|
export function getRouterEventsProxy(router: AnyRouter): EventsProxy {
|
|
let proxy = proxyCache.get(router)
|
|
if (!proxy) {
|
|
proxy = createProxy(router)
|
|
proxyCache.set(router, proxy)
|
|
}
|
|
return proxy
|
|
}
|