mirror of
https://github.com/supabase/supabase.git
synced 2026-06-29 03:50:30 -04:00
6946ec2b2d
**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>
59 lines
1.9 KiB
TypeScript
59 lines
1.9 KiB
TypeScript
import { lazy, Suspense, type ComponentType, type ReactNode } from 'react'
|
|
|
|
type DynamicOptions = {
|
|
loading?: () => ReactNode
|
|
ssr?: boolean
|
|
// Accepted-and-ignored: legacy/deprecated Next options. Listed so
|
|
// call sites that pass them don't fail TypeScript.
|
|
suspense?: boolean
|
|
loadableGenerated?: unknown
|
|
}
|
|
|
|
type Loader<P> = () => Promise<ComponentType<P>> | Promise<{ default: ComponentType<P> }>
|
|
|
|
type DynamicComponent<P> = ComponentType<P> & {
|
|
// Next stamps a `.preload()` on the returned component so consumers
|
|
// can trigger the loader ahead of render (e.g. on hover). Returns a
|
|
// promise that resolves when the loader settles.
|
|
preload: () => Promise<void>
|
|
}
|
|
|
|
function isDefaultExport<P>(
|
|
value: ComponentType<P> | { default: ComponentType<P> }
|
|
): value is { default: ComponentType<P> } {
|
|
return typeof value === 'object' && value !== null && 'default' in value
|
|
}
|
|
|
|
// eslint-disable-next-line no-restricted-exports
|
|
export default function dynamic<P extends object = {}>(
|
|
loader: Loader<P>,
|
|
options: DynamicOptions = {}
|
|
): DynamicComponent<P> {
|
|
const { loading, ssr = true } = options
|
|
|
|
// Cache the in-flight loader promise so calling `preload()` and then
|
|
// rendering doesn't kick off a second import. Matches Next's behaviour
|
|
// where preload is essentially a head-start on the same module load.
|
|
let cached: Promise<{ default: ComponentType<P> }> | null = null
|
|
const load = () => {
|
|
if (cached) return cached
|
|
cached = loader().then((mod) => (isDefaultExport<P>(mod) ? mod : { default: mod }))
|
|
return cached
|
|
}
|
|
|
|
const Lazy = lazy(load)
|
|
|
|
function DynamicComponent(props: P) {
|
|
if (ssr === false && typeof window === 'undefined') return null
|
|
return (
|
|
<Suspense fallback={loading ? loading() : null}>
|
|
<Lazy {...props} />
|
|
</Suspense>
|
|
)
|
|
}
|
|
|
|
;(DynamicComponent as DynamicComponent<P>).preload = () => load().then(() => undefined)
|
|
|
|
return DynamicComponent as DynamicComponent<P>
|
|
}
|