From 9eab4f8fbf22de4c813bd5fc72bd03477fe480fa Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Wed, 24 Jun 2026 17:55:22 +0800 Subject: [PATCH] build(studio): Vite/TanStack-Start build pipeline behind flag (stack 1/6, from #46424) (#47107) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **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 ✓. ## 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. --------- Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com> Co-authored-by: Ivan Vasilov --- .prettierignore | 3 + apps/design-system/package.json | 2 +- apps/docs/package.json | 2 +- apps/studio/.gitignore | 2 + apps/studio/TANSTACK_MIGRATION.md | 514 +++++++++++++++ .../Database/Replication/Destinations.tsx | 12 +- .../Subscription/CancellationFlow.tsx | 2 +- apps/studio/components/ui/AIEditor/index.tsx | 8 +- .../data/utils/deployment-commit-query.ts | 10 +- apps/studio/hooks/use-check-latest-deploy.tsx | 12 +- apps/studio/lib/ai/util.ts | 2 +- apps/studio/lib/hosted-api-allowlist.ts | 37 ++ apps/studio/lib/role-impersonation.ts | 4 +- apps/studio/lib/telemetry.tsx | 2 +- apps/studio/next.config.ts | 447 +------------ apps/studio/package.json | 37 +- apps/studio/proxy.ts | 37 +- apps/studio/redirects.shared.ts | 424 ++++++++++++ apps/studio/scripts/dispatch.js | 64 ++ apps/studio/scripts/lib/env.js | 52 ++ apps/studio/scripts/ratchet-eslint-rules.ts | 2 +- apps/studio/scripts/serve.js | 183 ++++++ apps/studio/scripts/smoke-server.mjs | 73 +++ apps/studio/start.ts | 35 + apps/studio/styles/fonts.css | 68 ++ apps/studio/tsconfig.json | 8 +- apps/studio/turbo.jsonc | 23 +- apps/studio/vercel.ts | 155 +++++ apps/studio/vite.config.ts | 466 ++++++++++++++ apps/ui-library/package.json | 6 +- package.json | 13 +- packages/ui-patterns/package.json | 2 +- .../AnimatedCounter/AnimatedCounter.tsx | 2 +- .../src/components/Typography/Typography.tsx | 2 - pnpm-lock.yaml | 604 ++++++++++++++++-- pnpm-workspace.yaml | 7 + scripts/generateLocalEnv.js | 7 +- turbo.jsonc | 1 + 38 files changed, 2756 insertions(+), 574 deletions(-) create mode 100644 apps/studio/TANSTACK_MIGRATION.md create mode 100644 apps/studio/lib/hosted-api-allowlist.ts create mode 100644 apps/studio/redirects.shared.ts create mode 100644 apps/studio/scripts/dispatch.js create mode 100644 apps/studio/scripts/lib/env.js create mode 100644 apps/studio/scripts/serve.js create mode 100644 apps/studio/scripts/smoke-server.mjs create mode 100644 apps/studio/start.ts create mode 100644 apps/studio/styles/fonts.css create mode 100644 apps/studio/vercel.ts create mode 100644 apps/studio/vite.config.ts diff --git a/.prettierignore b/.prettierignore index 8c41d148ff5..66e4d58fa92 100644 --- a/.prettierignore +++ b/.prettierignore @@ -22,6 +22,9 @@ apps/**/.turbo apps/docs/CONTRIBUTING.md apps/docs/__generated__ apps/design-system/__registry__ +# TanStack Router auto-generated route tree; the file header explicitly +# says to exclude it from formatters. +apps/studio/routeTree.gen.ts packages/icons/__registry__ packages/icons/src/icons/*.ts apps/ui-library/__registry__ diff --git a/apps/design-system/package.json b/apps/design-system/package.json index 5f8269c8af0..089349e2223 100644 --- a/apps/design-system/package.json +++ b/apps/design-system/package.json @@ -18,7 +18,7 @@ }, "dependencies": { "@hookform/resolvers": "^3.1.1", - "@tanstack/react-table": "^8.21.3", + "@tanstack/react-table": "catalog:", "contentlayer2": "0.4.6", "common": "workspace:*", "date-fns": "^2.30.0", diff --git a/apps/docs/package.json b/apps/docs/package.json index bf086e3a985..2dd0afcbd89 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -62,7 +62,7 @@ "@octokit/plugin-retry": "^8.1.0", "@sentry/nextjs": "catalog:", "@supabase/supabase-js": "catalog:", - "@tanstack/react-query": "^5.13.4", + "@tanstack/react-query": "~5.83.0", "acorn": "^8.11.3", "acorn-typescript": "^1.4.13", "ai-commands": "workspace:*", diff --git a/apps/studio/.gitignore b/apps/studio/.gitignore index 39577a49f19..55e8f54894a 100644 --- a/apps/studio/.gitignore +++ b/apps/studio/.gitignore @@ -15,6 +15,8 @@ next-env.d.ts # production /build +/dist +/.output # misc .DS_Store diff --git a/apps/studio/TANSTACK_MIGRATION.md b/apps/studio/TANSTACK_MIGRATION.md new file mode 100644 index 00000000000..2d49fd90c13 --- /dev/null +++ b/apps/studio/TANSTACK_MIGRATION.md @@ -0,0 +1,514 @@ +# TanStack Start migration — route checklist + +Temporary tracking doc. Delete once migration is done. + +**Runtime model — Next.js and TanStack Start run side-by-side** + +Throughout this migration both runtimes coexist in the same workspace: + +- The Next.js pages router (`pages/...`) and the TanStack route tree + (`routes/...`) ship **at the same time**. The Vite/TanStack build is + what we run today; the Next build (`build:next` / `dev:next` scripts + in `apps/studio/package.json`) stays alive as a fallback so we can + bisect regressions and ship either runtime if needed. +- **Do not delete any `apps/studio/pages/...` file during the per-route + migration.** Path A pages re-export their `pages/` default export, so + the Next file is load-bearing for both runtimes. Removing it breaks + the Next build and breaks the TanStack route too. +- Body-moves and `pages/...` deletion happen **only in the final + cleanup pass**, after every route is represented in `routes/...` and + we're ready to retire the Next runtime entirely. That's a separate, + deliberate phase — not something to fold into individual route PRs. +- Same rule for the Next compat shims (`apps/studio/compat/next/`): + they stay until the cleanup pass, regardless of how many routes have + moved. + +**Strategy — minimum-diff re-export** + +The goal is to flip URL ownership to TanStack without rewriting page internals yet. For each page we pick one of two paths: + +- **Path A — re-export from `pages/`** (default). The TanStack route imports the page's default export from `apps/studio/pages/...` and renders it inside a thin wrapper component used as the route's `component`. `getLayout` is dropped on the floor — the TanStack layout chain (pathless `_app.tsx` / `_auth.tsx` + sibling-file layouts) handles wrapping instead. The page's Next-specific imports keep working via the `compat/next/` shim. Because `NextPageWithLayout` declares `{ dehydratedState: any }` as required props, pass `dehydratedState={undefined}` in the wrapper. +- **Path B — direct component import**. When the `pages/...` file is essentially `export default SomeComponent` re-exporting a component from elsewhere (typical for thin page wrappers), skip the middle-man and import `SomeComponent` directly in the TanStack route. + +We still need to land the shared layouts up-front: + +- Pathless layout routes (`_app.tsx`, `_auth.tsx`) hold shared shells without contributing URL segments. +- Sibling-file layouts: `segment.tsx` next to a `segment/` directory provides the layout with `` for children in that directory (e.g. `_app/account.tsx` wraps `_app/account/me.tsx`). No `route.tsx` files. +- Each product layout (DatabaseLayout, AuthLayout, SQLEditorLayout, …) becomes one sibling-file layout. + +Once every page is represented in `routes/`, we do a second pass to properly move the page body into the route file and delete `pages/...`. Path-tracking (A vs B) below tells us which pages still have live Next files we need to eliminate. + +Other rules: + +- New code uses native TanStack APIs directly (no `next/router`, no `next/link`). The Next compat shim stays in place for pages we re-export. +- `withAuth()` HOC → TanStack `beforeLoad` on the containing route/layout. Apply this at shared-layout level where possible. +- **Never** delete a `pages/...` file mid-migration — both runtimes need to keep working. Body-moves and Next-file deletions are reserved for the cleanup pass at the very end, after every entry in this checklist is `[x]`. See "Runtime model" above. +- Not migrated via this list: `pages/api/**` (Next API routes — separate migration), `_app.tsx`, `_document.tsx`, `_error`, `pages/org/_/[[...routeSlug]].tsx`, `pages/project/_/[[...routeSlug]].tsx` (catch-alls — revisit at the end). + +**Legend** + +- `[ ]` not started +- `[~]` in progress +- `[x] A` done — re-exported from `pages/...` (Next file still exists, needs body-move later) +- `[x] A→done` done & body moved — Next file deleted +- `[x] B` done — direct component import, no Next file involved (or Next file already deletable) + +--- + +## Shared layouts + +These are the layout-only TanStack files. Most hold a single product layout component. + +### App shell (pathless) + +- [x] `routes/_app.tsx` — AppLayout + DefaultLayout (reads `defaultLayoutHeaderTitle`/`hideMobileMenu` from leaf `staticData`) +- [x] `routes/_app/account.tsx` — AccountLayout (reads `accountLayoutTitle` from leaf `staticData`) +- [x] `routes/_app/org.tsx` — OrganizationLayout (reads `orgLayoutTitle` from leaf `staticData`). **Delta vs plan:** placed at `_app/org.tsx` (wraps both `/org/` index and `/org/$slug/*`) instead of `_app/org/$slug.tsx`. PageLayout stays inline on `/org/$slug/index.tsx` since only that one route uses it. +- [x] `routes/_app/new.tsx` — skipped; only `_app/new/index.tsx` lives under \_app (inlines WizardLayout). `new/$slug` is top-level (no AppLayout) so a sub-shell would not actually share state. +- [x] `routes/integrations/vercel.tsx` — VercelIntegrationWindowLayout. **Delta vs plan:** placed at top-level rather than under `_app/` — Next getLayout for all three leaves wraps only in VercelIntegrationWindowLayout, no AppLayout/DefaultLayout. + +### Project shell + +- [x] `routes/project/$ref.tsx` — DefaultLayout only. **Delta vs plan:** ProjectLayoutWithAuth omitted from the shell because product layouts (DatabaseLayout, AuthLayout, StorageLayout, …) already render `withAuth(... ProjectLayout ...)` internally — adding it here would double-wrap. The home page (`/project/$ref/index.tsx`) wraps itself in `ProjectLayoutWithAuth` since it has no product layout. +- [x] `routes/project/$ref/database.tsx` — DatabaseLayout (reads `databaseLayoutTitle` from leaf `staticData`) +- [x] `routes/project/$ref/database/triggers.tsx` — sub-shell with `PageLayout` + permission gate + nav items, inlined from `DatabaseTriggersLayout`. **Delta vs plan:** the existing `DatabaseTriggersLayout` component wraps `` internally, so re-using it inside the database.tsx shell would double-wrap. Inlined the inner part instead; the Next-side component is left untouched (still used by the `pages/...` files we re-export). +- [x] `routes/project/$ref/auth.tsx` — AuthLayout (reads `authLayoutTitle` from leaf `staticData`). **Delta vs plan:** shell honours a `skipAuthLayout: true` opt-out in `staticData` for leaves whose own body or sub-layout already wraps in `AuthLayout` (`AuthProvidersLayout`, `AuthEmailsLayout`, `pages/.../auth/third-party.tsx`) — without it those routes would double-wrap (which also doubles `withAuth` + `ProjectLayout`). +- ~~`routes/project/$ref/auth/templates.tsx` — AuthEmailsLayout~~ **Delta vs plan: not landed.** A unified `templates.tsx` sub-shell would force `templates/$templateId.tsx` (which uses plain `AuthLayout`, not `AuthEmailsLayout`) into the wrong wrapping. Instead `templates/index.tsx` and `auth/smtp.tsx` each set `skipAuthLayout: true` and wrap themselves in `AuthEmailsLayout`; `templates/$templateId.tsx` uses the standard auth shell with `authLayoutTitle: 'Emails'`. +- [x] `routes/project/$ref/storage.tsx` — StorageLayout + StorageBucketsLayout (reads `storageLayoutTitle`, optional `skipStorageBucketsLayout`, `storageBucketsLayoutTitle`, `storageBucketsLayoutHideSubtitle` from leaf `staticData`). **Delta vs plan:** the shell wraps in BOTH StorageLayout and StorageBucketsLayout by default — every storage page except bucket-detail pages uses both. Bucket-detail pages set `skipStorageBucketsLayout: true`. `/storage/s3` uses `storageBucketsLayout{Title,HideSubtitle}` to override the inner header. +- [x] `routes/project/$ref/realtime.tsx` — RealtimeLayout (reads `realtimeLayoutTitle` from leaf `staticData`) +- [x] `routes/project/$ref/functions.tsx` — EdgeFunctionsLayout (reads `functionsLayoutTitle` from leaf `staticData`). Honours `skipFunctionsLayout: true` opt-out for the `$functionSlug` subtree, whose `EdgeFunctionDetailsLayout` already wraps `EdgeFunctionsLayout` internally — same pattern as auth.tsx. Sub-shell at `routes/project/$ref/functions/$functionSlug.tsx` provides `EdgeFunctionDetailsLayout` for all 5 slug leaves (reads `edgeFunctionDetailsTitle` from leaf staticData). +- [x] `routes/project/$ref/branches.tsx` — BranchLayout only. **Delta vs plan:** the per-page `PageLayout` (with different titles + primary/secondary actions) stays in each leaf. Hoisted `BranchesPageWrapper` and `MergeRequestsPageWrapper` to top-level exports in their respective `pages/...` files so the route files can import + re-use the same wrapping. +- [x] `routes/project/$ref/logs.tsx` — LogsLayout (reads `logsLayoutTitle` from leaf staticData). Honours `skipLogsLayout: true` for `logs/index` (page handles its own ProjectLayout-wrapped content for the UnifiedLogs / no-permission cases). Refactored `pages/.../logs/index.tsx` to move the inline `` into `getLayout` so it isn't duplicated when the TanStack project shell already provides DefaultLayout. +- [x] `routes/project/$ref/observability.tsx` — ObservabilityLayout (reads `observabilityLayoutTitle` from leaf staticData) +- [x] `routes/project/$ref/advisors.tsx` — AdvisorsLayout (reads `advisorsLayoutTitle` from leaf staticData). Honours `skipAdvisorsLayout: true` opt-out for the rules sub-shell, which provides its own AdvisorsLayout-less-DefaultLayout wrap. Scans whole match chain (same pattern as functions.tsx). +- [x] `routes/project/$ref/advisors/rules.tsx` — sub-shell that inlines the inner body of `AdvisorRulesLayout` (AdvisorsLayout + PageLayout with title/tabs/feature-preview badge), minus the outer DefaultLayout (already provided by the parent project shell). Sets `skipAdvisorsLayout: true` on its own staticData. **Delta vs plan:** the existing `AdvisorRulesLayout` component wraps in DefaultLayout + AdvisorsLayout internally, so reusing it as-is would double-wrap both. Inlined the inner part; the Next-side component is untouched. +- [x] `routes/project/$ref/settings.tsx` — SettingsLayout (reads `settingsLayoutTitle` from leaf staticData). Honours `skipSettingsLayout: true` for `settings/api` (redirect-only page). Adds a sub-shell at `routes/project/$ref/settings/api-keys.tsx` providing `ApiKeysLayout` for both api-keys leaves; `jwt/index` wraps in `JWTKeysLayout` inline since `jwt/legacy` doesn't share it. +- [x] `routes/project/$ref/integrations.tsx` — ProjectIntegrationsLayout (no staticData; all 4 leaves share identical layout). Layout is `withAuth(({ children }) => {children})`, so the shell just wraps `` once. +- [x] `routes/project/$ref/sql.tsx` — EditorBaseLayout + SQLEditorLayout. Twin of editor.tsx; all four leaves share identical layout props so the shell hardcodes them (no `staticData` overrides). EditorBaseLayout wraps in ProjectLayoutWithAuth; SQLEditorLayout adds its own `withAuth` HOC but no extra ProjectLayout — same shape as the table editor (auth check runs twice but no double render). +- [x] `routes/project/$ref/editor.tsx` — EditorBaseLayout + TableEditorLayout. All three leaves share identical layout props so the shell hardcodes them (no `staticData` overrides). EditorBaseLayout wraps in `ProjectLayoutWithAuth` internally; TableEditorLayout's happy path is just a fragment + side-effect (banner) and only wraps in `ProjectLayoutWithAuth` on its no-permission branch — same as Next, no double-wrap in normal use. + +### Auth shell (pathless) + +- [x] `routes/_auth.tsx` — AuthenticationLayout + +--- + +## Pages + +### App shell — `/account/*` + +- [x] A `routes/_app/account/me.tsx` ← `pages/account/me.tsx` +- [x] A `routes/_app/account/security.tsx` ← `pages/account/security.tsx` +- [x] A `routes/_app/account/audit.tsx` ← `pages/account/audit.tsx` +- [x] A `routes/_app/account/tokens/index.tsx` ← `pages/account/tokens.tsx` +- [x] A `routes/_app/account/tokens/scoped.tsx` ← `pages/account/tokens/scoped.tsx` + +### App shell — `/org/$slug/*` + +- [x] A `routes/_app/org/$slug/index.tsx` ← `pages/org/[slug]/index.tsx` +- [x] A `routes/_app/org/$slug/apps.tsx` ← `pages/org/[slug]/apps.tsx` +- [x] A `routes/_app/org/$slug/audit.tsx` ← `pages/org/[slug]/audit.tsx` +- [x] A `routes/_app/org/$slug/billing.tsx` ← `pages/org/[slug]/billing.tsx` +- [x] A `routes/_app/org/$slug/documents.tsx` ← `pages/org/[slug]/documents.tsx` +- [x] A `routes/_app/org/$slug/general.tsx` ← `pages/org/[slug]/general.tsx` +- [x] A `routes/_app/org/$slug/integrations.tsx` ← `pages/org/[slug]/integrations.tsx` +- [x] A `routes/_app/org/$slug/security.tsx` ← `pages/org/[slug]/security.tsx` +- [x] A `routes/_app/org/$slug/sso.tsx` ← `pages/org/[slug]/sso.tsx` +- [x] A `routes/_app/org/$slug/team.tsx` ← `pages/org/[slug]/team.tsx` +- [x] A `routes/_app/org/$slug/usage.tsx` ← `pages/org/[slug]/usage.tsx` +- [x] A `routes/_app/org/$slug/private-apps/index.tsx` ← `pages/org/[slug]/private-apps/index.tsx` +- [x] A `routes/_app/org/$slug/webhooks/index.tsx` ← `pages/org/[slug]/webhooks/index.tsx` +- [x] A `routes/_app/org/$slug/webhooks/$endpointId.tsx` ← `pages/org/[slug]/webhooks/[endpointId].tsx` +- [x] A `routes/_app/org/index.tsx` ← `pages/org/index.tsx` (redirect) + +### App shell — top-level pages + +- [x] A `routes/_app/organizations.tsx` ← `pages/organizations.tsx` (page default already withAuth-wrapped; PageLayout wraps body) +- [x] `routes/_app/new/index.tsx` ← `pages/new/index.tsx` (inlines WizardLayout; sets `defaultLayoutHeaderTitle: 'New organization'` + `hideMobileMenu: true` on staticData). **Delta vs plan:** no `_app/new.tsx` sub-shell — `new/$slug` doesn't fit under \_app and uses a different inner wrapper (PageLayout), so a shared shell wouldn't share anything. +- [x] A `routes/new/$slug.tsx` ← `pages/new/[slug].tsx` **Delta vs plan:** placed at top-level rather than under `_app/` — Next getLayout omits AppLayout and uses PageLayout (not WizardLayout) inside DefaultLayout, so leaf inlines the full DefaultLayout + PageLayout wrap itself. +- [x] A `routes/aws-marketplace-onboarding.tsx` ← `pages/aws-marketplace-onboarding.tsx` **Delta vs plan:** placed at root rather than under `_app/` — page uses its own `LinkAwsMarketplaceLayout` and doesn't want `AppLayout` + `DefaultLayout` wrapping. +- [x] A `routes/claim-project.tsx` ← `pages/claim-project.tsx` **Delta vs plan:** placed at root rather than under `_app/` — page uses its own `` + `
` layout and doesn't want `AppLayout` + `DefaultLayout` wrapping. +- [x] A `routes/join.tsx` ← `pages/join.tsx` **Delta vs plan:** placed at root rather than under `_app/` — page uses a centered-div layout and doesn't want `AppLayout` + `DefaultLayout` wrapping. +- [x] `routes/_app/support/new.tsx` ← `pages/support/new.tsx` (sets `hideMobileMenu: true` staticData; existing page is `withAuth`-wrapped so no beforeLoad migration needed yet) +- [x] `routes/_app/support/link.tsx` ← `pages/support/link.tsx` + +### App shell — integrations + +- [x] A `routes/integrations/vercel/install.tsx` ← `pages/integrations/vercel/install.tsx` +- [x] A `routes/integrations/vercel/$slug/marketplace/choose-project.tsx` ← `pages/integrations/vercel/[slug]/marketplace/choose-project.tsx` +- [x] A `routes/integrations/vercel/$slug/deploy-button/new-project.tsx` ← `pages/integrations/vercel/[slug]/deploy-button/new-project.tsx` +- [x] A `routes/integrations/github/authorize.tsx` ← `pages/integrations/github/authorize.tsx` **Delta vs plan:** placed at top-level rather than under `_app/` — Next page has no getLayout (renders bare), so adding AppLayout/DefaultLayout via \_app would be a behaviour change. + +### Project shell — home + +- [x] A `routes/project/$ref/index.tsx` ← `pages/project/[ref]/index.tsx` (route wraps in `ProjectLayoutWithAuth` itself — see shell delta above) +- [x] `routes/project/$ref/merge.tsx` ← `pages/project/[ref]/merge.tsx` (leaf wraps body in `ProjectLayoutWithAuth`; parent `project/$ref.tsx` shell provides DefaultLayout) + +### Project shell — `/api/*` + +- [x] `routes/project/$ref/api/index.tsx` ← `pages/project/[ref]/api/index.tsx` (redirect-only page; no extra wrap needed beyond the parent DefaultLayout shell) + +### Project shell — `/database/*` + +- [x] A `routes/project/$ref/database/schemas.tsx` ← `pages/project/[ref]/database/schemas.tsx` +- [x] A `routes/project/$ref/database/extensions.tsx` ← `pages/project/[ref]/database/extensions.tsx` +- [x] A `routes/project/$ref/database/functions.tsx` ← `pages/project/[ref]/database/functions.tsx` +- [x] A `routes/project/$ref/database/indexes.tsx` ← `pages/project/[ref]/database/indexes.tsx` +- [x] A `routes/project/$ref/database/migrations.tsx` ← `pages/project/[ref]/database/migrations.tsx` +- [x] A `routes/project/$ref/database/roles.tsx` ← `pages/project/[ref]/database/roles.tsx` +- [x] A `routes/project/$ref/database/settings.tsx` ← `pages/project/[ref]/database/settings.tsx` +- [x] A `routes/project/$ref/database/types.tsx` ← `pages/project/[ref]/database/types.tsx` +- [x] A `routes/project/$ref/database/column-privileges.tsx` ← `pages/project/[ref]/database/column-privileges.tsx` +- [x] A `routes/project/$ref/database/tables/index.tsx` ← `pages/project/[ref]/database/tables/index.tsx` +- [x] A `routes/project/$ref/database/tables/$id.tsx` ← `pages/project/[ref]/database/tables/[id].tsx` +- [x] A `routes/project/$ref/database/publications/index.tsx` ← `pages/project/[ref]/database/publications/index.tsx` +- [x] A `routes/project/$ref/database/publications/$id.tsx` ← `pages/project/[ref]/database/publications/[id].tsx` +- [x] A `routes/project/$ref/database/replication/index.tsx` ← `pages/project/[ref]/database/replication/index.tsx` +- [x] A `routes/project/$ref/database/replication/$pipelineId.tsx` ← `pages/project/[ref]/database/replication/[pipelineId].tsx` +- [x] A `routes/project/$ref/database/replication/replica/$replicaId.tsx` ← `pages/project/[ref]/database/replication/replica/[replicaId].tsx` +- [x] A `routes/project/$ref/database/triggers/index.tsx` ← `pages/project/[ref]/database/triggers/index.tsx` +- [x] A `routes/project/$ref/database/triggers/data.tsx` ← `pages/project/[ref]/database/triggers/data.tsx` (sub-shell at `database/triggers.tsx` provides PageLayout + nav, parent shell provides DatabaseLayout) +- [x] A `routes/project/$ref/database/triggers/event.tsx` ← `pages/project/[ref]/database/triggers/event.tsx` (same as data) +- [x] A `routes/project/$ref/database/backups/pitr.tsx` ← `pages/project/[ref]/database/backups/pitr.tsx` +- [x] A `routes/project/$ref/database/backups/restore-to-new-project.tsx` ← `pages/project/[ref]/database/backups/restore-to-new-project.tsx` +- [x] A `routes/project/$ref/database/backups/scheduled.tsx` ← `pages/project/[ref]/database/backups/scheduled.tsx` + +### Project shell — `/auth/*` + +- [x] A `routes/project/$ref/auth/overview.tsx` ← `pages/project/[ref]/auth/overview.tsx` +- [x] A `routes/project/$ref/auth/users.tsx` ← `pages/project/[ref]/auth/users.tsx` +- [x] A `routes/project/$ref/auth/policies.tsx` ← `pages/project/[ref]/auth/policies.tsx` +- [x] A `routes/project/$ref/auth/providers.tsx` ← `pages/project/[ref]/auth/providers.tsx` (sets `skipAuthLayout: true`, wraps in `AuthProvidersLayout` directly) +- [x] A `routes/project/$ref/auth/mfa.tsx` ← `pages/project/[ref]/auth/mfa.tsx` +- [x] A `routes/project/$ref/auth/hooks.tsx` ← `pages/project/[ref]/auth/hooks.tsx` +- [x] A `routes/project/$ref/auth/smtp.tsx` ← `pages/project/[ref]/auth/smtp.tsx` (sets `skipAuthLayout: true`, wraps in `AuthEmailsLayout` directly) +- [x] A `routes/project/$ref/auth/sessions.tsx` ← `pages/project/[ref]/auth/sessions.tsx` +- [x] A `routes/project/$ref/auth/passkeys.tsx` ← `pages/project/[ref]/auth/passkeys.tsx` +- [x] A `routes/project/$ref/auth/performance.tsx` ← `pages/project/[ref]/auth/performance.tsx` +- [x] A `routes/project/$ref/auth/protection.tsx` ← `pages/project/[ref]/auth/protection.tsx` +- [x] A `routes/project/$ref/auth/rate-limits.tsx` ← `pages/project/[ref]/auth/rate-limits.tsx` +- [x] A `routes/project/$ref/auth/third-party.tsx` ← `pages/project/[ref]/auth/third-party.tsx` (sets `skipAuthLayout: true` — page body inlines `` which already wraps ``) +- [x] A `routes/project/$ref/auth/oauth-apps.tsx` ← `pages/project/[ref]/auth/oauth-apps.tsx` +- [x] A `routes/project/$ref/auth/oauth-server.tsx` ← `pages/project/[ref]/auth/oauth-server.tsx` +- [x] A `routes/project/$ref/auth/url-configuration.tsx` ← `pages/project/[ref]/auth/url-configuration.tsx` +- [x] A `routes/project/$ref/auth/audit-logs.tsx` ← `pages/project/[ref]/auth/audit-logs.tsx` +- [x] A `routes/project/$ref/auth/templates/index.tsx` ← `pages/project/[ref]/auth/templates/index.tsx` (sets `skipAuthLayout: true`, wraps in `AuthEmailsLayout` directly) +- [x] A `routes/project/$ref/auth/templates/$templateId.tsx` ← `pages/project/[ref]/auth/templates/[templateId].tsx` (`authLayoutTitle: 'Emails'` — page uses plain `AuthLayout`, not `AuthEmailsLayout`) + +### Project shell — `/storage/*` + +- [x] A `routes/project/$ref/storage/s3.tsx` ← `pages/project/[ref]/storage/s3.tsx` +- [x] A `routes/project/$ref/storage/files/index.tsx` ← `pages/project/[ref]/storage/files/index.tsx` +- [x] A `routes/project/$ref/storage/files/policies.tsx` ← `pages/project/[ref]/storage/files/policies.tsx` +- [x] A `routes/project/$ref/storage/files/settings.tsx` ← `pages/project/[ref]/storage/files/settings.tsx` +- [x] A `routes/project/$ref/storage/files/buckets/$bucketId.tsx` ← `pages/project/[ref]/storage/files/buckets/[bucketId].tsx` (sets `skipStorageBucketsLayout: true`) +- [x] A `routes/project/$ref/storage/analytics/index.tsx` ← `pages/project/[ref]/storage/analytics/index.tsx` +- [x] A `routes/project/$ref/storage/analytics/buckets/$bucketId.tsx` ← `pages/project/[ref]/storage/analytics/buckets/[bucketId].tsx` (sets `skipStorageBucketsLayout: true`) +- [x] A `routes/project/$ref/storage/vectors/index.tsx` ← `pages/project/[ref]/storage/vectors/index.tsx` +- [x] A `routes/project/$ref/storage/vectors/buckets/$bucketId.tsx` ← `pages/project/[ref]/storage/vectors/buckets/[bucketId].tsx` (sets `skipStorageBucketsLayout: true`) + +### Project shell — `/realtime/*` + +- [x] A `routes/project/$ref/realtime/inspector.tsx` ← `pages/project/[ref]/realtime/inspector.tsx` +- [x] A `routes/project/$ref/realtime/policies.tsx` ← `pages/project/[ref]/realtime/policies.tsx` +- [x] A `routes/project/$ref/realtime/settings.tsx` ← `pages/project/[ref]/realtime/settings.tsx` + +### Project shell — `/functions/*` + +- [x] A `routes/project/$ref/functions/index.tsx` ← `pages/project/[ref]/functions/index.tsx` (route wraps in exported `EdgeFunctionsIndexPageWrapper` for the inline PageHeader + actions) +- [x] A `routes/project/$ref/functions/new.tsx` ← `pages/project/[ref]/functions/new.tsx` +- [x] A `routes/project/$ref/functions/secrets.tsx` ← `pages/project/[ref]/functions/secrets.tsx` (route wraps in exported `SecretsPageWrapper`) +- [x] A `routes/project/$ref/functions/$functionSlug/index.tsx` ← `pages/project/[ref]/functions/[functionSlug]/index.tsx` +- [x] A `routes/project/$ref/functions/$functionSlug/code.tsx` ← `pages/project/[ref]/functions/[functionSlug]/code.tsx` +- [x] A `routes/project/$ref/functions/$functionSlug/details.tsx` ← `pages/project/[ref]/functions/[functionSlug]/details.tsx` +- [x] A `routes/project/$ref/functions/$functionSlug/invocations.tsx` ← `pages/project/[ref]/functions/[functionSlug]/invocations.tsx` +- [x] A `routes/project/$ref/functions/$functionSlug/logs.tsx` ← `pages/project/[ref]/functions/[functionSlug]/logs.tsx` + +### Project shell — `/branches/*` + +- [x] A `routes/project/$ref/branches/index.tsx` ← `pages/project/[ref]/branches/index.tsx` (route wraps in exported `BranchesPageWrapper` to preserve the page's `PageLayout` + Create-branch action) +- [x] A `routes/project/$ref/branches/merge-requests.tsx` ← `pages/project/[ref]/branches/merge-requests.tsx` (route wraps in exported `MergeRequestsPageWrapper`) + +### Project shell — `/logs/*` + +- [x] A `routes/project/$ref/logs/index.tsx` ← `pages/project/[ref]/logs/index.tsx` (sets `skipLogsLayout: true`; page handles its own ProjectLayout; DefaultLayout moved to page's `getLayout` so Next still wraps it) +- [x] A `routes/project/$ref/logs/auth-logs.tsx` ← `pages/project/[ref]/logs/auth-logs.tsx` +- [x] A `routes/project/$ref/logs/cron-logs.tsx` ← `pages/project/[ref]/logs/cron-logs.tsx` +- [x] A `routes/project/$ref/logs/dedicated-pooler-logs.tsx` ← `pages/project/[ref]/logs/dedicated-pooler-logs.tsx` +- [x] A `routes/project/$ref/logs/edge-functions-logs.tsx` ← `pages/project/[ref]/logs/edge-functions-logs.tsx` +- [x] A `routes/project/$ref/logs/edge-logs.tsx` ← `pages/project/[ref]/logs/edge-logs.tsx` +- [x] A `routes/project/$ref/logs/pg-upgrade-logs.tsx` ← `pages/project/[ref]/logs/pg-upgrade-logs.tsx` +- [x] A `routes/project/$ref/logs/pgcron-logs.tsx` ← `pages/project/[ref]/logs/pgcron-logs.tsx` +- [x] A `routes/project/$ref/logs/pooler-logs.tsx` ← `pages/project/[ref]/logs/pooler-logs.tsx` +- [x] A `routes/project/$ref/logs/postgres-logs.tsx` ← `pages/project/[ref]/logs/postgres-logs.tsx` +- [x] A `routes/project/$ref/logs/postgrest-logs.tsx` ← `pages/project/[ref]/logs/postgrest-logs.tsx` +- [x] A `routes/project/$ref/logs/realtime-logs.tsx` ← `pages/project/[ref]/logs/realtime-logs.tsx` +- [x] A `routes/project/$ref/logs/replication-logs.tsx` ← `pages/project/[ref]/logs/replication-logs.tsx` +- [x] A `routes/project/$ref/logs/storage-logs.tsx` ← `pages/project/[ref]/logs/storage-logs.tsx` +- [x] A `routes/project/$ref/logs/explorer/index.tsx` ← `pages/project/[ref]/logs/explorer/index.tsx` +- [x] A `routes/project/$ref/logs/explorer/recent.tsx` ← `pages/project/[ref]/logs/explorer/recent.tsx` +- [x] A `routes/project/$ref/logs/explorer/saved.tsx` ← `pages/project/[ref]/logs/explorer/saved.tsx` +- [x] A `routes/project/$ref/logs/explorer/templates.tsx` ← `pages/project/[ref]/logs/explorer/templates.tsx` + +### Project shell — `/observability/*` + +- [x] A `routes/project/$ref/observability/index.tsx` ← `pages/project/[ref]/observability/index.tsx` +- [x] A `routes/project/$ref/observability/$id.tsx` ← `pages/project/[ref]/observability/[id].tsx` +- [x] A `routes/project/$ref/observability/auth.tsx` ← `pages/project/[ref]/observability/auth.tsx` +- [x] A `routes/project/$ref/observability/database.tsx` ← `pages/project/[ref]/observability/database.tsx` +- [x] A `routes/project/$ref/observability/api-overview.tsx` ← `pages/project/[ref]/observability/api-overview.tsx` +- [x] A `routes/project/$ref/observability/edge-functions.tsx` ← `pages/project/[ref]/observability/edge-functions.tsx` +- [x] A `routes/project/$ref/observability/postgrest.tsx` ← `pages/project/[ref]/observability/postgrest.tsx` +- [x] A `routes/project/$ref/observability/query-insights.tsx` ← `pages/project/[ref]/observability/query-insights.tsx` +- [x] A `routes/project/$ref/observability/query-performance.tsx` ← `pages/project/[ref]/observability/query-performance.tsx` +- [x] A `routes/project/$ref/observability/realtime.tsx` ← `pages/project/[ref]/observability/realtime.tsx` +- [x] A `routes/project/$ref/observability/storage.tsx` ← `pages/project/[ref]/observability/storage.tsx` + +### Project shell — `/advisors/*` + +- [x] A `routes/project/$ref/advisors/performance.tsx` ← `pages/project/[ref]/advisors/performance.tsx` +- [x] A `routes/project/$ref/advisors/security.tsx` ← `pages/project/[ref]/advisors/security.tsx` +- [x] A `routes/project/$ref/advisors/rules/performance.tsx` ← `pages/project/[ref]/advisors/rules/performance.tsx` +- [x] A `routes/project/$ref/advisors/rules/security.tsx` ← `pages/project/[ref]/advisors/rules/security.tsx` + +### Project shell — `/settings/*` + +- [x] A `routes/project/$ref/settings/general.tsx` ← `pages/project/[ref]/settings/general.tsx` +- [x] A `routes/project/$ref/settings/addons.tsx` ← `pages/project/[ref]/settings/addons.tsx` +- [x] A `routes/project/$ref/settings/api.tsx` ← `pages/project/[ref]/settings/api.tsx` (sets `skipSettingsLayout: true` — page is a useEffect redirect) +- [x] A `routes/project/$ref/settings/compute-and-disk.tsx` ← `pages/project/[ref]/settings/compute-and-disk.tsx` +- [x] A `routes/project/$ref/settings/dashboard.tsx` ← `pages/project/[ref]/settings/dashboard.tsx` +- [x] A `routes/project/$ref/settings/infrastructure.tsx` ← `pages/project/[ref]/settings/infrastructure.tsx` +- [x] A `routes/project/$ref/settings/integrations.tsx` ← `pages/project/[ref]/settings/integrations.tsx` +- [x] A `routes/project/$ref/settings/log-drains.tsx` ← `pages/project/[ref]/settings/log-drains.tsx` +- [x] A `routes/project/$ref/settings/api-keys/index.tsx` ← `pages/project/[ref]/settings/api-keys/index.tsx` (under `api-keys.tsx` sub-shell with ApiKeysLayout) +- [x] A `routes/project/$ref/settings/api-keys/legacy.tsx` ← `pages/project/[ref]/settings/api-keys/legacy.tsx` (under `api-keys.tsx` sub-shell) +- [x] A `routes/project/$ref/settings/billing/usage.tsx` ← `pages/project/[ref]/settings/billing/usage.tsx` +- [x] A `routes/project/$ref/settings/jwt/index.tsx` ← `pages/project/[ref]/settings/jwt/index.tsx` (wraps in JWTKeysLayout inline) +- [x] A `routes/project/$ref/settings/jwt/legacy.tsx` ← `pages/project/[ref]/settings/jwt/legacy.tsx` +- [x] A `routes/project/$ref/settings/webhooks/index.tsx` ← `pages/project/[ref]/settings/webhooks/index.tsx` +- [x] A `routes/project/$ref/settings/webhooks/$endpointId.tsx` ← `pages/project/[ref]/settings/webhooks/[endpointId].tsx` + +### Project shell — `/integrations/*` + +- [x] `routes/project/$ref/integrations/index.tsx` ← `pages/project/[ref]/integrations/index.tsx` +- [x] `routes/project/$ref/integrations/$id/index.tsx` ← `pages/project/[ref]/integrations/[id]/index.tsx` +- [x] `routes/project/$ref/integrations/$id/$pageId/index.tsx` ← `pages/project/[ref]/integrations/[id]/[pageId]/index.tsx` +- [x] `routes/project/$ref/integrations/$id/$pageId/$childId/index.tsx` ← `pages/project/[ref]/integrations/[id]/[pageId]/[childId]/index.tsx` + +### Project shell — `/sql/*` + +- [x] A `routes/project/$ref/sql/index.tsx` ← `pages/project/[ref]/sql/index.tsx` +- [x] A `routes/project/$ref/sql/$id.tsx` ← `pages/project/[ref]/sql/[id].tsx` +- [x] A `routes/project/$ref/sql/templates.tsx` ← `pages/project/[ref]/sql/templates.tsx` +- [x] A `routes/project/$ref/sql/quickstarts.tsx` ← `pages/project/[ref]/sql/quickstarts.tsx` + +### Project shell — `/editor/*` + +- [x] A `routes/project/$ref/editor/index.tsx` ← `pages/project/[ref]/editor/index.tsx` +- [x] A `routes/project/$ref/editor/$id.tsx` ← `pages/project/[ref]/editor/[id].tsx` +- [x] A `routes/project/$ref/editor/new.tsx` ← `pages/project/[ref]/editor/new.tsx` + +### Auth shell — `/sign-in`, `/sign-up`, etc. + +- [x] A `routes/_auth/sign-in.tsx` ← `pages/sign-in.tsx` +- [x] A `routes/_auth/sign-up.tsx` ← `pages/sign-up.tsx` +- [x] A `routes/_auth/sign-in-sso.tsx` ← `pages/sign-in-sso.tsx` +- [x] A `routes/_auth/sign-in-partner.tsx` ← `pages/sign-in-partner.tsx` +- [x] A `routes/_auth/sign-in-mfa.tsx` ← `pages/sign-in-mfa.tsx` (page inlines SignInLayout) +- [x] A `routes/_auth/forgot-password.tsx` ← `pages/forgot-password.tsx` +- [x] A `routes/_auth/forgot-password-mfa.tsx` ← `pages/forgot-password-mfa.tsx` (page inlines ForgotPasswordLayout) +- [x] A `routes/_auth/reset-password.tsx` ← `pages/reset-password.tsx` (page default already withAuth-wrapped) +- [x] A `routes/_auth/cli/login.tsx` ← `pages/cli/login.tsx` (page inlines APIAuthorizationLayout, withAuth) +- [x] A `routes/_auth/partners/stripe/projects/login.tsx` ← `pages/partners/stripe/projects/login.tsx` (page inlines APIAuthorizationLayout, withAuth) + +### Standalone (no shared shell) + +- [x] B `routes/index.tsx` — redirect-only root route. Mirrors the Next.js `redirects()` rules in `next.config.ts`: platform sends users to `/org` (or `/new/new-project` when deep-linked with `?next=new-project`), self-hosted sends them to `/project/default`. **Follow-up:** the redirect targets currently use `href` (full reload) because they were on the Next side when this was written; switch to `to` now that all of them live in the TanStack tree. +- [x] A `routes/authorize.tsx` ← `pages/authorize.tsx` (APIAuthorizationLayout) +- [x] A `routes/redeem.tsx` ← `pages/redeem.tsx` (RedeemCreditsLayout) +- [x] A `routes/logout.tsx` ← `pages/logout.tsx` +- [x] A `routes/maintenance.tsx` ← `pages/maintenance.tsx` + +### Error pages (handled at root) + +- [x] A `__root.tsx` — wired `notFoundComponent` to `pages/404.tsx` +- [x] `__root.tsx` — wired `errorComponent` to `pages/500.tsx`. Mirrors the in-tree `react-error-boundary` Sentry capture (`scope.setTag('routerErrorComponent', true)`) so router-level errors (loader/component-render failures before the in-tree boundary mounts) still report. `pages/_error.jsx` stays load-bearing under Next but isn't reached at runtime under TanStack — it's the pages-router catch-all that has no TanStack equivalent. + +--- + +## API routes + +**Strategy — shim + re-export.** `compat/next/api.ts` exposes +`toWebHandler(nextHandler)` that adapts a `(req, res) => …` Next.js handler +into a TanStack Start Web-fetch handler. Each `routes/api/...` file imports +the default export from `pages/api/...`, wraps with `toWebHandler`, and +registers via `createFileRoute(...).server.handlers`. `apiWrapper` and +`apiAuthenticate` stay untouched — they run inside the shim, seeing a +NextApiRequest-shaped `req` and a proxy `res`. + +**Path conventions** — `pages/api/foo/[bar]/baz.ts` → `routes/api/foo/$bar/baz.ts`; +`pages/api/foo/[[...slug]].ts` → `routes/api/foo/$.ts`. + +**Shim coverage.** The proxy `req` / `res` cover both the buffered and +streaming patterns that pages-router handlers use: + +- **Buffered responses** — `res.status`/`setHeader`/`json`/`send`/`write`/ + `end` accumulate into a single `Response` body when the handler returns. +- **Streaming responses** — `res.writeHead(status, headers?)` (or + `res.flushHeaders()`) flips the proxy into streaming mode: a Web + `ReadableStream` opens, buffered chunks flush into it, subsequent + `res.write(chunk)` enqueues live, `res.end()` closes it. `finalize()` + returns the `Response` while the handler keeps pushing chunks. This is + what makes `result.pipeUIMessageStreamToResponse(res, …)` (AI SDK) + stream token-by-token to the browser. +- **Client abort** — Web `Request.signal` is plumbed through as + `req.on('close' | 'aborted', …)`. AI handlers that wire + `abortController.abort()` off those events keep working. +- **EventEmitter surface** — `req.on`/`once`/`off`/`emit` (events `close` + / `aborted` are real; other names accepted but no-op). `res.on`/etc. + are no-op stubs so pipe helpers attaching `drain`/`close`/`error` + listeners don't crash. +- **Body parsing** — JSON and `application/x-www-form-urlencoded` parsed + to `req.body`; everything else is the raw text. Multipart inbound is + not implemented — no studio handler reads multipart in. + +Two routes still bypass the shim because they're easier to write +Web-natively from scratch: + +- `routes/api/v1/projects/$ref/functions/$slug/body.ts` — multipart + streaming OUT (artifact download). Builds the `Response` body as a + `ReadableStream`; each artifact file converts via + `Readable.toWeb(createReadStream(...))` and pulls chunk-by-chunk into + the stream. +- `routes/api/mcp/index.ts` — uses MCP SDK's + `WebStandardStreamableHTTPServerTransport` (`handleRequest(request)` + returns a `Response` directly). +- `pages/api/ai/docs.ts` was already edge-runtime / Web-Response native + — direct re-export, no shim involved. + +Tracking below is coarse — each bullet is a `pages/api/**` subtree. Check off +once every file in the subtree has a `routes/api/**` counterpart. Expand into +per-file items only when a subtree has special cases. + +- [x] `routes/api/get-ip-address.ts` — canary port (validates the shim) +- [x] `routes/api/**` — root-level simple endpoints (`check-cname`, + `cli-release-version`, `enabled-features-overrides`, `generate-attachment-url`, + `get-deployment-commit`, `get-utc-time`, `status-override`) +- [x] `routes/api/ai/**` — AI endpoints (`docs.ts` direct-ported as Web-native) +- [x] `routes/api/connect/**` +- [x] `routes/api/content/**` +- [x] `routes/api/edge-functions/**` +- [x] `routes/api/integrations/**` +- [x] `routes/api/platform/**` (60 files, scripted port) +- [x] `routes/api/v1/**` — except `body.ts` (streaming rewrite) +- [x] `routes/api/v1/projects/$ref/functions/$slug/body.ts` — Web-streams rewrite. Returns a `Response` whose body is a `ReadableStream`; each artifact file is converted via `Readable.toWeb(createReadStream(...))` and pulled chunk-by-chunk into the multipart stream. Skips the `apiWrapper` since `getFunctionsArtifactStore` already asserts self-hosted mode (the pages-router `withAuth` was a no-op outside `IS_PLATFORM`). +- [x] `routes/api/mcp/index.ts` — uses `WebStandardStreamableHTTPServerTransport` from `@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js`. Takes a Web `Request`, returns a `Response` directly — no shim needed. Query parsing pulled from `request.url`'s search params; headers passed through unchanged. +- [x] `routes/api/incident-banner.ts`, `routes/api/incident-status.ts` — App + Router routes under `app/api/**` (already Web-native, direct re-export) + +--- + +## Compat shim surface (`compat/next/`) + +The Next compat shims stay alive as long as any `pages/...` file is +load-bearing. Listed here so the cleanup PR knows what to delete / +inline. + +- `router.ts` — `useRouter()` for hook callers (TanStack `useRouter` + + `useLocation` + `useMatches` + `useParams` + `useSearch` glued + together), plus a `default` export (`SingletonRouter` shape) for the + one module-scope `import router from 'next/router'` consumer + (Support/DiscordCTACard) that reads `router.basePath` outside React. + `router.pathname` strips the trailing slash TanStack appends to index + routes (without it, `router.pathname.split('/')[3]` returns `''` + instead of `undefined` for index pages and the project sidebar's + active-route check breaks). +- `_router-events.ts` — adapts `router.events.on(event, handler)` onto + `router.subscribe(tsEvent, …)`. Forwards Next's `(url, { shallow })` + args. Maps `routeChangeStart` / `routeChangeComplete` / + `beforeHistoryChange` / `hashChangeStart` / `hashChangeComplete`. + **Known gap:** Next's throw-from-`routeChangeStart`-to-cancel pattern + isn't supportable — `subscribe` is fire-and-forget. + `usePreventNavigationOnUnsavedChanges` relies on it and needs + migrating to TanStack's `useBlocker` separately. +- `api.ts` — `toWebHandler(nextHandler)`. See **API routes → Shim + coverage** above. +- `link.tsx`, `navigation.ts`, `dynamic.tsx`, `image.tsx`, + `legacy/image.tsx`, `script.tsx`, `head.tsx`, `server.ts` — comprehensive + drop-in replacements for the `next/*` modules studio imports. All + bundled via `vite.config.ts`'s `nextCompat()` plugin (alias) + + `ssr.noExternal: [/^next(\/|$)/]` so the shims always win over the + real Next packages. + +--- + +## Build / bundler workarounds + +`vite.config.ts` carries two classes of build-time guard that exist +purely because of how Rolldown chunks our specific dependency graph. +They should be revisited (and ideally lifted) once the migration is +done. + +### `manualChunks` pins + +Pin shared library code into dedicated chunks so per-component chunks +can't import from a chunk that (transitively) imports them back — +chunk-level cycles surface in the browser as +`TypeError: is not a function` at module-load time. + +- `class-variance-authority` — entry #1 in CIRCULAR_IMPORTS.md. +- `lucide-react` — keeps Lucide icons from being per-icon-split into + chunks that import `createLucideIcon` back from the `ui` chunk + (`folder-open-.js` was the canary). +- `react-vendor` (react + react-dom + scheduler + jsx-runtime) — pinned + before `lucide-react` so Rolldown doesn't suck React into the + lucide chunk for CJS interop and shift live-bindings across the rest + of the graph (`Alert-.js` was the canary). + +All three are documented in `CIRCULAR_IMPORTS.md` — slated for a +follow-up structural fix in `packages/ui` so the pins can be lifted. + +### `assertNoChunkCycles` build plugin + +Vite plugin that runs Tarjan's SCC on the emitted chunk graph in +`generateBundle` and fails the build if any unknown chunk cycle exists. +The pre-existing CVA cycle is allowlisted by chunk basename +(`KNOWN_CHUNK_CYCLES` constant) so the build still passes; any **new** +cycle blocks the build with a message pointing at CIRCULAR_IMPORTS.md. + +Keep this plugin even after migration — it's not a Next-related shim, +it's general protection against this entire class of bug. Just clear +the allowlist when the underlying cycle is gone. + +### Other build-side migration changes + +- `pnpm-workspace.yaml` catalog now includes `@tanstack/react-router`, + `@tanstack/react-start`, `@tanstack/react-table` so studio and + ui-library stay aligned. `react-query` is **not** in the catalog yet + — three consumers (studio, docs, ui-library) sit on different 5.x + ranges and unifying them is a separate decision. +- `NODE_OPTIONS=--max-old-space-size=8192` is set on the studio + `dev` script — Vite's Rolldown-RC frontend hits the default 4 GB + ceiling when chewing through studio's module graph in watch mode. + +--- + +## Deferred / revisit + +- ~~`pages/org/_/[[...routeSlug]].tsx`~~ landed as `routes/org.[_].tsx` + `routes/org.[_].$.tsx`. **Naming delta:** path-as-filename form (not `routes/org/[_]/index.tsx`) because the index-file form trips a router-generator bug at `getRouteNodes.js:132` — when an `index.tsx` has a bracket-escaped _parent_ segment, `originalRoutePath` gets wiped wholesale and the escape info is lost, so `_` gets stripped as pathless. The path-as-filename form keeps the last segment non-index and avoids the bug branch entirely. Next page accepts either Next-style `routeSlug` (string[]) or TanStack-style `_splat` (string) and normalises to the array shape. +- ~~`pages/project/_/[[...routeSlug]].tsx`~~ landed as `routes/project.[_].tsx` + `routes/project.[_].$.tsx`. Same naming-delta rationale as the org catch-alls above. + +### Cleanup checklist (after every `pages/...` file is gone) + +- Switch `routes/index.tsx` redirects from `href` to `to` — all targets + now live in the TanStack tree. +- Migrate `usePreventNavigationOnUnsavedChanges` from `router.events.on('routeChangeStart', …)` (throw-to-cancel pattern) to TanStack's `useBlocker`. +- Drop the `_splat` / `routeSlug` normalisation block from + `pages/org/_/[[...routeSlug]].tsx` + `pages/project/_/[[...routeSlug]].tsx` (only there to keep both runtimes mounting the same body). +- Remove `RouteValidationWrapper` + `next/router` compat shim usage from `__root.tsx`. +- Remove `compat/next/` directory entirely once no `next/*` import remains in workspace source. +- Lift `manualChunks` pins (`class-variance-authority`, `lucide-react`, `react-vendor`) once the structural fix in `packages/ui` lands — see CIRCULAR_IMPORTS.md. Keep `assertNoChunkCycles`; just clear `KNOWN_CHUNK_CYCLES`. +- Delete `pages/_app.tsx`, `pages/_document.tsx`, `pages/_error.jsx`, `pages/500.tsx`, `pages/404.tsx` (Next-only catch-alls; TanStack equivalents on `__root.tsx`). +- Drop the `dev:next` / `build:next` / `start:next` scripts from `apps/studio/package.json` once we're committed to TanStack. +- Delete this file. diff --git a/apps/studio/components/interfaces/Database/Replication/Destinations.tsx b/apps/studio/components/interfaces/Database/Replication/Destinations.tsx index 462cb4c7125..d6cc0d79330 100644 --- a/apps/studio/components/interfaces/Database/Replication/Destinations.tsx +++ b/apps/studio/components/interfaces/Database/Replication/Destinations.tsx @@ -99,7 +99,17 @@ export const Destinations = () => { isError: isDatabasesError, isSuccess: isDatabasesSuccess, } = useReadReplicasQuery({ projectRef }, { refetchInterval: statusRefetchInterval }) - const readReplicas = databases.filter((x) => x.identifier !== projectRef) + // Memoise so the array reference is stable across renders. Without this + // the polling useEffect below has an unstable dep, runs every render, and + // its `setStatusRefetchInterval(false)` churn keeps the parent re-rendering + // — which trips a latent ref-instability bug in @radix-ui/react-slot + // (`composeRefs` is called per render instead of `useComposedRefs`) and + // tanks the page with "Maximum update depth exceeded" via the Tooltip + // trigger refs. + const readReplicas = useMemo( + () => databases.filter((x) => x.identifier !== projectRef), + [databases, projectRef] + ) const hasReplicas = isDatabasesSuccess && readReplicas.length > 0 const filteredReplicas = filterString.length === 0 diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/CancellationFlow.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/CancellationFlow.tsx index b64817dc36d..c1b4288de28 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/CancellationFlow.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/CancellationFlow.tsx @@ -1,6 +1,6 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common' -import React, { ComponentProps, useEffect, useMemo, useState } from 'react' +import { ComponentProps, useEffect, useMemo, useState } from 'react' import { DowngradeModal } from './DowngradeModal' import { ExitSurveyModal } from './ExitSurveyModal' diff --git a/apps/studio/components/ui/AIEditor/index.tsx b/apps/studio/components/ui/AIEditor/index.tsx index 63076cff831..1fa7d3bb507 100644 --- a/apps/studio/components/ui/AIEditor/index.tsx +++ b/apps/studio/components/ui/AIEditor/index.tsx @@ -218,15 +218,17 @@ export const AIEditor = ({ if (language === 'javascript' || language === 'typescript') { // The Deno libs are loaded as a raw text via raw-loader in next.config.ts. They're passed as raw text to the - // Monaco editor. - import('@/public/deno/edge-runtime.d.ts' as string) + // Monaco editor. The @vite-ignore stops Vite/Rolldown's dep scanner from following the dynamic import and + // trying to parse the .d.ts declarations as runtime modules — that crashes the scan in vite 8.0.13+ and + // cascades into SSR pre-bundling breakage. + import(/* @vite-ignore */ '@/public/deno/edge-runtime.d.ts' as string) .then((module) => { monaco.languages.typescript.typescriptDefaults.addExtraLib(module.default) }) .catch((error) => { console.error('Failed to load Deno edge-runtime typings:', error) }) - import('@/public/deno/lib.deno.d.ts' as string) + import(/* @vite-ignore */ '@/public/deno/lib.deno.d.ts' as string) .then((module) => { monaco.languages.typescript.typescriptDefaults.addExtraLib(module.default) }) diff --git a/apps/studio/data/utils/deployment-commit-query.ts b/apps/studio/data/utils/deployment-commit-query.ts index 44df7f26534..8729bd57cfd 100644 --- a/apps/studio/data/utils/deployment-commit-query.ts +++ b/apps/studio/data/utils/deployment-commit-query.ts @@ -5,7 +5,15 @@ import { BASE_PATH } from '@/lib/constants' import type { ResponseError, UseCustomQueryOptions } from '@/types' export async function getDeploymentCommit() { - const response = await fetchHandler(`${BASE_PATH}/api/get-deployment-commit`) + // `credentials: 'omit'` drops cookies for this one request — including the + // `__vdpl` skew-protection pin (TanStack, see router.tsx). With no pin cookie, + // Vercel's edge routes it to the LATEST deployment, so this check can detect a + // newer version even while the rest of the session stays pinned. The endpoint + // is public (no auth needed), and we keep the basePath URL so it still routes + // to studio in production (root `/api/*` there is the marketing site). + const response = await fetchHandler(`${BASE_PATH}/api/get-deployment-commit`, { + credentials: 'omit', + }) return (await response.json()) as { commitSha: string; commitTime: string } } diff --git a/apps/studio/hooks/use-check-latest-deploy.tsx b/apps/studio/hooks/use-check-latest-deploy.tsx index f695da8f0b4..8e1d4a073a7 100644 --- a/apps/studio/hooks/use-check-latest-deploy.tsx +++ b/apps/studio/hooks/use-check-latest-deploy.tsx @@ -6,6 +6,7 @@ import { toast } from 'sonner' import { Button, StatusIcon } from 'ui' import { useDeploymentCommitQuery } from '@/data/utils/deployment-commit-query' +import { BASE_PATH } from '@/lib/constants' const DeployCheckToast = ({ id }: { id: string | number }) => { const router = useRouter() @@ -24,7 +25,16 @@ const DeployCheckToast = ({ id }: { id: string | number }) => { - + ) diff --git a/apps/studio/lib/ai/util.ts b/apps/studio/lib/ai/util.ts index 953f1e58227..21b47997f7e 100644 --- a/apps/studio/lib/ai/util.ts +++ b/apps/studio/lib/ai/util.ts @@ -32,7 +32,7 @@ export async function selectWeightedKey( const keys = Object.keys(weights) as T[] const encoder = new TextEncoder() const data = encoder.encode(input) - const hashBuffer = await crypto.subtle.digest('SHA-256', data) + const hashBuffer = await crypto.subtle.digest('SHA-256', data as BufferSource) // Use first 4 bytes (32 bit integer) const hashInt = new DataView(hashBuffer).getUint32(0) diff --git a/apps/studio/lib/hosted-api-allowlist.ts b/apps/studio/lib/hosted-api-allowlist.ts new file mode 100644 index 00000000000..614d863ddc3 --- /dev/null +++ b/apps/studio/lib/hosted-api-allowlist.ts @@ -0,0 +1,37 @@ +// [Joshen] Allowlist of API endpoints supported in hosted (platform) mode. +// Every other /api/* route must 404 in platform mode. Shared by the Next +// middleware (proxy.ts) and the TanStack request middleware (start.ts) so +// the list can't drift between the two frameworks while both run in parallel. +export const HOSTED_SUPPORTED_API_URLS = [ + '/ai/sql/generate-v4', + '/ai/sql/policy', + '/ai/feedback/rate', + '/ai/code/complete', + '/ai/sql/cron-v2', + '/ai/sql/title-v2', + '/ai/sql/filter-v1', + '/ai/onboarding/design', + '/ai/feedback/classify', + '/ai/docs', + '/ai/sql/parse-client-code', + '/get-ip-address', + '/get-utc-time', + '/get-deployment-commit', + '/check-cname', + '/edge-functions/test', + '/edge-functions/body', + '/generate-attachment-url', + '/incident-status', + '/incident-banner', + '/status-override', + '/api/integrations/stripe-sync', + '/content/graphql', + '/parse-query', +] + +// `pathname` must be basePath-relative — Next's `nextUrl.pathname` already is, +// and the TanStack guard strips BASE_PATH before calling. Entries are path +// suffixes, so `endsWith` stays correct regardless. +export function isHostedSupportedApiPath(pathname: string): boolean { + return HOSTED_SUPPORTED_API_URLS.some((url) => pathname.endsWith(url)) +} diff --git a/apps/studio/lib/role-impersonation.ts b/apps/studio/lib/role-impersonation.ts index 5d49df34485..0f6353f12b7 100644 --- a/apps/studio/lib/role-impersonation.ts +++ b/apps/studio/lib/role-impersonation.ts @@ -126,7 +126,7 @@ function encodeBase64Url(data: ArrayBuffer | Uint8Array | string): string { function genKey(rawKey: string) { return window.crypto.subtle.importKey( 'raw', - encodeText(rawKey), + encodeText(rawKey) as BufferSource, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign', 'verify'] @@ -144,7 +144,7 @@ async function createToken(jwtPayload: object, key: string) { await window.crypto.subtle.sign( { name: 'HMAC' }, await genKey(key), - encodeText(headerAndPayload) + encodeText(headerAndPayload) as BufferSource ) ) ) diff --git a/apps/studio/lib/telemetry.tsx b/apps/studio/lib/telemetry.tsx index 9ae59ef7441..0b0cbb9358b 100644 --- a/apps/studio/lib/telemetry.tsx +++ b/apps/studio/lib/telemetry.tsx @@ -10,7 +10,7 @@ import { API_URL, IS_PLATFORM } from '@/lib/constants' const getAnonId = async (id: string) => { const encoder = new TextEncoder() const data = encoder.encode(id) - const hashBuffer = await crypto.subtle.digest('SHA-256', data) + const hashBuffer = await crypto.subtle.digest('SHA-256', data as BufferSource) const hashArray = Array.from(new Uint8Array(hashBuffer)) const base64String = btoa(hashArray.map((byte) => String.fromCharCode(byte)).join('')) diff --git a/apps/studio/next.config.ts b/apps/studio/next.config.ts index bdaba9c3961..0f956022608 100644 --- a/apps/studio/next.config.ts +++ b/apps/studio/next.config.ts @@ -5,6 +5,12 @@ import { withSentryConfig } from '@sentry/nextjs' import type { NextConfig } from 'next' import { getCSP } from './csp' +import { + getMaintenanceRedirects, + PLATFORM_REDIRECTS, + SELF_HOSTED_REDIRECTS, + SHARED_REDIRECTS, +} from './redirects.shared' const withBundleAnalyzer = bundleAnalyzer({ enabled: process.env.ANALYZE === 'true', @@ -61,423 +67,15 @@ const nextConfig = { ] }, async redirects() { + // Rules live in `redirects.shared.ts` (shared with `vercel.ts`). Next + // auto-prepends `basePath` to source and destination on its own, + // except for the special `/` → basePath bounce below which opts out + // via `basePath: false`. + const isPlatform = process.env.NEXT_PUBLIC_IS_PLATFORM === 'true' + const maintenance = process.env.MAINTENANCE_MODE === 'true' return [ - ...(process.env.NEXT_PUBLIC_IS_PLATFORM === 'true' - ? [ - { - source: '/', - has: [ - { - type: 'query' as const, - 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, - }, - ] - : [ - { - 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, - }, - ]), - { - 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, - }, - { - permanent: true, - source: '/project/:ref/settings/api-keys/new', - destination: '/project/:ref/settings/api-keys', - }, - { - 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, - }, - { - permanent: true, - source: '/project/:ref/settings/jwt/signing-keys', - destination: '/project/:ref/settings/jwt', - }, - { - 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, - }, - { - 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', - }, - { - 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, - }, - + ...(isPlatform ? PLATFORM_REDIRECTS : SELF_HOSTED_REDIRECTS), + ...SHARED_REDIRECTS, ...(process.env.NEXT_PUBLIC_BASE_PATH?.length ? [ { @@ -488,22 +86,7 @@ const nextConfig = { }, ] : []), - - ...(process.env.MAINTENANCE_MODE === 'true' - ? [ - { - source: '/((?!maintenance|img).*)', // Redirect all paths except /maintenance and /img - destination: '/maintenance', - permanent: false, - }, - ] - : [ - { - source: '/maintenance', - destination: '/', - permanent: false, - }, - ]), + ...getMaintenanceRedirects(maintenance), ] }, async headers() { diff --git a/apps/studio/package.json b/apps/studio/package.json index 5c1c9f7e86b..156f4d7e111 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -6,9 +6,17 @@ "scripts": { "preinstall": "npx only-allow pnpm", "predev": "node scripts/clean-turbopack-cache.mjs", - "dev": "next dev -p ${STUDIO_PORT:-8082}", - "build": "next build && if [ \"$SKIP_ASSET_UPLOAD\" != \"1\" ]; then ./../../scripts/upload-static-assets.sh; fi", - "start": "next start -p 8082", + "dev": "node scripts/dispatch.js dev", + "build": "node scripts/dispatch.js build", + "start": "node scripts/dispatch.js start", + "preview": "vite preview --mode ${MODE:-production} --port 8082", + "dev:next": "next dev -p ${STUDIO_PORT:-8082}", + "build:next": "next build && if [ \"$SKIP_ASSET_UPLOAD\" != \"1\" ]; then ./../../scripts/upload-static-assets.sh; fi", + "start:next": "next start -p 8082", + "dev:tanstack": "NODE_OPTIONS=--max-old-space-size=8192 vite dev --port ${STUDIO_PORT:-8082}", + "build:tanstack": "vite build --mode ${MODE:-production} && pnpm smoke:tanstack", + "smoke:tanstack": "node scripts/smoke-server.mjs", + "start:tanstack": "node scripts/serve.js", "lint": "eslint .", "lint:ratchet": "tsx scripts/ratchet-eslint-rules.ts --rule react-hooks/exhaustive-deps --rule import/no-anonymous-default-export --rule @tanstack/query/exhaustive-deps --rule @typescript-eslint/no-explicit-any --rule no-restricted-imports --rule no-restricted-exports --rule react/no-unstable-nested-components", "clean": "rimraf node_modules tsconfig.tsbuildinfo .next .turbo", @@ -54,7 +62,11 @@ "@monaco-editor/react": "^4.6.0", "@next/bundle-analyzer": "16.2.3", "@number-flow/react": "^0.3.2", + "@radix-ui/react-slider": "^1.1.2", + "@radix-ui/react-slot": "^1.2.0", + "@radix-ui/react-visually-hidden": "^1.2.3", "@sentry/nextjs": "catalog:", + "@sentry/react": "^10.27.0", "@std/path": "npm:@jsr/std__path@^1.0.8", "@stripe/react-stripe-js": "6.1.0", "@stripe/stripe-js": "9.1.0", @@ -65,11 +77,16 @@ "@supabase/shared-types": "0.1.88", "@supabase/sql-to-rest": "^0.1.6", "@supabase/supabase-js": "catalog:", - "@tanstack/react-hotkeys": "^0.9.1", - "@tanstack/react-query": "^5.0.0", - "@tanstack/react-query-devtools": "^5.0.0", - "@tanstack/react-table": "^8.21.3", - "@tanstack/react-virtual": "^3.13.12", + "@tanstack/react-devtools": "^0.10.3", + "@tanstack/react-hotkeys": "^0.10.0", + "@tanstack/react-query": "~5.83.0", + "@tanstack/react-query-devtools": "~5.90.2", + "@tanstack/react-router": "catalog:", + "@tanstack/react-router-devtools": "^1.166.13", + "@tanstack/react-router-ssr-query": "^1.166.12", + "@tanstack/react-start": "catalog:", + "@tanstack/react-table": "catalog:", + "@tanstack/react-virtual": "^3.13.24", "@types/d3-geo": "^3.1.0", "@uidotdev/usehooks": "^2.4.1", "@vercel/functions": "^2.1.0", @@ -154,6 +171,9 @@ "@graphql-typed-document-node/core": "^3.2.0", "@radix-ui/react-use-escape-keydown": "^1.1.1", "@smithy/property-provider": "^4.0.4", + "@tailwindcss/container-queries": "^0.1.1", + "@tailwindcss/vite": "4.2.4", + "@tanstack/devtools-vite": "^0.6.0", "@testing-library/dom": "^10.0.0", "@testing-library/jest-dom": "^6.6.0", "@testing-library/react": "^16.0.0", @@ -177,6 +197,7 @@ "@types/recharts": "^1.8.23", "@types/sqlstring": "^2.3.0", "@types/zxcvbn": "^4.4.1", + "@vercel/config": "^0.2.1", "@vitejs/plugin-react": "catalog:", "@vitest/coverage-v8": "catalog:", "@vitest/ui": "catalog:", diff --git a/apps/studio/proxy.ts b/apps/studio/proxy.ts index 2ca590998b7..cfb19146dfe 100644 --- a/apps/studio/proxy.ts +++ b/apps/studio/proxy.ts @@ -1,44 +1,17 @@ import type { NextRequest } from 'next/server' import { IS_PLATFORM } from '@/lib/constants' +import { isHostedSupportedApiPath } from '@/lib/hosted-api-allowlist' export const config = { matcher: '/api/:function*', } -// [Joshen] Return 404 for all next.js API endpoints EXCEPT the ones we use in hosted: -const HOSTED_SUPPORTED_API_URLS = [ - '/ai/sql/generate-v4', - '/ai/sql/policy', - '/ai/feedback/rate', - '/ai/code/complete', - '/ai/sql/cron-v2', - '/ai/sql/title-v2', - '/ai/sql/filter-v1', - '/ai/onboarding/design', - '/ai/feedback/classify', - '/ai/docs', - '/ai/sql/parse-client-code', - '/get-ip-address', - '/get-utc-time', - '/get-deployment-commit', - '/check-cname', - '/edge-functions/test', - '/edge-functions/body', - '/generate-attachment-url', - '/incident-status', - '/incident-banner', - '/status-override', - '/api/integrations/stripe-sync', - '/content/graphql', - '/parse-query', -] - +// Return 404 for all next.js API endpoints EXCEPT the ones we use in hosted. +// The allowlist is shared with the TanStack guard (start.ts) — see +// lib/hosted-api-allowlist.ts. export function proxy(request: NextRequest) { - if ( - IS_PLATFORM && - !HOSTED_SUPPORTED_API_URLS.some((url) => request.nextUrl.pathname.endsWith(url)) - ) { + if (IS_PLATFORM && !isHostedSupportedApiPath(request.nextUrl.pathname)) { return Response.json( { success: false, message: 'Endpoint not supported on hosted' }, { status: 404 } diff --git a/apps/studio/redirects.shared.ts b/apps/studio/redirects.shared.ts new file mode 100644 index 00000000000..a786fac35d3 --- /dev/null +++ b/apps/studio/redirects.shared.ts @@ -0,0 +1,424 @@ +// 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 | null { + const patternParts = pattern.split('/') + const pathParts = pathname.split('/') + const params: Record = {} + + 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 { + return dest.replace(/:(\w+)\*?/g, (_, name) => params[name] ?? '') +} + +function hasQueryMatches( + has: StudioRedirect['has'], + search: URLSearchParams | Record +): 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 + 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 +} diff --git a/apps/studio/scripts/dispatch.js b/apps/studio/scripts/dispatch.js new file mode 100644 index 00000000000..4ca2f43f3c5 --- /dev/null +++ b/apps/studio/scripts/dispatch.js @@ -0,0 +1,64 @@ +#!/usr/bin/env node +// Dispatch a top-level npm script (dev/build/start) to either the next- or +// tanstack-flavoured variant based on STUDIO_FRAMEWORK. We parse the env files +// (via the shared scripts/lib/env.js parser) and pull out only +// STUDIO_FRAMEWORK — we deliberately don't load the whole file into the +// child's process.env, because scripts/serve.js / vite do their own .env +// loading and would otherwise refuse to override the dispatcher-set values, +// including NEXT_PUBLIC_IS_PLATFORM which the e2e `.env.test` needs to flip to +// `false`. +// +// Usage: node scripts/dispatch.js +// target ∈ { dev, build, start } +// +// Resolves to `pnpm run :` where framework is `tanstack` +// when STUDIO_FRAMEWORK=tanstack (set in the shell env, `.env`, or +// `.env.local`), otherwise `next`. +import { spawn } from 'node:child_process' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +import { readEnvFiles } from './lib/env.js' + +const target = process.argv[2] +if (!target) { + console.error('dispatch.js: missing target (expected one of: dev, build, start)') + process.exit(2) +} + +const studioRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') + +// Shell env wins, then `.env.local`, then `.env` — the same precedence +// scripts/serve.js and vite use, so STUDIO_FRAMEWORK set in either file is +// picked up (not just `.env.local`). +const fileEnv = readEnvFiles(studioRoot, ['.env', '.env.local']) +const studioFramework = process.env.STUDIO_FRAMEWORK ?? fileEnv.STUDIO_FRAMEWORK +const framework = studioFramework === 'tanstack' ? 'tanstack' : 'next' +const script = `${target}:${framework}` + +// Use async `spawn` rather than `spawnSync` — long-running dev servers +// (vite dev / next dev) wedge under `spawnSync` because Node holds the +// event loop and stdin doesn't flow through cleanly. The dev server says +// "ready" then exits ~1s later. `spawn` + manual forwarding keeps the +// child interactive and lets the parent exit cleanly when the child does. +const child = spawn('pnpm', ['run', script], { + stdio: 'inherit', + env: process.env, +}) + +const forwardSignal = (signal) => { + if (!child.killed) child.kill(signal) +} +for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGQUIT']) { + process.on(signal, () => forwardSignal(signal)) +} + +child.on('exit', (code, signal) => { + if (signal) process.kill(process.pid, signal) + else process.exit(code ?? 1) +}) + +child.on('error', (err) => { + console.error('dispatch.js: failed to spawn child:', err) + process.exit(1) +}) diff --git a/apps/studio/scripts/lib/env.js b/apps/studio/scripts/lib/env.js new file mode 100644 index 00000000000..0c8c2e95d30 --- /dev/null +++ b/apps/studio/scripts/lib/env.js @@ -0,0 +1,52 @@ +// Shared .env parsing for the studio build/serve scripts (dispatch.js, +// serve.js). We parse the small subset of the dotenv format we actually rely +// on — `KEY=value`, an optional `export` prefix, and surrounding single/double +// quotes — rather than taking on the `dotenv` dependency for a couple of build +// scripts. Crucially, nothing here touches `process.env`: callers decide what +// to do with the parsed values, which is what lets dispatch.js read a single +// key without leaking the whole file into the child process. +// +// dispatch.js and serve.js must agree on this format, so it lives here once. +import { readFileSync } from 'node:fs' +import path from 'node:path' + +const ENV_LINE = /^\s*(?:export\s+)?([\w.-]+)\s*=\s*(.*?)\s*$/ + +// Parse the contents of a single env file into a plain key/value object. +export function parseEnv(content) { + const parsed = {} + for (const raw of content.split('\n')) { + const m = ENV_LINE.exec(raw) + if (!m) continue + let value = m[2] + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + value = value.slice(1, -1) + } + parsed[m[1]] = value + } + return parsed +} + +// Read a cascade of env files from `dir`, with later files overriding earlier +// ones — matching the precedence vite/Next use +// (`.env` < `.env.local` < `.env.` < `.env..local`). A missing file +// is skipped, but any other read error (permissions, IO) is surfaced rather +// than silently dropping that file's values. Returns the merged key/value +// object; never mutates process.env. +export function readEnvFiles(dir, files) { + const parsed = {} + for (const file of files) { + let content + try { + content = readFileSync(path.join(dir, file), 'utf8') + } catch (err) { + if (err && typeof err === 'object' && err.code === 'ENOENT') continue + throw err + } + Object.assign(parsed, parseEnv(content)) + } + return parsed +} diff --git a/apps/studio/scripts/ratchet-eslint-rules.ts b/apps/studio/scripts/ratchet-eslint-rules.ts index a26de11e4af..e516aa446ad 100644 --- a/apps/studio/scripts/ratchet-eslint-rules.ts +++ b/apps/studio/scripts/ratchet-eslint-rules.ts @@ -128,7 +128,7 @@ function dangerouslyRunEsLint(eslintCmd: string, eslintArgs: string): ESLintExec encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], env: process.env, - maxBuffer: 32 * 1024 * 1024, // allow large ESLint JSON payloads + maxBuffer: 128 * 1024 * 1024, // allow large ESLint JSON payloads (the studio repo regularly emits ~35MB+) }) const stdout = typeof proc.stdout === 'string' ? proc.stdout : '' diff --git a/apps/studio/scripts/serve.js b/apps/studio/scripts/serve.js new file mode 100644 index 00000000000..18aff7f15bb --- /dev/null +++ b/apps/studio/scripts/serve.js @@ -0,0 +1,183 @@ +#!/usr/bin/env node +// Standalone Node HTTP server that hosts the production studio build. +// +// We export the fetch-handler shape from `dist/server/server.js` because +// Vercel consumes it directly (see `apps/studio/api/server.js`). For +// self-hosted / e2e, we need an HTTP listener of our own — this is that +// listener. +// +// Responsibilities: +// - Load env files in vite preview's order so non-NEXT_PUBLIC_* values +// (POSTGRES_PASSWORD, PG_META_CRYPTO_KEY, etc.) are in process.env +// at request time. NEXT_PUBLIC_* are already inlined into the bundle +// at build time and don't need to be re-loaded. +// - Serve static client assets from `dist/client/` directly with the +// right MIME types and cache headers. +// - Forward everything else to the TanStack Start handler exported +// from `dist/server/server.js`. +import { createReadStream } from 'node:fs' +import { stat } from 'node:fs/promises' +import { createServer } from 'node:http' +import path from 'node:path' +import { Readable } from 'node:stream' +import { fileURLToPath } from 'node:url' + +import { readEnvFiles } from './lib/env.js' + +const studioRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const clientDir = path.join(studioRoot, 'dist/client') +const mode = process.env.MODE || 'production' + +const envFiles = ['.env', '.env.local', `.env.${mode}`, `.env.${mode}.local`] +const parsed = readEnvFiles(studioRoot, envFiles) +// Don't clobber values the shell already provides — match `vite preview`. +for (const [k, v] of Object.entries(parsed)) { + if (process.env[k] !== undefined) continue + process.env[k] = v.replace( + /\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?/g, + (_, name) => process.env[name] ?? parsed[name] ?? '' + ) +} + +const { default: handler } = await import(path.join(studioRoot, 'dist/server/server.js')) + +const mimeByExt = new Map([ + ['.js', 'application/javascript; charset=utf-8'], + ['.mjs', 'application/javascript; charset=utf-8'], + ['.css', 'text/css; charset=utf-8'], + ['.html', 'text/html; charset=utf-8'], + ['.json', 'application/json; charset=utf-8'], + ['.map', 'application/json; charset=utf-8'], + ['.png', 'image/png'], + ['.jpg', 'image/jpeg'], + ['.jpeg', 'image/jpeg'], + ['.gif', 'image/gif'], + ['.svg', 'image/svg+xml'], + ['.ico', 'image/x-icon'], + ['.woff', 'font/woff'], + ['.woff2', 'font/woff2'], + ['.txt', 'text/plain; charset=utf-8'], + ['.webmanifest', 'application/manifest+json'], +]) + +// Vite emits hashed filenames (e.g. `index-DB4J79t9.js`) for everything +// it bundles. Those are content-addressed so we serve them immutable. +const HASHED_RE = /-[A-Za-z0-9_-]{6,}\.[a-z0-9]+$/ + +async function serveStatic(req, res) { + let pathname + try { + pathname = new URL(req.url, 'http://localhost').pathname + } catch { + return false + } + if (pathname === '/' || pathname.endsWith('/')) return false + if (pathname.includes('..') || pathname.includes('\\')) return false + const filePath = path.join(clientDir, pathname) + if (!filePath.startsWith(clientDir + path.sep)) return false + + let st + try { + st = await stat(filePath) + } catch { + return false + } + if (!st.isFile()) return false + + res.statusCode = 200 + res.setHeader( + 'content-type', + mimeByExt.get(path.extname(filePath).toLowerCase()) ?? 'application/octet-stream' + ) + res.setHeader('content-length', String(st.size)) + res.setHeader( + 'cache-control', + HASHED_RE.test(pathname) ? 'public, max-age=31536000, immutable' : 'no-cache' + ) + await new Promise((resolve, reject) => { + const stream = createReadStream(filePath) + stream.on('error', reject) + stream.on('end', resolve) + stream.pipe(res) + }) + return true +} + +function toWebRequest(req) { + const protocol = req.socket.encrypted ? 'https' : 'http' + const url = `${protocol}://${req.headers.host ?? 'localhost'}${req.url}` + const headers = new Headers() + for (const [k, v] of Object.entries(req.headers)) { + if (k.startsWith(':')) continue + if (Array.isArray(v)) for (const vv of v) headers.append(k, vv) + else if (v !== undefined) headers.set(k, v) + } + const init = { method: req.method, headers } + // Only attach a body for methods that can carry one AND that actually + // have body bytes coming. Wrapping `req` in `Readable.toWeb(req)` for + // requests where Node has nothing to deliver leaves undici's + // `extractBody` looking at an already-consumed stream and throwing + // `TypeError: Response body object should not be disturbed or locked` + // at the `new Request(...)` call below. + const contentLength = Number(req.headers['content-length'] ?? '0') + const hasBody = + req.method !== 'GET' && + req.method !== 'HEAD' && + (contentLength > 0 || req.headers['transfer-encoding'] === 'chunked') + if (hasBody) { + init.body = Readable.toWeb(req) + init.duplex = 'half' + } + return new Request(url, init) +} + +async function pipeWebResponse(response, res) { + res.statusCode = response.status + // The Headers iterator collapses duplicate keys, and for `set-cookie` it joins + // every cookie into one comma-separated value — which corrupts auth/session + // cookies. Pull the cookies out separately via getSetCookie() and set them as + // an array so each one becomes its own header. + const setCookies = + typeof response.headers.getSetCookie === 'function' ? response.headers.getSetCookie() : [] + for (const [k, v] of response.headers) { + if (k.toLowerCase() === 'set-cookie') continue + res.setHeader(k, v) + } + if (setCookies.length > 0) res.setHeader('set-cookie', setCookies) + if (!response.body) { + res.end() + return + } + // Pipe via Readable.fromWeb so the underlying stream gets proper backpressure + // and gets released cleanly. `for await (chunk of response.body)` works in + // simple cases but can leave the body in a "disturbed / locked" state when + // the handler internally peeks at it — surfacing as + // `TypeError: Response body object should not be disturbed or locked` on a + // subsequent request. + await new Promise((resolve, reject) => { + const readable = Readable.fromWeb(response.body) + readable.on('error', reject) + res.on('error', reject) + res.on('close', resolve) + res.on('finish', resolve) + readable.pipe(res) + }) +} + +const port = Number(process.env.PORT || 8082) +createServer(async (req, res) => { + try { + if (await serveStatic(req, res)) return + const response = await handler.fetch(toWebRequest(req)) + await pipeWebResponse(response, res) + } catch (err) { + console.error('[serve] request failed:', err) + if (!res.headersSent) { + res.statusCode = 500 + res.setHeader('content-type', 'text/plain; charset=utf-8') + } + res.end('Internal Server Error') + } +}).listen(port, () => { + console.log(`Studio listening on http://localhost:${port} (mode=${mode})`) +}) diff --git a/apps/studio/scripts/smoke-server.mjs b/apps/studio/scripts/smoke-server.mjs new file mode 100644 index 00000000000..dda258119b3 --- /dev/null +++ b/apps/studio/scripts/smoke-server.mjs @@ -0,0 +1,73 @@ +#!/usr/bin/env node +// Post-build smoke test: boot the built TanStack server handler and make a +// real request with a platform-like environment, so module-scope boot +// crashes fail the BUILD instead of the deployed function at runtime. +// +// Why this exists: the TanStack server entry eagerly imports the entire route +// tree (`loadEntries`), so every route module is evaluated the first time the +// single function handler serves a request. A bad top-level side effect in any +// one route — e.g. `createClient(process.env.SUPABASE_URL!, ...)` at module +// scope, where SUPABASE_URL is unset on platform — throws during that import +// and 500s every route, including trivial ones like /api/get-utc-time. Those +// failures only showed up at runtime on Vercel; this catches them at build. +// +// What this catches: anything that throws while the route tree is imported +// (the most common class — missing env vars read at module scope). +// What this does NOT catch: assets missing from the *Vercel function bundle* +// (e.g. libpg-query.wasm). Local node_modules still has those, so booting the +// plain `dist/server` build won't surface them — that needs booting the +// `vercel build` output. Pass that function's entry as argv[1] to reuse this +// script in a deploy-gating CI step. + +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const studioRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const serverEntry = process.argv[2] + ? path.resolve(process.argv[2]) + : path.join(studioRoot, 'dist/server/server.js') + +// Simulate the platform function runtime: these are only set on self-hosted, +// so removing them surfaces any route that needs them at module-load time. +for (const key of ['SUPABASE_URL', 'SUPABASE_SERVICE_KEY', 'SUPABASE_SERVICE_ROLE_KEY']) { + delete process.env[key] +} + +const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? '' + +// A request to any route forces `loadEntries` to import the full route tree, +// so a single cheap, dependency-free endpoint is enough to exercise the boot. +const ROUTES = ['/api/get-utc-time'] + +console.log(`[smoke] booting ${path.relative(studioRoot, serverEntry)}`) +const { default: handler } = await import(serverEntry) + +let failed = false +for (const route of ROUTES) { + const url = `http://localhost${basePath}${route}` + try { + const res = await handler.fetch(new Request(url)) + if (res.status >= 500) { + failed = true + const body = await res.text().catch(() => '') + console.error(`[smoke] ✗ ${route} → ${res.status}\n${body.slice(0, 800)}`) + } else { + console.log(`[smoke] ✓ ${route} → ${res.status}`) + } + } catch (err) { + failed = true + console.error(`[smoke] ✗ ${route} threw while booting the server:\n`, err) + } +} + +if (failed) { + console.error( + '\n[smoke] FAILED — the server bundle does not boot cleanly. This usually means a\n' + + 'route module has a top-level side effect (e.g. createClient at module scope with a\n' + + 'missing env var). Make it lazy so it only runs inside the handler.' + ) + process.exit(1) +} + +console.log('\n[smoke] passed — server boots and routes respond without a 5xx.') +process.exit(0) diff --git a/apps/studio/start.ts b/apps/studio/start.ts new file mode 100644 index 00000000000..158248a618a --- /dev/null +++ b/apps/studio/start.ts @@ -0,0 +1,35 @@ +import { createMiddleware, createStart } from '@tanstack/react-start' + +import { BASE_PATH, IS_PLATFORM } from '@/lib/constants' +import { isHostedSupportedApiPath } from '@/lib/hosted-api-allowlist' + +// Self-hosted-only API routes must 404 in platform (hosted) mode. Under the +// Next pages router this lives in middleware (proxy.ts), but TanStack Start +// has no middleware runtime, so the guard is migrated here as a global +// request middleware sharing the same allowlist (lib/hosted-api-allowlist.ts). +// On Vercel our `/api/*` (and `/_serverFn/*`) requests are rewritten to the +// api/server.js function which runs the Start handler, so createStartHandler +// runs this server-side for every API request — even though pages are served +// as a static SPA shell. The guard therefore covers all API routes from a +// single place. + +const platformApiGuard = createMiddleware({ type: 'request' }).server(({ request, next }) => { + const { pathname } = new URL(request.url) + // Path relative to the configured basePath — mirrors Next's basePath- + // relative middleware matcher. + const relativePath = + BASE_PATH && pathname.startsWith(BASE_PATH) ? pathname.slice(BASE_PATH.length) : pathname + + if (IS_PLATFORM && relativePath.startsWith('/api/') && !isHostedSupportedApiPath(relativePath)) { + return Response.json( + { success: false, message: 'Endpoint not supported on hosted' }, + { status: 404 } + ) + } + + return next() +}) + +export const startInstance = createStart(() => ({ + requestMiddleware: [platformApiGuard], +})) diff --git a/apps/studio/styles/fonts.css b/apps/studio/styles/fonts.css new file mode 100644 index 00000000000..46905213121 --- /dev/null +++ b/apps/studio/styles/fonts.css @@ -0,0 +1,68 @@ +/* + * Font setup for the Vite/TanStack Start build. + * The Next.js build uses next/font via fonts/index.ts; keep the + * CSS var names (--font-custom, --font-source-code-pro) in sync so + * styles that reference them work in both pipelines. + * Source Code Pro is loaded via a in the root route's head(). + */ + +@font-face { + font-family: 'CustomFont'; + src: url('../fonts/CustomFont-Book.woff2') format('woff2'); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'CustomFont'; + src: url('../fonts/CustomFont-BookItalic.woff2') format('woff2'); + font-weight: 400; + font-style: italic; + font-display: swap; +} + +@font-face { + font-family: 'CustomFont'; + src: url('../fonts/CustomFont-Medium.woff2') format('woff2'); + font-weight: 500; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'CustomFont'; + src: url('../fonts/CustomFont-Bold.woff2') format('woff2'); + font-weight: 700; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'CustomFont'; + src: url('../fonts/CustomFont-BoldItalic.woff2') format('woff2'); + font-weight: 700; + font-style: italic; + font-display: swap; +} + +@font-face { + font-family: 'CustomFont'; + src: url('../fonts/CustomFont-Black.woff2') format('woff2'); + font-weight: 800; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'CustomFont'; + src: url('../fonts/CustomFont-BlackItalic.woff2') format('woff2'); + font-weight: 800; + font-style: italic; + font-display: swap; +} + +:root { + --font-custom: 'CustomFont', Circular, 'Helvetica Neue', Helvetica, Arial, sans-serif; + --font-source-code-pro: 'Source Code Pro', 'Office Code Pro', Menlo, monospace; +} diff --git a/apps/studio/tsconfig.json b/apps/studio/tsconfig.json index b1735434dee..5bf688391c2 100644 --- a/apps/studio/tsconfig.json +++ b/apps/studio/tsconfig.json @@ -10,7 +10,13 @@ "@/*": ["./*"], "@ui/*": ["./../../packages/ui/src/*"] // handle ui package paths }, - "plugins": [{ "name": "next" }] + "plugins": [{ "name": "next" }], + "jsx": "react-jsx", + "moduleResolution": "Bundler", + "module": "ESNext", + "target": "ES2022", + "skipLibCheck": true, + "strictNullChecks": true }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules", "public/deno/*.ts"] diff --git a/apps/studio/turbo.jsonc b/apps/studio/turbo.jsonc index 53b62b1c357..d47f1beea95 100644 --- a/apps/studio/turbo.jsonc +++ b/apps/studio/turbo.jsonc @@ -100,6 +100,22 @@ "STATUSPAGE_PAGE_ID", "INCIDENT_IO_API_KEY", "LIVE_SUPABASE_SECRET_KEY", + // Selects the build mode for the studio's `build`/`start` scripts + // (e.g. e2e sets `MODE=test`). Listed so turbo invalidates the + // cache when it changes — without this, switching between test and + // production builds reuses a stale cached output. + "MODE", + // Read by scripts/serve.js (the Node host for `pnpm start`). + // Declared here so the studio-package turbo env lint rule passes + // even though turbo doesn't directly drive `start`. + "PORT", + // Gates the TanStack vs Next path in api/server.js, vercel.ts, + // and scripts/dispatch.js (the dev/build/start dispatcher). + "STUDIO_FRAMEWORK", + // Vite's built-in `import.meta.env.SSR` flag (used in ConnectStepsSection + // to gate Vite-only `import.meta.glob`). Not a real process env var + // but turbo's `no-undeclared-env-vars` lint flags any `env.SSR` access. + "SSR", ], "passThroughEnv": [ "CURRENT_CLI_VERSION", @@ -110,7 +126,12 @@ "S3_PROTOCOL_ACCESS_KEY_ID", "S3_PROTOCOL_ACCESS_KEY_SECRET", ], - "outputs": [".next/**", "!.next/cache/**", "!.next/dev/**/*"], + "outputs": [ + ".next/**", + "!.next/cache/**", + "!.next/dev/**/*", + "dist/**", + ], }, }, } diff --git a/apps/studio/vercel.ts b/apps/studio/vercel.ts new file mode 100644 index 00000000000..2c0275a63a0 --- /dev/null +++ b/apps/studio/vercel.ts @@ -0,0 +1,155 @@ +import { routes, type Redirect, type VercelConfig } from '@vercel/config/v1' + +import { + getMaintenanceRedirects, + PLATFORM_REDIRECTS, + SELF_HOSTED_REDIRECTS, + SHARED_REDIRECTS, + type StudioRedirect, +} from './redirects.shared' + +// STUDIO_FRAMEWORK gates the TanStack Start deploy. When the env var is +// unset (the default — used by the Next.js prod deploy) this file returns +// an empty `VercelConfig` so Vercel honours the dashboard-configured +// Next.js preset untouched. Vercel reads `vercel.ts` regardless of the +// framework preset (per vercel.com/docs/project-configuration — +// `vercel.ts`'s `framework` field overrides the dashboard preset), so a +// no-op early return is the only way to keep the TanStack rewrites, +// `framework: null`, and `outputDirectory: 'dist/client'` below from +// clobbering the Next build. Set `STUDIO_FRAMEWORK=tanstack` on the +// TanStack Vercel project to opt in. +const isTanstack = process.env.STUDIO_FRAMEWORK === 'tanstack' + +// Vite's `base` bakes the prefix into asset URLs but leaves the filesystem +// layout at `dist/client/...`. On Vercel we strip the prefix for file lookups +// and fall through to the SPA shell. When NEXT_PUBLIC_BASE_PATH is empty +// the prefixed rule set is skipped and only the root-level rules fire. +const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? '' + +// Build the rewrites + headers for a given prefix ('' for root, or a base +// path like '/dashboard'). We run this once for each prefix and concatenate +// the results so we don't hand-duplicate every rule. +// +// Rewrite ordering: API + server-function passthrough first so extensioned +// API paths (/api/foo.json) don't get caught by the asset rule. Asset rule +// next — strips the basePath prefix so `/dashboard/assets/x.js` maps onto the +// `dist/client/assets/x.js` filesystem layout (a no-op identity when +// prefix=''). Shell rule LAST, and it deliberately matches only extensionless +// paths via a negative lookahead. +// +// Why the lookahead matters: a request WITH a file extension that doesn't +// resolve to a real file — e.g. a hashed chunk from an older deployment after +// a redeploy — must fall through to a 404, NOT the HTML shell. A catch-all +// `(.*)` shell swallows those misses and returns `text/html`, so the browser +// gets HTML for a `.js` request and throws "Failed to load module script … +// MIME type text/html" (and, because /assets/* is cached immutable, that HTML +// poisons the edge cache under the asset URL). A clean 404 instead lets skew +// protection's `__vdpl` routing serve the chunk from the deployment that still +// has it, or the client's `vite:preloadError` backstop recover. +function routesFor(prefix: string) { + return { + rewrites: [ + routes.rewrite(`${prefix}/api/(.*)`, '/api/server'), + routes.rewrite(`${prefix}/_serverFn/(.*)`, '/api/server'), + routes.rewrite(`${prefix}/(.*\\.\\w+)`, '/$1'), + routes.rewrite(`${prefix}/((?!.*\\.\\w+$).*)`, '/_shell'), + ], + headers: [ + // Dynamic function responses must not be cached by any shared cache — + // handlers can still opt in with their own Cache-Control on the + // Response when a response IS safe to cache. + routes.cacheControl(`${prefix}/api/(.*)`, { private: true, noStore: true }), + routes.cacheControl(`${prefix}/_serverFn/(.*)`, { private: true, noStore: true }), + // Hashed bundles under /assets/* are content-addressed — safe to + // cache forever. + routes.cacheControl(`${prefix}/assets/(.*)`, { + public: true, + maxAge: '1year', + immutable: true, + }), + ], + } +} + +// --------------------------------------------------------------------------- +// Redirects — entries live in `redirects.shared.ts`, consumed by both +// `next.config.ts` and this file. Next auto-prepends `basePath` to its +// redirects; Vercel doesn't, so we apply it here. +// --------------------------------------------------------------------------- + +function applyBasePath(r: StudioRedirect): Redirect { + if (!basePath) return r + const prefix = (path: string) => + path.startsWith('/') ? (path === '/' ? basePath : `${basePath}${path}`) : path + return { ...r, source: prefix(r.source), destination: prefix(r.destination) } +} + +function buildRedirects(): Redirect[] { + const isPlatform = process.env.NEXT_PUBLIC_IS_PLATFORM === 'true' + const maintenance = process.env.MAINTENANCE_MODE === 'true' + const conditional = isPlatform ? PLATFORM_REDIRECTS : SELF_HOSTED_REDIRECTS + + // Bare-domain bounce to the basePath when one is configured. Source + // stays literally `/` (NOT prefixed) so the entry-point redirect fires. + const basePathBounce: Redirect[] = basePath + ? [{ source: '/', destination: basePath, permanent: false }] + : [] + + return [ + ...conditional.map(applyBasePath), + ...SHARED_REDIRECTS.map(applyBasePath), + ...basePathBounce, + ...getMaintenanceRedirects(maintenance).map(applyBasePath), + ] +} + +function buildTanstackConfig(): VercelConfig { + // When a base path is configured, emit both the prefixed and root rule + // sets (prefixed first so it wins for explicit /dashboard/* hits, root as + // a fallback for bare-domain traffic). + const ruleSets = (basePath ? [basePath, ''] : ['']).map(routesFor) + + return { + framework: null, + outputDirectory: 'dist/client', + cleanUrls: true, + redirects: buildRedirects(), + rewrites: ruleSets.flatMap((r) => r.rewrites), + headers: ruleSets.flatMap((r) => r.headers), + // `api/server.js` imports the TanStack SSR bundle via a computed + // path so Vercel's function bundler doesn't try to statically + // resolve `dist/server/server.js` during the Next.js prod build + // (where `dist/` doesn't exist). `includeFiles` ships the SSR + // output into the function bundle for the TanStack build so the + // runtime import resolves. + functions: { + 'api/server.js': { + // Ship the SSR output, plus libpg-query's wasm. libpg-query is + // externalized for SSR and loads its `.wasm` relative to its own + // dir (`__dirname`) at import time; Vercel's function bundler + // doesn't trace that node_modules asset, so co-ship it explicitly + // or the server crashes at boot with ENOENT on libpg-query.wasm. + // + // Target the real pnpm store path (repo-root `.pnpm`, two levels up + // from this Root Directory) rather than `node_modules/libpg-query`, + // which is a pnpm symlink — Vercel rejects packaging files reached + // through symlinked dirs ("invalid deployment package"). The real + // path also matches where Node resolves __dirname at runtime + // (/var/task/node_modules/.pnpm/libpg-query@.../wasm/...). + includeFiles: + '{dist/server/**,../../node_modules/.pnpm/libpg-query@*/node_modules/libpg-query/wasm/libpg-query.wasm}', + }, + }, + } +} + +// Empty config = no overrides; Vercel falls back to the dashboard preset. +const passthrough: VercelConfig = {} + +export const config: VercelConfig = isTanstack ? buildTanstackConfig() : passthrough + +// Belt-and-braces: local @vercel/config CLI reads module.default, but the +// docs claim Vercel's platform looks for a named `config` export. Export +// both so whichever path runs wins. +// eslint-disable-next-line no-restricted-exports +export default config diff --git a/apps/studio/vite.config.ts b/apps/studio/vite.config.ts new file mode 100644 index 00000000000..5abdf9392fd --- /dev/null +++ b/apps/studio/vite.config.ts @@ -0,0 +1,466 @@ +/* eslint-disable no-restricted-exports */ + +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import tailwindcss from '@tailwindcss/vite' +import { devtools } from '@tanstack/devtools-vite' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import viteReact from '@vitejs/plugin-react' +import { defineConfig, loadEnv, type Plugin } from 'vite' + +const rootDir = path.dirname(fileURLToPath(import.meta.url)) +const compatRoot = path.resolve(rootDir, 'compat/next') + +// Map of Next imports we've shimmed to their TanStack-backed replacement. +// Add an entry here + a file under compat/next/ when a new Next surface is +// needed by app source. +const nextShims: Record = { + 'next/compat/router': path.join(compatRoot, 'compat/router.ts'), + 'next/dynamic': path.join(compatRoot, 'dynamic.tsx'), + 'next/head': path.join(compatRoot, 'head.tsx'), + 'next/image': path.join(compatRoot, 'image.tsx'), + 'next/legacy/image': path.join(compatRoot, 'legacy/image.tsx'), + 'next/link': path.join(compatRoot, 'link.tsx'), + 'next/navigation': path.join(compatRoot, 'navigation.ts'), + 'next/router': path.join(compatRoot, 'router.ts'), + 'next/script': path.join(compatRoot, 'script.tsx'), + 'next/server': path.join(compatRoot, 'server.ts'), +} + +// Combined compat + migration guard: +// - If app source imports a shimmed `next/*` id, resolve it to the local +// shim (acts like resolve.alias). +// - Otherwise, if app source imports from `next` or `next/*`, fail the +// build so we catch unshimmed usage at build time during the migration. +// - node_modules imports (e.g. @sentry/nextjs reaching into next) pass +// through untouched. +function nextCompat(): Plugin { + return { + name: 'studio-next-compat', + enforce: 'pre', + resolveId(id, importer) { + if (!importer || importer.includes('/node_modules/')) return + if (nextShims[id]) return nextShims[id] + if (id === 'next' || id.startsWith('next/')) { + throw new Error( + `[next-compat] "${id}" imported from ${importer}.\n` + + `Add a shim under apps/studio/compat/next/ and register it in vite.config.ts, ` + + `or use a framework-agnostic equivalent.` + ) + } + }, + } +} + +// Short-circuit UMD wrappers' AMD branch by string-replacing the +// `define.amd` check. Vite's `config.define` doesn't reach pre-bundled +// deps (Vite 8's Rolldown-based optimizer doesn't honour member- +// expression define keys at the prebundle stage), and adding the same +// substitution to `optimizeDeps.rolldownOptions.define` had no effect +// on the emitted `node_modules/.vite/deps/*.js`. This transform fires +// when Vite *serves* the prebundled file, rewriting the runtime AMD +// check before it reaches the browser. +// +// Applied broadly to any module containing the AMD check (not just +// papaparse) — UMD wrappers all share the same shape, and we never +// want to take the AMD branch when Monaco's loader is around. +// +// Surfaces concretely on /functions/[slug]/invocations: papaparse +// pre-bundled into `.vite/deps/papaparse.js` retained the literal +// `"function" == typeof define && define.amd` check; Monaco's CDN +// loader installs `window.define` first, so papaparse's UMD takes the +// AMD branch and calls an anonymous `define([], t)` that Monaco +// rejects with "Can only have one anonymous define call per script +// file". +function umdAmdShortCircuit(): Plugin { + // Matches both unminified (`typeof define === 'function' && define.amd`) + // and minified (`"function" == typeof define && define.amd`) forms of + // the UMD AMD-detection check. Replaces the whole expression with + // `false` so dead-code elimination drops the AMD branch. + const AMD_CHECK_PATTERNS = [ + /typeof\s+define\s*===?\s*['"]function['"]\s*&&\s*define\.amd/g, + /['"]function['"]\s*===?\s*typeof\s+define\s*&&\s*define\.amd/g, + ] + + return { + name: 'studio-umd-amd-short-circuit', + enforce: 'pre', + transform(code, id) { + if (!code.includes('define.amd')) return + // Skip Monaco's loader.js if it ever ends up in our graph — it + // legitimately needs `define.amd` to register itself as AMD. + if (id.includes('monaco-editor/min/vs/loader')) return + let next = code + for (const pattern of AMD_CHECK_PATTERNS) { + next = next.replace(pattern, 'false') + } + if (next === code) return + return { code: next, map: null } + }, + } +} + +// Replace our `components/interfaces/GraphQL/GraphiQL` module with a no-op +// React component in SSR builds only. +// +// `@graphiql/react` transitively loads a codemirror addon that touches +// `document` at module-evaluation time. During the SPA shell prerender, +// that hard-crashes with "document is not defined" as soon as the graphiql +// chunk gets loaded. +// +// Stubbing `@graphiql/react` directly would require enumerating its 30+ named +// exports so Rolldown's static analysis is satisfied. Easier to stub the one +// internal consumer — `GraphiQL.tsx` only exposes a default-export component, +// and no SSR-reachable route renders it (the GraphiQL tab is client-only). +function ssrStubGraphiql(): Plugin { + return { + name: 'studio-ssr-stub-graphiql', + enforce: 'pre', + transform(_code, id, options) { + if (!options?.ssr) return + if (id.endsWith('/components/interfaces/GraphQL/GraphiQL.tsx')) { + return { code: 'export default function GraphiQLStub() { return null }', map: null } + } + }, + } +} + +// Build-time guard: scan the emitted client chunks for cross-chunk +// circular imports and fail the build if any are found. Catches the +// class of bug that produces runtime errors like +// "TypeError: is not a function" at module load — when chunk +// A imports a binding from chunk B and B (transitively) imports A +// back, ES module live-bindings can be undefined at the point the +// chunk that evaluates first tries to use them. +// +// Cycles are matched by chunk basename prefix (stripping the +// `assets/` directory and the `-.js` suffix), so the allowlist +// stays stable across builds even as Rolldown reassigns hashes. +const KNOWN_CHUNK_CYCLES: ReadonlyArray> = [ + // `ui` ↔ `TreeView` chunk cycle. `cva` lives in the `ui` chunk + // (Rolldown pools it there because many ui files use it), TreeView + // imports `cva` back from `ui` while `ui`'s barrel re-exports + // TreeView — runtime crash is "cva is not a function" at SSR. + // Worked around via the `class-variance-authority` manualChunks + // pin below; the chunk graph still surfaces the SCC even though + // the top-level `cva(...)` call inside TreeView no longer crashes. + // The variants below are the same SCC in different shapes — they + // shuffle as Rolldown re-chunks across merges. + ['LoadingLine', 'TreeView', 'ui'], + ['FormLayout', 'LoadingLine', 'TreeView', 'ui', 'index'], + ['LoadingLine', 'TreeView', 'ui', 'index'], +] + +function chunkPrefix(name: string): string { + return name + .replace(/^assets\//, '') + .replace(/-[A-Za-z0-9_-]{6,10}\.js$/, '') + .replace(/\.js$/, '') +} + +function isKnownCycle(scc: string[]): boolean { + const prefixes = new Set(scc.map(chunkPrefix)) + return KNOWN_CHUNK_CYCLES.some( + (known) => known.length === prefixes.size && known.every((p) => prefixes.has(p)) + ) +} + +function assertNoChunkCycles(): Plugin { + return { + name: 'studio-assert-no-chunk-cycles', + apply: 'build', + generateBundle(_options, bundle) { + const graph: Record> = {} + for (const [name, asset] of Object.entries(bundle)) { + if (asset.type !== 'chunk') continue + graph[name] = new Set(asset.imports.filter((i) => i in bundle)) + } + + // Tarjan's strongly-connected-components algorithm. Any SCC with + // more than one node is a cycle in the output chunk graph. + const indices: Record = {} + const lowlinks: Record = {} + const onStack: Record = {} + const stack: string[] = [] + const sccs: string[][] = [] + let nextIndex = 0 + + const strongconnect = (v: string) => { + indices[v] = nextIndex + lowlinks[v] = nextIndex + nextIndex++ + stack.push(v) + onStack[v] = true + for (const w of graph[v] || []) { + if (indices[w] === undefined) { + strongconnect(w) + lowlinks[v] = Math.min(lowlinks[v], lowlinks[w]) + } else if (onStack[w]) { + lowlinks[v] = Math.min(lowlinks[v], indices[w]) + } + } + if (lowlinks[v] === indices[v]) { + const scc: string[] = [] + let w: string | undefined + do { + w = stack.pop() + if (w === undefined) break + onStack[w] = false + scc.push(w) + } while (w !== v) + if (scc.length > 1) sccs.push(scc) + } + } + + for (const v of Object.keys(graph)) { + if (indices[v] === undefined) strongconnect(v) + } + + const unexpected = sccs.filter((scc) => !isKnownCycle(scc)) + if (unexpected.length === 0) return + + const summary = unexpected + .map((scc, i) => ` Cycle ${i + 1}:\n` + scc.map((c) => ` ${c}`).join('\n')) + .join('\n\n') + const msg = + `studio-assert-no-chunk-cycles: detected ${unexpected.length} new chunk-level cycle(s) in the client bundle.\n` + + `These cause "X is not a function" runtime errors at module-load time. ` + + `Either restructure the modules involved or add the cycle to KNOWN_CHUNK_CYCLES ` + + `in apps/studio/vite.config.ts.\n\n` + + summary + this.error(msg) + }, + } +} + +export default defineConfig(({ command, mode }) => { + // Match Next's "always production-NODE_ENV during build" behaviour. + // `pnpm run e2e:setup:selfhosted` invokes the build with a shell + // `NODE_ENV=test` so Next can pick up `.env.test` for env loading; + // Next overrides NODE_ENV back to 'production' internally before + // emitting code, so the bundle never sees 'test'. Vite respects the + // user's NODE_ENV by default and would bake `process.env.NODE_ENV === + // 'test'` into the client bundle, which trips vitest-only code paths + // (notably `API_URL` in `lib/constants/index.ts` pointing the browser + // at the vitest MSW host on port 3000, breaking every API fetch in + // e2e). Override here so `--mode test` still loads `.env.test` (via + // Vite's mode-based env resolution) while the bundle stays at + // `NODE_ENV='production'`, mirroring Next. + if (command === 'build') { + // Next's types declare NODE_ENV as read-only, so cast to assign it. + ;(process.env as Record).NODE_ENV = 'production' + } + + // Inline NEXT_PUBLIC_* env vars at build time so `process.env.NEXT_PUBLIC_*` + // works in the browser bundle (mirrors Next.js behaviour). + const env = loadEnv(mode, rootDir, '') + const publicEnvDefines = Object.fromEntries( + Object.entries(env) + .filter(([key]) => key.startsWith('NEXT_PUBLIC_')) + .map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]) + ) + + // Vercel auto-populates `NEXT_PUBLIC_VERCEL_*` for Next.js projects but not + // for other frameworks. Mirror that behaviour by re-exposing the unprefixed + // system vars under their `NEXT_PUBLIC_VERCEL_*` names so call sites that + // predate the TanStack migration keep working. + const vercelPublicVars = [ + 'VERCEL_ENV', + 'VERCEL_BRANCH_URL', + // Skew protection: the client pins its session to this deployment (see + // router.tsx). Both are build-time system env vars on Vercel. + 'VERCEL_DEPLOYMENT_ID', + 'VERCEL_SKEW_PROTECTION_ENABLED', + ] as const + for (const key of vercelPublicVars) { + const value = env[key] + if (value !== undefined) { + publicEnvDefines[`process.env.NEXT_PUBLIC_${key}`] = JSON.stringify(value) + } + } + + // Mirror Next's `basePath` via NEXT_PUBLIC_BASE_PATH. Unlike Next, TanStack + // Start has no single knob — the prefix has to be declared in three places + // (see BASE_PATH_REDIRECT_GUIDE.md): + // - Vite `base` — bakes the prefix into asset URLs in the + // built bundle. + // - tanstackStart router.basepath — must be passed explicitly. If + // omitted, the plugin's internal + // `deriveRouterBasepath` derives a value + // from `publicBase` and strips both + // leading and trailing slashes + // (`/dashboard` → `dashboard`), which then + // surfaces in `useRouter().basePath` + // consumers as relative URLs (e.g. + // `${BASE_PATH}/img/...` becomes + // `dashboard/img/...` and the browser + // resolves it against the current path). + // See planning.js:14 in + // @tanstack/start-plugin-core. + // - createRouter({ basepath }) — runtime navigation prefix; configured + // in router.tsx off the same env var + // (inlined via `define` above). + // Leaving the var empty keeps the app at `/` as today. + const basePath = env.NEXT_PUBLIC_BASE_PATH || undefined + + // Substitutions that have to apply to *both* our app source (via Vite's + // `define`) and any pre-bundled dependencies (via esbuild's optimizeDeps). + // The two pipelines don't share config — Vite's `define` only touches + // files going through Vite's transform, while optimizeDeps runs esbuild + // on `node_modules` deps with its own separate `define`. + // - `global` → `globalThis`: makes Node-style libs (`randombytes` via + // `generate-password-browser`, etc.) work in the browser. Surfaces + // on /auth/hooks via `randombytes/browser.js:16`. + // - `define.amd` → `false`: short-circuits the AMD branch in UMD + // wrappers (papaparse, others). Monaco's CDN loader installs an + // AMD-style `window.define` at runtime; without this substitution, + // UMD libs evaluate after Monaco has loaded and call an anonymous + // `define([], t)` that Monaco's queue rejects with "Can only have + // one anonymous define call per script file". Surfaces concretely + // on /functions/[slug] (papaparse pulled in by invocations). + // Monaco's loader.js itself runs from a CDN script tag (not in our + // bundle / not pre-bundled), so its own `define.amd = true` write + // isn't affected by the substitution. + const sharedDefines = { + global: 'globalThis', + 'define.amd': 'false', + } + + return { + server: { + port: 3000, + }, + resolve: { + tsconfigPaths: true, + }, + ...(basePath && { base: basePath }), + define: { + ...publicEnvDefines, + ...sharedDefines, + }, + // Circular-dep workaround: pin shared library code into dedicated + // chunks so per-component chunks don't import from a chunk that + // (transitively) imports them back. + // + // `class-variance-authority` — TreeView gets split into its own + // chunk that imports `cva` from the `ui` chunk while `ui` imports + // TreeView back. Leaves `cva` undefined at TreeView's top-level + // `cva(...)` call during SSR prerender. + // + // `lucide-react` — each icon (e.g. `FolderOpen`) gets a per-icon + // chunk that imports `createLucideIcon` from the `ui` chunk; the + // `ui` chunk in turn re-exports icons from `lucide-react`. The + // circular leaves `createLucideIcon` undefined when the icon + // chunk's top-level `createLucideIcon('FolderOpen', …)` runs — + // surfaces in the browser as "TypeError: e is not a function" at + // `folder-open-.js`. + // + // `react` / `react-dom` — pinning lucide-react alone caused + // Rolldown to suck React into the lucide-react chunk (lucide + // depends on React, no explicit pin further up the graph). That + // shifted live-bindings across the rest of the chunk graph and + // broke unrelated chunks (e.g. `Alert-.js` started crashing + // with `c is not a function` because its `styleHandler` import + // came in through the now-too-large `lucide-react` chunk). Pin + // React explicitly so it stays a leaf vendor chunk. + build: { + rollupOptions: { + output: { + manualChunks: (id) => { + if (id.includes('node_modules/class-variance-authority/')) { + return 'class-variance-authority' + } + // Pin React / React-DOM (and their JSX runtimes + scheduler) + // before lucide-react, so downstream chunks consume React + // from one place. Rolldown can still inline React into + // adjacent chunks for CJS interop, but the explicit pin + // anchors the canonical copy here. + if ( + /node_modules\/(react|react-dom|scheduler)(\/|$)/.test(id) || + /node_modules\/react\/jsx-(runtime|dev-runtime)/.test(id) + ) { + return 'react-vendor' + } + if (id.includes('node_modules/lucide-react/')) { + return 'lucide-react' + } + return undefined + }, + }, + }, + }, + css: { + // Disable PostCSS auto-discovery. Studio's postcss.config.cjs is kept + // for the Next build (`build:next`) and uses `@tailwindcss/postcss`, + // but under Vite we let `@tailwindcss/vite` (added below) handle + // Tailwind v4 directives directly. Running both plugins on the same + // CSS would double-process Tailwind output. + postcss: { plugins: [] }, + }, + ssr: { + // `lodash` is CJS; its named-export interop fails in Node ESM unless bundled. + // `next/*` must be bundled so our nextCompat shim wins — otherwise Vite's + // SSR externalizer leaves `next/router` as a runtime package import and + // Node resolves it to Next's real module. + // `tslib`'s Node ESM entry (`modules/index.js`) destructures from a + // default-imported CJS wrapper (`tslib.js`). When consumers like + // `@ai-sdk/amazon-bedrock` / `configcat-common` `import … from "tslib"` + // and that ESM-wrapper gets picked, Rolldown botches the flattened UMD + // body — "__extends is not a function" at SSR module evaluation time. + // Inlining `tslib` lets the bundler reach the pure ESM entry directly. + // `react-use` ships a CJS entry that Vite's SSR externalizer emits as + // `import pkg from 'react-use'` + destructure. Works locally but + // Vercel's Node resolves it differently and fails at module instantiate + // (`ModuleJob._instantiate`). Inlining sidesteps the interop entirely. + // `awesome-debounce-promise`'s CJS entry only emits + // `exports.default = fn` (no `module.exports = fn`, no `__esModule` + // flag). Node's CJS→ESM bridge therefore makes the default import the + // entire exports object `{ default: fn }`, and call sites like + // `AwesomeDebouncePromise(fn, 500)` crash with "is not a function" at + // SSR module evaluation. Surfaces on routes that load the table grid. + // `@sentry/nextjs`'s CJS entry doesn't surface `startSpan` (and other + // v8 APIs) onto the namespace shape Vite's SSR externalizer produces, + // so `import * as Sentry from '@sentry/nextjs'` + `Sentry.startSpan` + // crashes with "is not a function" inside the pg-meta proxy on the + // first table-editor request. + noExternal: [ + 'lodash', + /^next(\/|$)/, + 'tslib', + 'react-use', + 'awesome-debounce-promise', + '@sentry/nextjs', + ], + // Vite 8.0.13's SSR module runner evaluates `@sentry/nextjs`'s + // CJS file via `runInlinedModule` without the CJS-compat wrapper + // older vite applied, crashing with "exports is not defined" at + // SSR. Forcing pre-bundling via esbuild rewrites it to ESM + // before the SSR runner sees it. Only `@sentry/nextjs` needs + // this — the other CJS deps in `noExternal` work via vite's SSR + // transform; pre-bundling React-using deps (e.g. `react-use`) + // inlines a duplicate React into the bundle and breaks hook + // dedupe at SSR (useRef → null). + optimizeDeps: { + include: ['@sentry/nextjs'], + }, + }, + plugins: [ + nextCompat(), + ssrStubGraphiql(), + umdAmdShortCircuit(), + assertNoChunkCycles(), + devtools(), + tailwindcss(), + tanstackStart({ + srcDirectory: './', + spa: { + enabled: true, + }, + // Set `configuredBasepath` so `deriveRouterBasepath` short-circuits + // its slash-stripping branch. See the basePath comment above. + ...(basePath && { router: { basepath: basePath } }), + }), + viteReact(), + ], + } +}) diff --git a/apps/ui-library/package.json b/apps/ui-library/package.json index 99465e659cf..7f85d766cab 100644 --- a/apps/ui-library/package.json +++ b/apps/ui-library/package.json @@ -25,7 +25,7 @@ "@supabase/postgrest-js": "catalog:", "@supabase/supa-mdx-lint": "0.2.6-alpha", "@supabase/vue-blocks": "workspace:*", - "@tanstack/react-query": "^5.83.0", + "@tanstack/react-query": "~5.83.0", "@xyflow/react": "^12.10.1", "api-types": "workspace:*", "axios": "^1.12.0", @@ -74,8 +74,8 @@ "@shikijs/compat": "^1.1.7", "@supabase/ssr": "catalog:", "@supabase/supabase-js": "catalog:", - "@tanstack/react-router": "^1.168.0", - "@tanstack/react-start": "^1.168.0", + "@tanstack/react-router": "catalog:", + "@tanstack/react-start": "catalog:", "@types/common-tags": "^1.8.4", "@types/lodash": "^4.17.16", "@types/react": "catalog:", diff --git a/package.json b/package.json index 63813bec1dc..e1d95dfb8c4 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,8 @@ "test:studio": "turbo run test --filter=studio", "test:studio:watch": "turbo run test --filter=studio -- watch", "e2e:setup:cli": "supabase stop --all --no-backup ; supabase start --exclude studio,mailpit && if [ -z \"${CI}\" ]; then supabase db reset; fi && supabase status --output json > keys.json && node scripts/generateLocalEnv.js", - "e2e:setup:selfhosted": "SKIP_ASSET_UPLOAD=1 pnpm e2e:setup:cli && NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=4096\" pnpm run build:studio && NODE_ENV=test pnpm --prefix ./apps/studio start", - "e2e:setup:selfhosted:start-studio": "SKIP_ASSET_UPLOAD=1 NODE_ENV=test pnpm --prefix ./apps/studio start", + "e2e:setup:selfhosted": "SKIP_ASSET_UPLOAD=1 pnpm e2e:setup:cli && NODE_ENV=test MODE=test NODE_OPTIONS=\"--max-old-space-size=4096\" pnpm run build:studio && NODE_ENV=test MODE=test pnpm --prefix ./apps/studio start", + "e2e:setup:selfhosted:start-studio": "SKIP_ASSET_UPLOAD=1 NODE_ENV=test MODE=test pnpm --prefix ./apps/studio start", "e2e:setup:platform": "SKIP_ASSET_UPLOAD=1 NODE_OPTIONS=\"--max-old-space-size=4096\" pnpm run build:studio && pnpm --prefix ./apps/studio start", "e2e": "pnpm --prefix e2e/studio run e2e", "e2e:ui": "pnpm --prefix e2e/studio run e2e:ui", @@ -67,13 +67,6 @@ "pnpm": "10.24", "node": ">=22" }, - "keywords": [ - "postgres", - "firebase", - "storage", - "functions", - "database", - "auth" - ], + "keywords": ["postgres", "firebase", "storage", "functions", "database", "auth"], "packageManager": "pnpm@10.24.0" } diff --git a/packages/ui-patterns/package.json b/packages/ui-patterns/package.json index 72b4cd0a574..78e1def0a02 100644 --- a/packages/ui-patterns/package.json +++ b/packages/ui-patterns/package.json @@ -805,7 +805,7 @@ "@std/toml": "jsr:^1.0.11", "@supabase/sql-to-rest": "^0.1.6", "@supabase/supabase-js": "catalog:", - "@tanstack/react-table": "^8.21.3", + "@tanstack/react-table": "catalog:", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", diff --git a/packages/ui/src/components/AnimatedCounter/AnimatedCounter.tsx b/packages/ui/src/components/AnimatedCounter/AnimatedCounter.tsx index 59103eab284..b50199fbcb2 100644 --- a/packages/ui/src/components/AnimatedCounter/AnimatedCounter.tsx +++ b/packages/ui/src/components/AnimatedCounter/AnimatedCounter.tsx @@ -1,7 +1,7 @@ 'use client' import { animate, motion, useInView, useMotionValue, useTransform } from 'framer-motion' -import React, { FC, useEffect, useRef } from 'react' +import { FC, useEffect, useRef } from 'react' import { cn } from '../../lib/utils/cn' diff --git a/packages/ui/src/components/Typography/Typography.tsx b/packages/ui/src/components/Typography/Typography.tsx index adf311e2e1b..059f84acf14 100644 --- a/packages/ui/src/components/Typography/Typography.tsx +++ b/packages/ui/src/components/Typography/Typography.tsx @@ -1,5 +1,3 @@ -import React from 'react' - import Link from './Link' import Text from './Text' import Title from './Title' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 673021e2845..1fce526b355 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,6 +24,15 @@ catalogs: '@supabase/supabase-js': specifier: 2.108.2 version: 2.108.2 + '@tanstack/react-router': + specifier: ^1.169.2 + version: 1.170.10 + '@tanstack/react-start': + specifier: ^1.167.65 + version: 1.168.18 + '@tanstack/react-table': + specifier: ^8.21.3 + version: 8.21.3 '@types/node': specifier: ^22.0.0 version: 22.13.14 @@ -96,6 +105,7 @@ overrides: cacache>tar: ^7.5.11 dompurify: ^3.3.2 express-rate-limit>ip-address: ^10.1.1 + h3@1: 1.15.11 lodash: ^4.18.1 lodash-es: ^4.18.1 node-gyp>tar: ^7.5.11 @@ -169,7 +179,7 @@ importers: specifier: ^3.1.1 version: 3.3.1(react-hook-form@7.72.1(react@19.2.6)) '@tanstack/react-table': - specifier: ^8.21.3 + specifier: 'catalog:' version: 8.21.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) common: specifier: workspace:* @@ -356,7 +366,7 @@ importers: specifier: 'catalog:' version: 2.108.2 '@tanstack/react-query': - specifier: ^5.13.4 + specifier: ~5.83.0 version: 5.83.0(react@19.2.6) acorn: specifier: ^8.11.3 @@ -925,9 +935,21 @@ importers: '@number-flow/react': specifier: ^0.3.2 version: 0.3.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-slider': + specifier: ^1.1.2 + version: 1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-slot': + specifier: ^1.2.0 + version: 1.2.4(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-visually-hidden': + specifier: ^1.2.3 + version: 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@sentry/nextjs': specifier: 'catalog:' version: 10.27.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@16.2.6(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react@19.2.6)(supports-color@8.1.1)(webpack@5.105.4(esbuild@0.28.1)) + '@sentry/react': + specifier: ^10.27.0 + version: 10.27.0(react@19.2.6) '@std/path': specifier: npm:@jsr/std__path@^1.0.8 version: '@jsr/std__path@1.0.8' @@ -961,21 +983,36 @@ importers: '@supabase/supabase-js': specifier: 'catalog:' version: 2.108.2 + '@tanstack/react-devtools': + specifier: ^0.10.3 + version: 0.10.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(solid-js@1.9.13) '@tanstack/react-hotkeys': - specifier: ^0.9.1 - version: 0.9.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: ^0.10.0 + version: 0.10.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@tanstack/react-query': - specifier: ^5.0.0 + specifier: ~5.83.0 version: 5.83.0(react@19.2.6) '@tanstack/react-query-devtools': - specifier: ^5.0.0 + specifier: ~5.90.2 version: 5.90.2(@tanstack/react-query@5.83.0(react@19.2.6))(react@19.2.6) + '@tanstack/react-router': + specifier: 'catalog:' + version: 1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@tanstack/react-router-devtools': + specifier: ^1.166.13 + version: 1.167.0(@tanstack/react-router@1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@tanstack/router-core@1.171.8)(csstype@3.2.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@tanstack/react-router-ssr-query': + specifier: ^1.166.12 + version: 1.167.1(@tanstack/query-core@5.83.0)(@tanstack/react-query@5.83.0(react@19.2.6))(@tanstack/react-router@1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@tanstack/router-core@1.171.8)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@tanstack/react-start': + specifier: 'catalog:' + version: 1.168.18(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1)) '@tanstack/react-table': - specifier: ^8.21.3 + specifier: 'catalog:' version: 8.21.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@tanstack/react-virtual': - specifier: ^3.13.12 - version: 3.13.12(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: ^3.13.24 + version: 3.14.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@types/d3-geo': specifier: ^3.1.0 version: 3.1.0 @@ -1220,6 +1257,15 @@ importers: '@smithy/property-provider': specifier: ^4.0.4 version: 4.0.4 + '@tailwindcss/container-queries': + specifier: ^0.1.1 + version: 0.1.1(tailwindcss@4.2.4) + '@tailwindcss/vite': + specifier: 4.2.4 + version: 4.2.4(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0)) + '@tanstack/devtools-vite': + specifier: ^0.6.0 + version: 0.6.1(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0)) '@testing-library/dom': specifier: ^10.0.0 version: 10.4.1 @@ -1289,6 +1335,9 @@ importers: '@types/zxcvbn': specifier: ^4.4.1 version: 4.4.2 + '@vercel/config': + specifier: ^0.2.1 + version: 0.2.1 '@vitejs/plugin-react': specifier: 'catalog:' version: 6.0.1(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0)) @@ -1395,7 +1444,7 @@ importers: specifier: workspace:* version: link:../../blocks/vue '@tanstack/react-query': - specifier: ^5.83.0 + specifier: ~5.83.0 version: 5.83.0(react@19.2.6) '@xyflow/react': specifier: ^12.10.1 @@ -1537,10 +1586,10 @@ importers: specifier: 'catalog:' version: 2.108.2 '@tanstack/react-router': - specifier: ^1.168.0 - version: 1.168.18(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: 'catalog:' + version: 1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@tanstack/react-start': - specifier: ^1.168.0 + specifier: 'catalog:' version: 1.168.18(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4) '@types/common-tags': specifier: ^1.8.4 @@ -1919,7 +1968,7 @@ importers: specifier: ^2.1.1 version: 2.1.1 h3: - specifier: ^1.15.10 + specifier: 1.15.11 version: 1.15.11 lucide-vue-next: specifier: ^0.562.0 @@ -2526,7 +2575,7 @@ importers: specifier: 'catalog:' version: 2.108.2 '@tanstack/react-table': - specifier: ^8.21.3 + specifier: 'catalog:' version: 8.21.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) class-variance-authority: specifier: ^0.7.1 @@ -7710,6 +7759,36 @@ packages: resolution: {integrity: sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==} engines: {node: '>=18.0.0'} + '@solid-primitives/event-listener@2.4.5': + resolution: {integrity: sha512-nwRV558mIabl4yVAhZKY8cb6G+O1F0M6Z75ttTu5hk+SxdOnKSGj+eetDIu7Oax1P138ZdUU01qnBPR8rnxaEA==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/keyboard@1.3.5': + resolution: {integrity: sha512-sav+l+PL+74z3yaftVs7qd8c2SXkqzuxPOVibUe5wYMt+U5Hxp3V3XCPgBPN2I6cANjvoFtz0NiU8uHVLdi9FQ==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/resize-observer@2.1.5': + resolution: {integrity: sha512-AiyTknKcNBaKHbcSMuxtSNM8FjIuiSuFyFghdD0TcCMU9hKi9EmsC5pjfjDwxE+5EueB1a+T/34PLRI5vbBbKw==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/rootless@1.5.3': + resolution: {integrity: sha512-N8cIDAHbWcLahNRLr0knAAQvXyEdEMoAZvIMZKmhNb1mlx9e2UOv9BRD5YNwQUJwbNoYVhhLwFOEOcVXFx0HqA==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/static-store@0.1.3': + resolution: {integrity: sha512-uxez7SXnr5GiRnzqO2IEDjOJRIXaG+0LZLBizmUA1FwSi+hrpuMzVBwyk70m4prcl8X6FDDXUl9O8hSq8wHbBQ==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/utils@6.4.0': + resolution: {integrity: sha512-AeGTBg8Wtkh/0s+evyLtP8piQoS4wyqqQaAFs2HJcFMMjYAtUgo+ZPduRXLjPlqKVc2ejeR544oeqpbn8Egn8A==} + peerDependencies: + solid-js: ^1.6.12 + '@speed-highlight/core@1.2.14': resolution: {integrity: sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==} @@ -7929,6 +8008,11 @@ packages: '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@tailwindcss/container-queries@0.1.1': + resolution: {integrity: sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==} + peerDependencies: + tailwindcss: '>=3.2.0' + '@tailwindcss/forms@0.5.11': resolution: {integrity: sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA==} peerDependencies: @@ -8036,21 +8120,58 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 || ^8 + '@tanstack/devtools-client@0.0.6': + resolution: {integrity: sha512-f85ZJXJnDIFOoykG/BFIixuAevJovCvJF391LPs6YjBAPhGYC50NWlx1y4iF/UmK5/cCMx+/JqI5SBOz7FanQQ==} + engines: {node: '>=18'} + + '@tanstack/devtools-client@0.0.7': + resolution: {integrity: sha512-bAqBnXQlg/1PqmIC3XhqzG8jV3YmUQ41fD9VlOVzSilFBD4Kp6WJFJa+7N6TvlYXqMAy8xoeF9gg0d2Lt74OEQ==} + engines: {node: '>=18'} + + '@tanstack/devtools-event-bus@0.4.1': + resolution: {integrity: sha512-cNnJ89Q021Zf883rlbBTfsaxTfi2r73/qejGtyTa7ksErF3hyDyAq1aTbo5crK9dAL7zSHh9viKY1BtMls1QOA==} + engines: {node: '>=18'} + + '@tanstack/devtools-event-bus@0.4.2': + resolution: {integrity: sha512-2LHzhwBFlKHCcklsQrGe8TeyjHd4XAF8nuCO6wHmva5fePUkJUULbu6CsCNAlGlCi0KkEsMXZSvRdR4HgMq4yA==} + engines: {node: '>=18'} + + '@tanstack/devtools-event-client@0.4.4': + resolution: {integrity: sha512-6T5Yop/793YI+H+5J8Hsyj4kCih9sl4t3ElLgKioW5hk3ocn+ZdSJ94tT7vL7uabxSugWYBZlOTMPzEw2puvQw==} + engines: {node: '>=18'} + hasBin: true + + '@tanstack/devtools-ui@0.5.3': + resolution: {integrity: sha512-iJjwWtdXhUGpeHyyW9+3NhXhmlVFhh3v3UBNKCouykG9UFXEtneVVNXgSRpd70DeYJFmvKOY19LafKRI5/cM7A==} + engines: {node: '>=18'} + peerDependencies: + solid-js: '>=1.9.7' + + '@tanstack/devtools-vite@0.6.1': + resolution: {integrity: sha512-4NMdFwVDoJZlh4gnKl/LJYSoEKuZr2GiFgbCITfaKYWXk6XbXilclfLwAAdoX5gF2p0mEPPv5JCuAlbJgO9aWQ==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + + '@tanstack/devtools@0.12.4': + resolution: {integrity: sha512-fYZ0KTEpKq7JyjULDe4kGQBN77aw5jtULs1MaVraWvwtcoIwe4UOKV48UJQ9/mEQMFTytbgKxYk2QaSDgI/Znw==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + solid-js: '>=1.9.7' + '@tanstack/eslint-plugin-query@5.91.2': resolution: {integrity: sha512-UPeWKl/Acu1IuuHJlsN+eITUHqAaa9/04geHHPedY8siVarSaWprY0SVMKrkpKfk5ehRT7+/MZ5QwWuEtkWrFw==} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - '@tanstack/history@1.161.6': - resolution: {integrity: sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg==} - engines: {node: '>=20.19'} - '@tanstack/history@1.162.0': resolution: {integrity: sha512-79pf/RkhteYZTRgcR4F9kbk84P2N8rugQJswxfIqovlbRiT3yI7eBE+5QorIrZaOKktsgzRlXh1l/du/xpl4iA==} engines: {node: '>=20.19'} - '@tanstack/hotkeys@0.7.1': - resolution: {integrity: sha512-YHVO1z6wnvUCu7bg870Kv5k2D+FIuIOSIcbN0dAmTTsJ3mLMDLwcTVx0qVaq+SZp1B514JJTqGVstvUp85yIpQ==} + '@tanstack/hotkeys@0.8.0': + resolution: {integrity: sha512-vqH7X9nb0MTJ/O08++dB5bP9jgj4+BIPOUu/U+6myG86lDsirZSVSobpq5UQpE7nBuk62i8eIYeOhd+OMl/UrA==} engines: {node: '>=18'} '@tanstack/query-core@5.83.0': @@ -8059,8 +8180,17 @@ packages: '@tanstack/query-devtools@5.90.1': resolution: {integrity: sha512-GtINOPjPUH0OegJExZ70UahT9ykmAhmtNVcmtdnOZbxLwT7R5OmRztR5Ahe3/Cu7LArEmR6/588tAycuaWb1xQ==} - '@tanstack/react-hotkeys@0.9.1': - resolution: {integrity: sha512-/qdQUUVkYAHAWRGdFXqFgWpW/S+a6OzkvxWNWKLLDHQODJlO6EPBPa073CglaafBfzig58RK07T09ET+NnZhpg==} + '@tanstack/react-devtools@0.10.7': + resolution: {integrity: sha512-AYHQH06uuK07Asqq8eASgJjpILlaFBpjnTesxx1JVHGoBl4ijwbyIlKnj3Z8+M8sEOPn5mvtV5o6mielPXKHWg==} + engines: {node: '>=18'} + peerDependencies: + '@types/react': '>=16.8' + '@types/react-dom': '>=16.8' + react: '>=16.8' + react-dom: '>=16.8' + + '@tanstack/react-hotkeys@0.10.0': + resolution: {integrity: sha512-GwOSndI5j3qBVYTmgP1mYyRTnlxb2MS17cwGlsavSxMQPSnmDf+m3LzMIpRMs+3zzQMjg3cYhHsFYizYlFI2tw==} engines: {node: '>=18'} peerDependencies: react: '>=16.8' @@ -8077,10 +8207,25 @@ packages: peerDependencies: react: ^18 || ^19 - '@tanstack/react-router@1.168.18': - resolution: {integrity: sha512-RmBptS3/qtkGhvG/u41JWOgxz1FIWybBz7iBTgLUIoFkqOj6NE4XlhUOsP2fabxACtbZdJnpvCWcJFWpWGIngw==} + '@tanstack/react-router-devtools@1.167.0': + resolution: {integrity: sha512-nGw095EG7IHx0h5NtlEmzf6vcCTaFNPWdTSuDKazajhN0ct/v/TkekJ9J6KYUCeV1a8/2ZmToc58M+0rrOyn7w==} engines: {node: '>=20.19'} peerDependencies: + '@tanstack/react-router': ^1.170.0 + '@tanstack/router-core': ^1.170.0 + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + peerDependenciesMeta: + '@tanstack/router-core': + optional: true + + '@tanstack/react-router-ssr-query@1.167.1': + resolution: {integrity: sha512-W9j5JPnBikyafvuUfykFfHIWod58OAbAAa5leNkXBcoDoocghMmu6w9uZOmUZvAWT7CSvgj5tBUtF7CM2OoHXQ==} + engines: {node: '>=20.19'} + peerDependencies: + '@tanstack/query-core': '>=5.90.0' + '@tanstack/react-query': '>=5.90.0' + '@tanstack/react-router': '>=1.127.0' react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' @@ -8139,6 +8284,12 @@ packages: vite: optional: true + '@tanstack/react-store@0.11.0': + resolution: {integrity: sha512-tX4YXh3PDkmpvGQWkWqKpzs/MSqbtuwY9dWdWhtV9Q50PmO+jOkUKIWIX4G85dwt7lxdHLXsiaEKPdKmC8F41w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@tanstack/react-store@0.9.3': resolution: {integrity: sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==} peerDependencies: @@ -8152,21 +8303,26 @@ packages: react: '>=16.8' react-dom: '>=16.8' - '@tanstack/react-virtual@3.13.12': - resolution: {integrity: sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==} + '@tanstack/react-virtual@3.14.3': + resolution: {integrity: sha512-k/cnHPVaOfn46hSbiY6n4Dzf4QjCGWSF40zR5QIIYUqPAjpA6TN7InfYmcMiDVQGP2iUn9xsRbAl8u1v3UmeVQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/router-core@1.168.14': - resolution: {integrity: sha512-UhCJtjNrd5wcTmhgB2HyUP0+Rj1M7BD4dS11YsF9x6VC2KH/eqxzs/vK+nN5f+cOhPOLZdmLkWMW+WGmacZ8HA==} - engines: {node: '>=20.19'} - hasBin: true - '@tanstack/router-core@1.171.8': resolution: {integrity: sha512-PbrTBbofFcacrH3RLgHYILRqTFnAGq+gXrXoA/vo7qUSkJpSO4GWfLtrtCahD4VayzRm19IPwcjPPLEugag6pw==} engines: {node: '>=20.19'} + '@tanstack/router-devtools-core@1.168.0': + resolution: {integrity: sha512-wQoQhlBK7nlZgqzaqdYXKWNTpdHdsaREdaPhFZVH0/Ador+F+eM3/NF2i3f2LPeS0GgKraZUQXe1Q/1+KHyEYg==} + engines: {node: '>=20.19'} + peerDependencies: + '@tanstack/router-core': ^1.170.0 + csstype: ^3.0.10 + peerDependenciesMeta: + csstype: + optional: true + '@tanstack/router-generator@1.167.12': resolution: {integrity: sha512-FGr7nn6VhjL53TUCTyDgApSkAYRxhId+v0HVQdSu0ADkNuHY+sUnYEMqiF6aN82jYWuXzrSL1xazg6/rfEP82g==} engines: {node: '>=20.19'} @@ -8192,6 +8348,13 @@ packages: webpack: optional: true + '@tanstack/router-ssr-query-core@1.169.1': + resolution: {integrity: sha512-rngux8s/3mPQzcjLYDLkNU31coYVyCgrVTfpdwqUdY5jIEHqGTXrO73DTkPR1PppwYUeVhmNCgl8TctRcnupjg==} + engines: {node: '>=20.19'} + peerDependencies: + '@tanstack/query-core': '>=5.90.0' + '@tanstack/router-core': '>=1.127.0' + '@tanstack/router-utils@1.162.1': resolution: {integrity: sha512-62layyTGmclHDQS/eidwKRfN1hhCKwViG7iEBcVmL0MXgcAB3OOucWCEcDDGd9Cu11H6b4QQ5oOo47MWIqwz0A==} engines: {node: '>=20.19'} @@ -8224,6 +8387,9 @@ packages: resolution: {integrity: sha512-geCsFpgCt+S2gQjzXILdPZ9obIxtzuN8C0Esc1fcyWZhwYyqo4C8G2o/dIck8xGixCMSvOsxL5NkCXDdOm2KOQ==} engines: {node: '>=22.12.0'} + '@tanstack/store@0.11.0': + resolution: {integrity: sha512-WlzzCt3xi0G6pCAJu1U+2jiECwabETDpQDi3hfkFZvJii9AuZqEKbOiVarX1/bWhTNjU486yQtJCCasi/0q+Cw==} + '@tanstack/store@0.9.3': resolution: {integrity: sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==} @@ -8231,8 +8397,8 @@ packages: resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} engines: {node: '>=12'} - '@tanstack/virtual-core@3.13.12': - resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==} + '@tanstack/virtual-core@3.17.1': + resolution: {integrity: sha512-VZyW2Uiml5tmBZwPGrSD3Sz73OxzljQMCmzYHsUTPEuTsERf5xwa+uWb01xEzkz3ZSYTjj8NEb/mKHvgKxyZdA==} '@tanstack/virtual-file-routes@1.162.0': resolution: {integrity: sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA==} @@ -8775,6 +8941,10 @@ packages: '@usercentrics/cmp-browser-sdk@4.42.0': resolution: {integrity: sha512-/cik01TdeiYUV1+EasK83Ip05TDqmowM5tmWGfRDdaOrneSic5BedBdJHKya4pP2vwiYAtzDYVJe4BwPt2M16g==} + '@vercel/config@0.2.1': + resolution: {integrity: sha512-MWLB1WjVqj2z7oMBzhQez+uMr1JjK3Gq50tZ1EgcJpOua1Dyt2ITLNjh+nNyO0Z8VXdurszFeXvaDnswJRgSgQ==} + hasBin: true + '@vercel/functions@1.6.0': resolution: {integrity: sha512-R6FKQrYT5MZs5IE1SqeCJWxMuBdHawFcCZboKKw8p7s+6/mcd55Gx6tWmyKnQTyrSEA04NH73Tc9CbqpEle8RA==} engines: {node: '>= 16'} @@ -8810,6 +8980,9 @@ packages: resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==} engines: {node: '>= 20'} + '@vercel/routing-utils@6.1.1': + resolution: {integrity: sha512-kUJn6CRuLvua5xvDZTJu1rC8GLdniC8D5VIzoJKm0dUdcUxv3LEx+pi8dokxWS+YM7cIs1Z3Y8ms/BDW1iBzzA==} + '@vitejs/plugin-react@6.0.1': resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -9653,6 +9826,10 @@ packages: resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + change-case-all@1.0.15: resolution: {integrity: sha512-3+GIFhk3sNuvFAJKU46o26OdzudQlPNBCu1ZQi3cMeMHhty1bhDxu2WrEilVNYaGvqUtR1VSigFcJOiS13dRhQ==} @@ -11721,6 +11898,11 @@ packages: globrex@0.1.2: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + goober@2.1.19: + resolution: {integrity: sha512-U7veizMqxyKlM58+Z5j2ngJBH/r9siDmxpvNxSw0PylF6WQvrASJEZrxh1hidRBJc2jqoBVSyOban5u8m+6Rxg==} + peerDependencies: + csstype: ^3.0.10 + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -14610,6 +14792,9 @@ packages: path-to-regexp@0.1.13: resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==} + path-to-regexp@6.1.0: + resolution: {integrity: sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw==} + path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} @@ -14991,6 +15176,10 @@ packages: resolution: {integrity: sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw==} engines: {node: '>=20'} + pretty-cache-header@1.0.0: + resolution: {integrity: sha512-xtXazslu25CdnGnUkByU1RoOjK55TqwatJkjjJLg5ZAdz2Lngko/mmaUgeET36P2GMlNwh3fdM7FWBO717pNcw==} + engines: {node: '>=12.13'} + pretty-format@27.5.1: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -15880,12 +16069,6 @@ packages: resolution: {integrity: sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==} engines: {node: '>=20.0.0'} - seroval-plugins@1.5.0: - resolution: {integrity: sha512-EAHqADIQondwRZIdeW2I636zgsODzoBDwb3PT/+7TLDWyw1Dy/Xv7iGUIEXXav7usHDE9HVhOU61irI3EnyyHA==} - engines: {node: '>=10'} - peerDependencies: - seroval: ^1.0 - seroval-plugins@1.5.4: resolution: {integrity: sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw==} engines: {node: '>=10'} @@ -16114,6 +16297,9 @@ packages: resolution: {integrity: sha512-NlGELfPrgX2f1TAAcz0WawlLn+0r3FyhhCRpFFK2CemXenPYvzMWWZINv3eDNo9ucdwme7oCHRY0Jnbs4aIkog==} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + solid-js@1.9.13: + resolution: {integrity: sha512-6hJeJMOcEX8ktqjpDoJZEmld3ijvcvWBDtiXBm7f4332SiFN66QeAQI1REQshvyUoISsSeJ4PHDauKYbwao9JQ==} + sonner@1.5.0: resolution: {integrity: sha512-FBjhG/gnnbN6FY0jaNnqZOMmB73R+5IiyYAw8yBj7L54ER7HB3fOSE5OFiQiE2iXWxeXKvg6fIP4LtVppHEdJA==} peerDependencies: @@ -16565,6 +16751,10 @@ packages: resolution: {integrity: sha512-YBGpG4bWsHoPvofT6y/5iqulfXIiIErl5B0LdtHT1mGXDFTAhhRrbUpTvBgYbovr+3cKblya2WAOcpoy90XguA==} engines: {node: '>=16'} + timestring@6.0.0: + resolution: {integrity: sha512-wMctrWD2HZZLuIlchlkE2dfXJh7J2KDI9Dwl+2abPYg0mswQHfOAyQW3jJg1pY5VfttSINZuKcXoB3FGypVklA==} + engines: {node: '>=8'} + tiny-inflate@1.0.3: resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} @@ -20057,7 +20247,7 @@ snapshots: '@floating-ui/react': 0.26.28(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@react-aria/focus': 3.20.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@react-aria/interactions': 3.25.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@tanstack/react-virtual': 3.13.12(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@tanstack/react-virtual': 3.14.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) use-sync-external-store: 1.6.0(react@19.2.6) @@ -23969,6 +24159,40 @@ snapshots: dependencies: tslib: 2.8.1 + '@solid-primitives/event-listener@2.4.5(solid-js@1.9.13)': + dependencies: + '@solid-primitives/utils': 6.4.0(solid-js@1.9.13) + solid-js: 1.9.13 + + '@solid-primitives/keyboard@1.3.5(solid-js@1.9.13)': + dependencies: + '@solid-primitives/event-listener': 2.4.5(solid-js@1.9.13) + '@solid-primitives/rootless': 1.5.3(solid-js@1.9.13) + '@solid-primitives/utils': 6.4.0(solid-js@1.9.13) + solid-js: 1.9.13 + + '@solid-primitives/resize-observer@2.1.5(solid-js@1.9.13)': + dependencies: + '@solid-primitives/event-listener': 2.4.5(solid-js@1.9.13) + '@solid-primitives/rootless': 1.5.3(solid-js@1.9.13) + '@solid-primitives/static-store': 0.1.3(solid-js@1.9.13) + '@solid-primitives/utils': 6.4.0(solid-js@1.9.13) + solid-js: 1.9.13 + + '@solid-primitives/rootless@1.5.3(solid-js@1.9.13)': + dependencies: + '@solid-primitives/utils': 6.4.0(solid-js@1.9.13) + solid-js: 1.9.13 + + '@solid-primitives/static-store@0.1.3(solid-js@1.9.13)': + dependencies: + '@solid-primitives/utils': 6.4.0(solid-js@1.9.13) + solid-js: 1.9.13 + + '@solid-primitives/utils@6.4.0(solid-js@1.9.13)': + dependencies: + solid-js: 1.9.13 + '@speed-highlight/core@1.2.14': {} '@standard-schema/spec@1.0.0': {} @@ -24178,6 +24402,10 @@ snapshots: dependencies: tslib: 2.8.1 + '@tailwindcss/container-queries@0.1.1(tailwindcss@4.2.4)': + dependencies: + tailwindcss: 4.2.4 + '@tailwindcss/forms@0.5.11(tailwindcss@4.2.4)': dependencies: mini-svg-data-uri: 1.4.4 @@ -24264,6 +24492,80 @@ snapshots: tailwindcss: 4.2.4 vite: 7.3.5(@types/node@22.13.14)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0) + '@tailwindcss/vite@4.2.4(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))': + dependencies: + '@tailwindcss/node': 4.2.4 + '@tailwindcss/oxide': 4.2.4 + tailwindcss: 4.2.4 + vite: 8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0) + + '@tanstack/devtools-client@0.0.6': + dependencies: + '@tanstack/devtools-event-client': 0.4.4 + + '@tanstack/devtools-client@0.0.7': + dependencies: + '@tanstack/devtools-event-client': 0.4.4 + + '@tanstack/devtools-event-bus@0.4.1': + dependencies: + ws: 8.21.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@tanstack/devtools-event-bus@0.4.2': + dependencies: + ws: 8.21.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@tanstack/devtools-event-client@0.4.4': {} + + '@tanstack/devtools-ui@0.5.3(csstype@3.2.3)(solid-js@1.9.13)': + dependencies: + clsx: 2.1.1 + dayjs: 1.11.20 + goober: 2.1.19(csstype@3.2.3) + solid-js: 1.9.13 + transitivePeerDependencies: + - csstype + + '@tanstack/devtools-vite@0.6.1(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))': + dependencies: + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/generator': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) + '@babel/types': 7.29.7 + '@tanstack/devtools-client': 0.0.6 + '@tanstack/devtools-event-bus': 0.4.1 + chalk: 5.6.2 + launch-editor: 2.14.1 + picomatch: 4.0.4 + vite: 8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@tanstack/devtools@0.12.4(csstype@3.2.3)(solid-js@1.9.13)': + dependencies: + '@solid-primitives/event-listener': 2.4.5(solid-js@1.9.13) + '@solid-primitives/keyboard': 1.3.5(solid-js@1.9.13) + '@solid-primitives/resize-observer': 2.1.5(solid-js@1.9.13) + '@tanstack/devtools-client': 0.0.7 + '@tanstack/devtools-event-bus': 0.4.2 + '@tanstack/devtools-ui': 0.5.3(csstype@3.2.3)(solid-js@1.9.13) + clsx: 2.1.1 + goober: 2.1.19(csstype@3.2.3) + solid-js: 1.9.13 + transitivePeerDependencies: + - bufferutil + - csstype + - utf-8-validate + '@tanstack/eslint-plugin-query@5.91.2(eslint@9.37.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.2)': dependencies: '@typescript-eslint/utils': 8.48.0(eslint@9.37.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.2) @@ -24272,22 +24574,33 @@ snapshots: - supports-color - typescript - '@tanstack/history@1.161.6': {} - '@tanstack/history@1.162.0': {} - '@tanstack/hotkeys@0.7.1': + '@tanstack/hotkeys@0.8.0': dependencies: - '@tanstack/store': 0.9.3 + '@tanstack/store': 0.11.0 '@tanstack/query-core@5.83.0': {} '@tanstack/query-devtools@5.90.1': {} - '@tanstack/react-hotkeys@0.9.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@tanstack/react-devtools@0.10.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(solid-js@1.9.13)': dependencies: - '@tanstack/hotkeys': 0.7.1 - '@tanstack/react-store': 0.9.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@tanstack/devtools': 0.12.4(csstype@3.2.3)(solid-js@1.9.13) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + transitivePeerDependencies: + - bufferutil + - csstype + - solid-js + - utf-8-validate + + '@tanstack/react-hotkeys@0.10.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@tanstack/hotkeys': 0.8.0 + '@tanstack/react-store': 0.11.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) @@ -24302,14 +24615,27 @@ snapshots: '@tanstack/query-core': 5.83.0 react: 19.2.6 - '@tanstack/react-router@1.168.18(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@tanstack/react-router-devtools@1.167.0(@tanstack/react-router@1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@tanstack/router-core@1.171.8)(csstype@3.2.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@tanstack/history': 1.161.6 - '@tanstack/react-store': 0.9.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@tanstack/router-core': 1.168.14 - isbot: 5.1.36 + '@tanstack/react-router': 1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@tanstack/router-devtools-core': 1.168.0(@tanstack/router-core@1.171.8)(csstype@3.2.3) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@tanstack/router-core': 1.171.8 + transitivePeerDependencies: + - csstype + + '@tanstack/react-router-ssr-query@1.167.1(@tanstack/query-core@5.83.0)(@tanstack/react-query@5.83.0(react@19.2.6))(@tanstack/react-router@1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@tanstack/router-core@1.171.8)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@tanstack/query-core': 5.83.0 + '@tanstack/react-query': 5.83.0(react@19.2.6) + '@tanstack/react-router': 1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@tanstack/router-ssr-query-core': 1.169.1(@tanstack/query-core@5.83.0)(@tanstack/router-core@1.171.8) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + transitivePeerDependencies: + - '@tanstack/router-core' '@tanstack/react-router@1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: @@ -24328,6 +24654,28 @@ snapshots: react: 19.2.6 react-dom: 19.2.6(react@19.2.6) + '@tanstack/react-start-rsc@0.1.17(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1))': + dependencies: + '@tanstack/react-router': 1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@tanstack/react-start-server': 1.167.13(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@tanstack/router-core': 1.171.8 + '@tanstack/router-utils': 1.162.1(supports-color@8.1.1) + '@tanstack/start-client-core': 1.170.6 + '@tanstack/start-fn-stubs': 1.162.0 + '@tanstack/start-plugin-core': 1.171.10(@tanstack/react-router@1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1)) + '@tanstack/start-server-core': 1.169.8 + '@tanstack/start-storage-context': 1.167.10 + pathe: 2.0.3 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + transitivePeerDependencies: + - '@rsbuild/core' + - crossws + - supports-color + - vite + - vite-plugin-solid + - webpack + '@tanstack/react-start-rsc@0.1.17(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4)': dependencies: '@tanstack/react-router': 1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -24362,6 +24710,29 @@ snapshots: transitivePeerDependencies: - crossws + '@tanstack/react-start@1.168.18(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1))': + dependencies: + '@tanstack/react-router': 1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@tanstack/react-start-client': 1.168.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@tanstack/react-start-rsc': 0.1.17(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1)) + '@tanstack/react-start-server': 1.167.13(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@tanstack/router-utils': 1.162.1(supports-color@8.1.1) + '@tanstack/start-client-core': 1.170.6 + '@tanstack/start-plugin-core': 1.171.10(@tanstack/react-router@1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1)) + '@tanstack/start-server-core': 1.169.8 + pathe: 2.0.3 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + vite: 8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0) + transitivePeerDependencies: + - '@rspack/core' + - crossws + - react-server-dom-rspack + - supports-color + - vite-plugin-solid + - webpack + '@tanstack/react-start@1.168.18(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4)': dependencies: '@tanstack/react-router': 1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -24385,6 +24756,13 @@ snapshots: - vite-plugin-solid - webpack + '@tanstack/react-store@0.11.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@tanstack/store': 0.11.0 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + use-sync-external-store: 1.6.0(react@19.2.6) + '@tanstack/react-store@0.9.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@tanstack/store': 0.9.3 @@ -24398,19 +24776,12 @@ snapshots: react: 19.2.6 react-dom: 19.2.6(react@19.2.6) - '@tanstack/react-virtual@3.13.12(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@tanstack/react-virtual@3.14.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@tanstack/virtual-core': 3.13.12 + '@tanstack/virtual-core': 3.17.1 react: 19.2.6 react-dom: 19.2.6(react@19.2.6) - '@tanstack/router-core@1.168.14': - dependencies: - '@tanstack/history': 1.161.6 - cookie-es: 3.1.1 - seroval: 1.5.4 - seroval-plugins: 1.5.0(seroval@1.5.4) - '@tanstack/router-core@1.171.8': dependencies: '@tanstack/history': 1.162.0 @@ -24418,6 +24789,14 @@ snapshots: seroval: 1.5.4 seroval-plugins: 1.5.4(seroval@1.5.4) + '@tanstack/router-devtools-core@1.168.0(@tanstack/router-core@1.171.8)(csstype@3.2.3)': + dependencies: + '@tanstack/router-core': 1.171.8 + clsx: 2.1.1 + goober: 2.1.19(csstype@3.2.3) + optionalDependencies: + csstype: 3.2.3 + '@tanstack/router-generator@1.167.12(supports-color@8.1.1)': dependencies: '@babel/types': 7.29.7 @@ -24431,6 +24810,28 @@ snapshots: transitivePeerDependencies: - supports-color + '@tanstack/router-plugin@1.168.13(@tanstack/react-router@1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1))': + dependencies: + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) + '@babel/types': 7.29.7 + '@tanstack/router-core': 1.171.8 + '@tanstack/router-generator': 1.167.12(supports-color@8.1.1) + '@tanstack/router-utils': 1.162.1(supports-color@8.1.1) + '@tanstack/virtual-file-routes': 1.162.0 + chokidar: 5.0.0 + unplugin: 3.0.0 + zod: 4.4.3 + optionalDependencies: + '@tanstack/react-router': 1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + vite: 8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0) + webpack: 5.105.4(esbuild@0.28.1) + transitivePeerDependencies: + - supports-color + '@tanstack/router-plugin@1.168.13(@tanstack/react-router@1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4)': dependencies: '@babel/core': 7.29.7(supports-color@8.1.1) @@ -24453,6 +24854,11 @@ snapshots: transitivePeerDependencies: - supports-color + '@tanstack/router-ssr-query-core@1.169.1(@tanstack/query-core@5.83.0)(@tanstack/router-core@1.171.8)': + dependencies: + '@tanstack/query-core': 5.83.0 + '@tanstack/router-core': 1.171.8 + '@tanstack/router-utils@1.162.1(supports-color@8.1.1)': dependencies: '@babel/core': 7.29.7(supports-color@8.1.1) @@ -24476,6 +24882,39 @@ snapshots: '@tanstack/start-fn-stubs@1.162.0': {} + '@tanstack/start-plugin-core@1.171.10(@tanstack/react-router@1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1))': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/types': 7.29.7 + '@rolldown/pluginutils': 1.0.1 + '@tanstack/router-core': 1.171.8 + '@tanstack/router-generator': 1.167.12(supports-color@8.1.1) + '@tanstack/router-plugin': 1.168.13(@tanstack/react-router@1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1)) + '@tanstack/router-utils': 1.162.1(supports-color@8.1.1) + '@tanstack/start-client-core': 1.170.6 + '@tanstack/start-server-core': 1.169.8 + exsolve: 1.0.8 + lightningcss: 1.32.0 + pathe: 2.0.3 + picomatch: 4.0.4 + seroval: 1.5.4 + source-map: 0.7.6 + srvx: 0.11.16 + tinyglobby: 0.2.17 + ufo: 1.6.4 + vitefu: 1.1.1(vite@8.0.16(@types/node@22.13.14)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0)) + xmlbuilder2: 4.0.3 + zod: 4.4.3 + optionalDependencies: + vite: 8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0) + transitivePeerDependencies: + - '@tanstack/react-router' + - crossws + - supports-color + - vite-plugin-solid + - webpack + '@tanstack/start-plugin-core@1.171.10(@tanstack/react-router@1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4)': dependencies: '@babel/code-frame': 7.27.1 @@ -24525,11 +24964,13 @@ snapshots: dependencies: '@tanstack/router-core': 1.171.8 + '@tanstack/store@0.11.0': {} + '@tanstack/store@0.9.3': {} '@tanstack/table-core@8.21.3': {} - '@tanstack/virtual-core@3.13.12': {} + '@tanstack/virtual-core@3.17.1': {} '@tanstack/virtual-file-routes@1.162.0': {} @@ -25157,6 +25598,12 @@ snapshots: lz-string: 1.5.0 uuid: 11.1.1 + '@vercel/config@0.2.1': + dependencies: + '@vercel/routing-utils': 6.1.1 + pretty-cache-header: 1.0.0 + zod: 3.25.76 + '@vercel/functions@1.6.0(@aws-sdk/credential-provider-web-identity@3.972.38)': optionalDependencies: '@aws-sdk/credential-provider-web-identity': 3.972.38 @@ -25194,6 +25641,13 @@ snapshots: '@vercel/oidc@3.2.0': {} + '@vercel/routing-utils@6.1.1': + dependencies: + path-to-regexp: 6.1.0 + path-to-regexp-updated: path-to-regexp@6.3.0 + optionalDependencies: + ajv: 6.14.0 + '@vitejs/plugin-react@6.0.1(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 @@ -26256,6 +26710,8 @@ snapshots: chalk@5.4.1: {} + chalk@5.6.2: {} + change-case-all@1.0.15: dependencies: change-case: 4.1.2 @@ -28591,6 +29047,10 @@ snapshots: globrex@0.1.2: {} + goober@2.1.19(csstype@3.2.3): + dependencies: + csstype: 3.2.3 + gopd@1.2.0: {} gqlmin@0.3.1: @@ -32431,6 +32891,8 @@ snapshots: path-to-regexp@0.1.13: {} + path-to-regexp@6.1.0: {} + path-to-regexp@6.3.0: {} path-to-regexp@8.4.1: {} @@ -32779,6 +33241,10 @@ snapshots: pretty-bytes@7.1.0: {} + pretty-cache-header@1.0.0: + dependencies: + timestring: 6.0.0 + pretty-format@27.5.1: dependencies: ansi-regex: 5.0.1 @@ -33986,10 +34452,6 @@ snapshots: serialize-javascript@7.0.5: {} - seroval-plugins@1.5.0(seroval@1.5.4): - dependencies: - seroval: 1.5.4 - seroval-plugins@1.5.4(seroval@1.5.4): dependencies: seroval: 1.5.4 @@ -34334,6 +34796,12 @@ snapshots: ip-address: 10.2.0 smart-buffer: 4.2.0 + solid-js@1.9.13: + dependencies: + csstype: 3.2.3 + seroval: 1.5.4 + seroval-plugins: 1.5.4(seroval@1.5.4) + sonner@1.5.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: react: 19.2.6 @@ -34825,6 +35293,8 @@ snapshots: timeout-signal@2.0.0: {} + timestring@6.0.0: {} + tiny-inflate@1.0.3: {} tiny-invariant@1.3.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3ba74bde8df..3ebbde767ea 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -13,6 +13,9 @@ catalog: '@supabase/realtime-js': 2.108.2 '@supabase/ssr': 0.10.2 '@supabase/supabase-js': 2.108.2 + '@tanstack/react-router': ^1.169.2 + '@tanstack/react-start': ^1.167.65 + '@tanstack/react-table': ^8.21.3 '@types/node': ^22.0.0 '@types/react': ^19.2.14 '@types/react-dom': ^19.2.3 @@ -73,6 +76,10 @@ overrides: cacache>tar: ^7.5.11 dompurify: ^3.3.2 express-rate-limit>ip-address: ^10.1.1 + # Pin h3 v1 to a single version so the Nuxt registry example (vue-blocks) + # doesn't end up with two copies (1.15.10 + 1.15.11) and hit nominal + # H3Event type mismatches. v2 (h3@2) is intentionally left untouched. + 'h3@1': 1.15.11 lodash: 'catalog:' lodash-es: 'catalog:' node-gyp>tar: ^7.5.11 diff --git a/scripts/generateLocalEnv.js b/scripts/generateLocalEnv.js index 808f964d89d..8f8f645f858 100644 --- a/scripts/generateLocalEnv.js +++ b/scripts/generateLocalEnv.js @@ -11,7 +11,10 @@ const defaultEnv = { // NEXT_ANALYTICS_BACKEND_PROVIDER: 'postgres', // SUPABASE_REST_URL: 'http://127.0.0.1:54321/rest/v1/', // NEXT_PUBLIC_ENABLE_LOGS: 'false', - // NEXT_PUBLIC_IS_PLATFORM: 'false', + // E2E tests target self-hosted studio (no auth). Explicitly written so that + // .env.test overrides any IS_PLATFORM=true that a developer's .env.local + // might set for their own platform-mode work. + NEXT_PUBLIC_IS_PLATFORM: 'false', PG_META_CRYPTO_KEY: 'SAMPLE_KEY', POSTGRES_PASSWORD: 'postgres', POSTGRES_HOST: 'db', @@ -31,7 +34,7 @@ const defaultEnv = { NEXT_PUBLIC_HCAPTCHA_SITE_KEY: '10000000-ffff-ffff-ffff-000000000001', NEXT_PUBLIC_NODE_ENV: 'test', SNIPPETS_MANAGEMENT_FOLDER: '../../supabase/snippets', - EDGE_FUNCTIONS_MANAGEMENT_FOLDER: '../../supabase/functions' // path relative to studio project + EDGE_FUNCTIONS_MANAGEMENT_FOLDER: '../../supabase/functions', // path relative to studio project } const environment = { ...generatedEnv, ...defaultEnv } diff --git a/turbo.jsonc b/turbo.jsonc index 26a5147b454..cbc370c99da 100644 --- a/turbo.jsonc +++ b/turbo.jsonc @@ -16,6 +16,7 @@ }, "dev": { "cache": false, + "persistent": true, }, "test": { "cache": false,