mirror of
https://github.com/supabase/supabase.git
synced 2026-06-28 03:19:09 -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>
108 lines
3.3 KiB
TypeScript
108 lines
3.3 KiB
TypeScript
import { useEffect, useRef, type ComponentPropsWithoutRef, type ReactNode } from 'react'
|
|
|
|
// Next/Script handles ordering, deduplication, and load callbacks for
|
|
// third-party scripts. Under Vite we have no orchestrator — render a
|
|
// `<script>` and approximate the callback contract. Studio doesn't
|
|
// currently mount any <Script>, but the surface is shimmed
|
|
// comprehensively so the migration doesn't catch anyone out later.
|
|
|
|
type Strategy = 'beforeInteractive' | 'afterInteractive' | 'lazyOnload' | 'worker'
|
|
|
|
interface ScriptProps extends Omit<
|
|
ComponentPropsWithoutRef<'script'>,
|
|
'children' | 'onLoad' | 'onError'
|
|
> {
|
|
// Accepted-and-dropped in this shim — the browser handles network
|
|
// priority via the regular `<script>` element; we don't reorder.
|
|
strategy?: Strategy
|
|
// Children as the script body (inline scripts) are also accepted via
|
|
// dangerouslySetInnerHTML; Next allows either. We honour both.
|
|
children?: ReactNode
|
|
onLoad?: (e: Event) => void
|
|
onReady?: () => void
|
|
onError?: (e: Event | string) => void
|
|
}
|
|
|
|
// eslint-disable-next-line no-restricted-exports
|
|
export default function Script({
|
|
strategy: _strategy,
|
|
children,
|
|
dangerouslySetInnerHTML,
|
|
onLoad,
|
|
onError,
|
|
onReady,
|
|
src,
|
|
id,
|
|
...rest
|
|
}: ScriptProps) {
|
|
const ref = useRef<HTMLScriptElement | null>(null)
|
|
const readyFiredRef = useRef(false)
|
|
|
|
// onReady fires once the script has loaded (or immediately on mount
|
|
// if it's already been loaded by a previous instance with the same
|
|
// id). Approximate by firing once after mount when the element is
|
|
// present and either has no src (inline) or has finished loading.
|
|
useEffect(() => {
|
|
const node = ref.current
|
|
if (!node || !onReady || readyFiredRef.current) return
|
|
if (!src) {
|
|
// Inline script — body executed synchronously on mount.
|
|
readyFiredRef.current = true
|
|
onReady()
|
|
return
|
|
}
|
|
if (node.dataset.loaded === 'true') {
|
|
readyFiredRef.current = true
|
|
onReady()
|
|
}
|
|
}, [onReady, src])
|
|
|
|
const handleLoad = (e: Event | React.SyntheticEvent<HTMLScriptElement>) => {
|
|
const node = ref.current
|
|
if (node) node.dataset.loaded = 'true'
|
|
onLoad?.(e as Event)
|
|
if (onReady && !readyFiredRef.current) {
|
|
readyFiredRef.current = true
|
|
onReady()
|
|
}
|
|
}
|
|
|
|
const handleError = (e: Event | React.SyntheticEvent<HTMLScriptElement>) => {
|
|
onError?.(e as Event)
|
|
}
|
|
|
|
// For inline scripts, `children` is preferred over
|
|
// dangerouslySetInnerHTML when both are present (matches Next).
|
|
if (children !== undefined) {
|
|
return (
|
|
<script
|
|
{...rest}
|
|
id={id}
|
|
ref={ref}
|
|
onLoad={handleLoad}
|
|
onError={handleError}
|
|
dangerouslySetInnerHTML={{
|
|
__html: typeof children === 'string' ? children : '',
|
|
}}
|
|
/>
|
|
)
|
|
}
|
|
|
|
return (
|
|
// This shim mirrors next/script for the TanStack build (where vite aliases
|
|
// `next/script` to this file via nextCompat). Call sites that opt for an
|
|
// explicit `async`/`defer` already pass it through via {...rest}; we don't
|
|
// force one here because Next's <Script> doesn't either at this layer.
|
|
// eslint-disable-next-line @next/next/no-sync-scripts
|
|
<script
|
|
{...rest}
|
|
id={id}
|
|
src={src}
|
|
ref={ref}
|
|
onLoad={handleLoad}
|
|
onError={handleError}
|
|
dangerouslySetInnerHTML={dangerouslySetInnerHTML}
|
|
/>
|
|
)
|
|
}
|