Files
supabase/apps/studio/compat/next/dynamic.tsx
Alaister Young 6946ec2b2d build(studio): Next-compat shims (stack 2/6, from #46424) (#47110)
**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>
2026-06-25 16:52:34 +08:00

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>
}