mirror of
https://github.com/supabase/supabase.git
synced 2026-06-27 11:02:53 -04:00
9eab4f8fbf
**Stack 1/6** of the TanStack Start migration (#46424), split into reviewable, independently-mergeable PRs. > [!IMPORTANT] > **Next stays the default and only active framework after this PR.** This wires up the Vite/TanStack-Start build pipeline behind the `STUDIO_FRAMEWORK` flag, but there are no TanStack routes yet — so the TanStack build isn't functional or tested until later PRs in the stack. Nothing about the Next build, dev, or deploy changes behaviourally here. ## What's in this PR - **Dispatch:** `dev`/`build`/`start` now go through `scripts/dispatch.js`, which runs the Next variant unless `STUDIO_FRAMEWORK=tanstack`. The original commands are preserved as `dev:next`/`build:next`/`start:next`. - **Build pipeline:** `vite.config.ts`, `serve.js`, `smoke-server.mjs`, vite/tanstack deps, `turbo.jsonc`. - **`tsconfig.json`:** `jsx: react-jsx`, `moduleResolution: Bundler`, `target: ES2022`. Because `include` is `**/*.ts(x)`, this re-typechecks the whole app, so the companion adaptations below land with it. - **Shared adaptations (companions to the tsconfig change):** `BufferSource` casts, `packages/ui` unused-`React` import removals, etc. - **Routing/middleware plumbing:** `next.config.ts` + `redirects.shared.ts` (redirect rules now shared with `vercel.ts`), `proxy.ts`/`start.ts` middleware + `hosted-api-allowlist.ts`. ## Verification Run locally off `master`: frozen install ✓, `studio` typecheck ✓, **Next build ✓** (compiles + generates all routes), lint ratchet ✓ ("some rules improved"), prettier ✓. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a hosted API endpoint allowlist to return 404 for non-supported `/api/*` routes. * Introduced a TanStack route-migration checklist and expanded TanStack Start routing support. * **Improvements** * Enhanced deployment refresh/detection by tightening cookie handling for “latest deployment” updates. * Centralized redirect/maintenance-mode rules for consistent platform vs self-hosted behavior. * Improved production serving with a dedicated static + proxy server and a post-build smoke test. * **Dependencies** * Updated TanStack-related packages and React Table/query tooling versions. * **Documentation / Chores** * Updated formatting and tooling config; added shared build environment parsing utilities. <!-- 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>
425 lines
13 KiB
TypeScript
425 lines
13 KiB
TypeScript
// Shared redirect tables consumed by both `next.config.ts` (the Next.js
|
|
// build) and `vercel.ts` (the TanStack/Vercel deploy). Defined once here
|
|
// so the two configs don't drift.
|
|
//
|
|
// Shape mirrors Next's `redirects()` entry and Vercel's `Redirect` type
|
|
// (they're identical — `source`, `destination`, `permanent`, optional
|
|
// `has`). Basepath handling differs between the two: Next auto-prefixes,
|
|
// Vercel doesn't — each consumer is responsible for that.
|
|
|
|
export interface StudioRedirect {
|
|
source: string
|
|
destination: string
|
|
permanent: boolean
|
|
has?: Array<{ type: 'query'; key: string; value: string }>
|
|
}
|
|
|
|
export const PLATFORM_REDIRECTS: StudioRedirect[] = [
|
|
{
|
|
source: '/',
|
|
has: [{ type: 'query', key: 'next', value: 'new-project' }],
|
|
destination: '/new/new-project',
|
|
permanent: false,
|
|
},
|
|
{ source: '/', destination: '/org', permanent: false },
|
|
{ source: '/register', destination: '/sign-up', permanent: false },
|
|
{ source: '/signup', destination: '/sign-up', permanent: false },
|
|
{ source: '/signin', destination: '/sign-in', permanent: false },
|
|
{ source: '/login', destination: '/sign-in', permanent: false },
|
|
{ source: '/log-in', destination: '/sign-in', permanent: false },
|
|
{ source: '/project/:ref/building', destination: '/project/:ref', permanent: false },
|
|
]
|
|
|
|
export const SELF_HOSTED_REDIRECTS: StudioRedirect[] = [
|
|
{ source: '/', destination: '/project/default', permanent: false },
|
|
{ source: '/register', destination: '/project/default', permanent: false },
|
|
{ source: '/signup', destination: '/project/default', permanent: false },
|
|
{ source: '/signin', destination: '/project/default', permanent: false },
|
|
{ source: '/login', destination: '/project/default', permanent: false },
|
|
{ source: '/log-in', destination: '/project/default', permanent: false },
|
|
{ source: '/project/:ref/building', destination: '/project/:ref', permanent: false },
|
|
]
|
|
|
|
export const SHARED_REDIRECTS: StudioRedirect[] = [
|
|
{ source: '/project/:ref/auth', destination: '/project/:ref/auth/users', permanent: true },
|
|
{
|
|
source: '/project/:ref/auth/advanced',
|
|
destination: '/project/:ref/auth/performance',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/database',
|
|
destination: '/project/:ref/database/tables',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/database/graphiql',
|
|
destination: '/project/:ref/api/graphiql',
|
|
permanent: true,
|
|
},
|
|
{ source: '/project/:ref/storage', destination: '/project/:ref/storage/files', permanent: true },
|
|
{
|
|
source: '/project/:ref/storage/buckets',
|
|
destination: '/project/:ref/storage/files',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/storage/policies',
|
|
destination: '/project/:ref/storage/files/policies',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/storage/buckets/:bucketId',
|
|
destination: '/project/:ref/storage/files/buckets/:bucketId',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/settings/api-keys/new',
|
|
destination: '/project/:ref/settings/api-keys',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/settings/storage',
|
|
destination: '/project/:ref/storage/files/settings',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/storage/settings',
|
|
destination: '/project/:ref/storage/files/settings',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/settings/database',
|
|
destination: '/project/:ref/database/settings',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/settings',
|
|
destination: '/project/:ref/settings/general',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/auth/settings',
|
|
destination: '/project/:ref/auth/users',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/settings/billing/subscription',
|
|
has: [{ type: 'query', key: 'panel', value: 'subscriptionPlan' }],
|
|
destination: '/org/_/billing?panel=subscriptionPlan',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/settings/billing/subscription',
|
|
has: [{ type: 'query', key: 'panel', value: 'pitr' }],
|
|
destination: '/project/:ref/settings/addons?panel=pitr',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/settings/billing/subscription',
|
|
has: [{ type: 'query', key: 'panel', value: 'computeInstance' }],
|
|
destination: '/project/:ref/settings/compute-and-disk',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/settings/billing/subscription',
|
|
has: [{ type: 'query', key: 'panel', value: 'customDomain' }],
|
|
destination: '/project/:ref/settings/addons?panel=customDomain',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/settings/billing/subscription',
|
|
destination: '/org/_/billing',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/settings/jwt/signing-keys',
|
|
destination: '/project/:ref/settings/jwt',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/database/api-logs',
|
|
destination: '/project/:ref/logs/edge-logs',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/database/postgres-logs',
|
|
destination: '/project/:ref/logs/postgres-logs',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/database/postgrest-logs',
|
|
destination: '/project/:ref/logs/postgrest-logs',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/database/pgbouncer-logs',
|
|
destination: '/project/:ref/logs/pooler-logs',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/logs/pgbouncer-logs',
|
|
destination: '/project/:ref/logs/pooler-logs',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/database/realtime-logs',
|
|
destination: '/project/:ref/logs/realtime-logs',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/storage/logs',
|
|
destination: '/project/:ref/logs/storage-logs',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/auth/logs',
|
|
destination: '/project/:ref/logs/auth-logs',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/logs-explorer',
|
|
destination: '/project/:ref/logs/explorer',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/sql/quickstarts',
|
|
destination: '/project/:ref/sql/examples',
|
|
permanent: true,
|
|
},
|
|
{ source: '/org/:slug/settings', destination: '/org/:slug/general', permanent: true },
|
|
{
|
|
source: '/project/:ref/settings/billing/update',
|
|
destination: '/org/_/billing',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/settings/billing/update/free',
|
|
destination: '/org/_/billing',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/settings/billing/update/pro',
|
|
destination: '/org/_/billing',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/settings/billing/update/team',
|
|
destination: '/org/_/billing',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/settings/billing/update/enterprise',
|
|
destination: '/org/_/billing',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/reports/linter',
|
|
destination: '/project/:ref/database/linter',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/reports',
|
|
destination: '/project/:ref/observability',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/reports/:path*',
|
|
destination: '/project/:ref/observability/:path*',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/query-performance',
|
|
destination: '/project/:ref/observability/query-performance',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/advisors/query-performance',
|
|
destination: '/project/:ref/observability/query-performance',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/database/query-performance',
|
|
destination: '/project/:ref/observability/query-performance',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/auth/column-privileges',
|
|
destination: '/project/:ref/database/column-privileges',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/database/linter',
|
|
destination: '/project/:ref/database/security-advisor',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/database/security-advisor',
|
|
destination: '/project/:ref/advisors/security',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/database/performance-advisor',
|
|
destination: '/project/:ref/advisors/performance',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/database/webhooks',
|
|
destination: '/project/:ref/integrations/webhooks/overview',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/database/wrappers',
|
|
destination: '/project/:ref/integrations?category=wrapper',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/database/cron-jobs',
|
|
destination: '/project/:ref/integrations/cron',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/api/graphiql',
|
|
destination: '/project/:ref/integrations/graphiql',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/settings/vault/secrets',
|
|
destination: '/project/:ref/integrations/vault/secrets',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/settings/vault/keys',
|
|
destination: '/project/:ref/integrations/vault/keys',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/integrations/cron-jobs',
|
|
destination: '/project/:ref/integrations/cron',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/settings/warehouse',
|
|
destination: '/project/:ref/settings/general',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/settings/functions',
|
|
destination: '/project/:ref/functions/secrets',
|
|
permanent: true,
|
|
},
|
|
{ source: '/org/:slug/invoices', destination: '/org/:slug/billing#invoices', permanent: true },
|
|
{ source: '/projects', destination: '/organizations', permanent: false },
|
|
{
|
|
source: '/project/:ref/settings/auth',
|
|
destination: '/project/:ref/auth/providers',
|
|
permanent: true,
|
|
},
|
|
{
|
|
source: '/project/:ref/settings/api',
|
|
destination: '/project/:ref/integrations/data_api/overview',
|
|
permanent: false,
|
|
},
|
|
{
|
|
source: '/project/:ref/api',
|
|
destination: '/project/:ref/integrations/data_api/docs',
|
|
permanent: false,
|
|
},
|
|
]
|
|
|
|
// The two maintenance-mode rules are mutually exclusive; pick by env at
|
|
// build time. Same shape for Next and Vercel.
|
|
export function getMaintenanceRedirects(maintenanceMode: boolean): StudioRedirect[] {
|
|
return maintenanceMode
|
|
? [{ source: '/((?!maintenance|img).*)', destination: '/maintenance', permanent: false }]
|
|
: [{ source: '/maintenance', destination: '/', permanent: false }]
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Runtime matcher — used by TanStack's `__root` beforeLoad so self-hosted
|
|
// (and any request Vercel's edge didn't already intercept) still gets the
|
|
// same redirect behaviour as the Next.js / Vercel deploys.
|
|
//
|
|
// The patterns above are written in Next/Vercel-flavoured syntax: `:name`
|
|
// for a single segment, `:name*` for a trailing catch-all, literals
|
|
// otherwise. That's all the redirect rules use, so a hand-rolled matcher
|
|
// is simpler (and dependency-free) than pulling in path-to-regexp here.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type RedirectMatch = { destination: string; permanent: boolean }
|
|
|
|
function matchPattern(pattern: string, pathname: string): Record<string, string> | null {
|
|
const patternParts = pattern.split('/')
|
|
const pathParts = pathname.split('/')
|
|
const params: Record<string, string> = {}
|
|
|
|
for (let i = 0; i < patternParts.length; i++) {
|
|
const p = patternParts[i]
|
|
if (p?.startsWith(':') && p.endsWith('*')) {
|
|
params[p.slice(1, -1)] = pathParts.slice(i).join('/')
|
|
return params
|
|
}
|
|
if (p?.startsWith(':')) {
|
|
const v = pathParts[i]
|
|
if (!v) return null
|
|
params[p.slice(1)] = decodeURIComponent(v)
|
|
continue
|
|
}
|
|
if (p !== pathParts[i]) return null
|
|
}
|
|
return patternParts.length === pathParts.length ? params : null
|
|
}
|
|
|
|
function substituteDestination(dest: string, params: Record<string, string>): string {
|
|
return dest.replace(/:(\w+)\*?/g, (_, name) => params[name] ?? '')
|
|
}
|
|
|
|
function hasQueryMatches(
|
|
has: StudioRedirect['has'],
|
|
search: URLSearchParams | Record<string, string | string[] | undefined>
|
|
): boolean {
|
|
if (!has?.length) return true
|
|
const get = (k: string) =>
|
|
search instanceof URLSearchParams
|
|
? search.get(k)
|
|
: Array.isArray(search[k])
|
|
? (search[k] as string[])[0]
|
|
: (search[k] as string | undefined)
|
|
return has.every((h) => h.type === 'query' && get(h.key) === h.value)
|
|
}
|
|
|
|
export function matchRedirect(input: {
|
|
pathname: string
|
|
search: URLSearchParams | Record<string, string | string[] | undefined>
|
|
isPlatform: boolean
|
|
maintenanceMode?: boolean
|
|
}): RedirectMatch | null {
|
|
const { pathname, search, isPlatform, maintenanceMode = false } = input
|
|
|
|
// Maintenance mode handled inline — the maintenance-on rule uses a
|
|
// negative-lookahead regex source that the segment matcher above can't
|
|
// parse. Cheap to special-case here.
|
|
if (maintenanceMode) {
|
|
if (!pathname.startsWith('/maintenance') && !pathname.startsWith('/img')) {
|
|
return { destination: '/maintenance', permanent: false }
|
|
}
|
|
} else if (pathname === '/maintenance') {
|
|
return { destination: '/', permanent: false }
|
|
}
|
|
|
|
const ordered = [
|
|
...(isPlatform ? PLATFORM_REDIRECTS : SELF_HOSTED_REDIRECTS),
|
|
...SHARED_REDIRECTS,
|
|
]
|
|
for (const rule of ordered) {
|
|
const params = matchPattern(rule.source, pathname)
|
|
if (!params) continue
|
|
if (!hasQueryMatches(rule.has, search)) continue
|
|
return {
|
|
destination: substituteDestination(rule.destination, params),
|
|
permanent: rule.permanent,
|
|
}
|
|
}
|
|
return null
|
|
}
|