Files
supabase/apps/studio/compat/next/script.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

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