Upgrade backend to Next.js 15 (#360)

This commit is contained in:
Konsti Wohlwend 2024-12-10 00:11:13 -08:00 committed by GitHub
parent b8a7e911ea
commit 0413706c39
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 1019 additions and 961 deletions

113
.github/workflows/check-codegen.yaml vendored Normal file
View File

@ -0,0 +1,113 @@
name: Ensure up-to-date codegen
on:
push:
branches:
- dev
- main
pull_request:
branches:
- dev
- main
jobs:
check_codegen:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [22.x]
steps:
- uses: actions/checkout@v3
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 9.1.2
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Create .env.production.local file for apps/backend
run: cp apps/backend/.env.development apps/backend/.env.production.local
- name: Create .env.production.local file for apps/dashboard
run: cp apps/dashboard/.env.development apps/dashboard/.env.production.local
- name: Create .env.production.local file for apps/e2e
run: cp apps/e2e/.env.development apps/e2e/.env.production.local
- name: Create .env.production.local file for examples/cjs-test
run: cp examples/cjs-test/.env.development examples/cjs-test/.env.production.local
- name: Create .env.production.local file for examples/demo
run: cp examples/demo/.env.development examples/demo/.env.production.local
- name: Create .env.production.local file for examples/docs-examples
run: cp examples/docs-examples/.env.development examples/docs-examples/.env.production.local
- name: Create .env.production.local file for examples/e-commerce
run: cp examples/e-commerce/.env.development examples/e-commerce/.env.production.local
- name: Create .env.production.local file for examples/middleware
run: cp examples/middleware/.env.development examples/middleware/.env.production.local
- name: Create .env.production.local file for examples/partial-prerendering
run: cp examples/partial-prerendering/.env.development examples/partial-prerendering/.env.production.local
- name: Create .env.production.local file for examples/supabase
run: cp examples/supabase/.env.development examples/supabase/.env.production.local
- name: Build
run: pnpm build
- name: Store Quetzal API key in packages/stack/.env.local
run: echo "QUETZAL_API_KEY=${{ secrets.QUETZAL_API_KEY }}" > packages/stack/.env.local
- name: Run code gen
run: pnpm codegen
- name: Check for uncommitted changes
run: |
if [[ -n $(git status --porcelain) ]]; then
echo "Error: There are uncommitted changes after build/lint/typecheck."
echo "Please commit all changes before pushing."
git status
exit 1
fi
check_prisma_migrations:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [22.x]
steps:
- uses: actions/checkout@v3
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 9.1.2
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Start Postgres shadow DB
run: docker run -d --name postgres-prisma-diff-shadow -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=PLACEHOLDER-PASSWORD--dfaBC1hm1v -e POSTGRES_DB=postgres -p 5432:5432 postgres:latest
- name: Check for differences in Prisma schema and migrations
run: pnpm run prisma migrate diff --from-migrations ./prisma/migrations --to-schema-datamodel ./prisma/schema.prisma --shadow-database-url postgres://postgres:PLACEHOLDER-PASSWORD--dfaBC1hm1v@localhost:5432/postgres --exit-code

View File

@ -69,8 +69,6 @@ jobs:
- name: Build
run: pnpm build
env:
QUETZAL_API_KEY: ${{ secrets.QUETZAL_API_KEY }}
- name: Start Docker Compose
run: docker compose -f dependencies.compose.yaml up -d

View File

@ -66,11 +66,6 @@ jobs:
- name: Build
run: pnpm build
env:
QUETZAL_API_KEY: ${{ secrets.QUETZAL_API_KEY }}
- name: Run code gen
run: pnpm codegen
- name: Lint
run: pnpm lint

View File

@ -56,8 +56,6 @@ jobs:
- name: Build
run: pnpm build
env:
QUETZAL_API_KEY: ${{ secrets.QUETZAL_API_KEY }}
- name: Check API is valid
run: pnpm run fern check

View File

@ -74,8 +74,6 @@ jobs:
- name: Build
run: pnpm build
env:
QUETZAL_API_KEY: ${{ secrets.QUETZAL_API_KEY }}
- name: Check API is valid
run: pnpm run fern check

View File

@ -30,7 +30,7 @@
"seed": "pnpm run with-env tsx prisma/seed.ts"
},
"dependencies": {
"@next/bundle-analyzer": "^14.0.3",
"@next/bundle-analyzer": "15.0.3",
"@node-oauth/oauth2-server": "^5.1.0",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/api-logs": "^0.53.0",
@ -55,15 +55,15 @@
"bcrypt": "^5.1.1",
"dotenv-cli": "^7.3.0",
"jose": "^5.2.2",
"next": "^14.2.5",
"next": "15.0.3",
"nodemailer": "^6.9.10",
"oidc-provider": "^8.5.1",
"openid-client": "^5.6.4",
"oslo": "^1.2.1",
"pg": "^8.11.3",
"posthog-node": "^4.1.0",
"react": "^18.2",
"react-dom": "^18.2",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"semver": "^7.6.3",
"server-only": "^0.0.1",
"sharp": "^0.32.6",
@ -76,7 +76,8 @@
"@types/node": "^20.8.10",
"@types/nodemailer": "^6.4.14",
"@types/oidc-provider": "^8.5.1",
"@types/react": "^18.3.12",
"@types/react": "link:@types/react@18.3.12",
"@types/react-dom": "^19.0.0",
"@types/semver": "^7.5.8",
"concurrently": "^8.2.2",
"glob": "^10.4.1",

View File

@ -61,7 +61,7 @@ export const GET = createSmartRouteHandler({
throw new KnownErrors.InvalidOAuthClientIdOrSecret(query.client_id);
}
if (!await checkApiKeySet(query.client_id, { publishableClientKey: query.client_secret })) {
if (!(await checkApiKeySet(query.client_id, { publishableClientKey: query.client_secret }))) {
throw new KnownErrors.InvalidPublishableClientKey(query.client_id);
}
@ -124,7 +124,7 @@ export const GET = createSmartRouteHandler({
// prevent CSRF by keeping track of the inner state in cookies
// the callback route must ensure that the inner state cookie is set
cookies().set(
(await cookies()).set(
"stack-oauth-inner-" + innerState,
"true",
{

View File

@ -47,8 +47,8 @@ const handler = createSmartRouteHandler({
}),
async handler({ params, query, body }, fullReq) {
const innerState = query.state ?? (body as any)?.state ?? "";
const cookieInfo = cookies().get("stack-oauth-inner-" + innerState);
cookies().delete("stack-oauth-inner-" + innerState);
const cookieInfo = (await cookies()).get("stack-oauth-inner-" + innerState);
(await cookies()).delete("stack-oauth-inner-" + innerState);
if (cookieInfo?.value !== 'true') {
throw new StatusError(StatusError.BadRequest, "OAuth cookie not found. This is likely because you refreshed the page during the OAuth sign in process. Please try signing in again");

View File

@ -1,5 +1,5 @@
import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors";
import { assertIpAddress, isIpAddress } from "@stackframe/stack-shared/dist/utils/ips";
import { isIpAddress } from "@stackframe/stack-shared/dist/utils/ips";
import { pick } from "@stackframe/stack-shared/dist/utils/objects";
import { headers } from "next/headers";
@ -66,7 +66,7 @@ export async function getEndUserInfo(): Promise<
| { maybeSpoofed: false, exactInfo: EndUserInfoInner }
| null
> {
const allHeaders = headers();
const allHeaders = await headers();
// note that this is just the requester claiming to be a browser; we can't trust them as they could just fake the
// headers
@ -84,7 +84,6 @@ export async function getEndUserInfo(): Promise<
?? allHeaders.get("x-vercel-forwarded-for")
?? allHeaders.get("x-real-ip")
?? allHeaders.get("x-forwarded-for")?.split(",").at(0)
?? allHeaders.get("x-stack-direct-requester-or-proxy-ip")
?? undefined;
if (!ip || !isIpAddress(ip)) {
console.warn("getEndUserIp() found IP address in headers, but is invalid. This is most likely a misconfigured client", { ip, headers: Object.fromEntries(allHeaders) });

View File

@ -54,8 +54,7 @@ export async function middleware(request: NextRequest) {
const isApiRequest = url.pathname.startsWith('/api/');
const newRequestHeaders = new Headers(request.headers);
// store the direct IP address of the requester or proxy so we can read it with `headers()` later
newRequestHeaders.set("x-stack-direct-requester-or-proxy-ip", request.ip ?? '');
// here we could update the request headers (currently we don't)
const responseInit = isApiRequest ? {
request: {

View File

@ -257,7 +257,7 @@ async function parseAuth(req: NextRequest): Promise<SmartRequestAuth | null> {
};
}
export async function createSmartRequest(req: NextRequest, bodyBuffer: ArrayBuffer, options?: { params: Record<string, string> }): Promise<SmartRequest> {
export async function createSmartRequest(req: NextRequest, bodyBuffer: ArrayBuffer, options?: { params: Promise<Record<string, string>> }): Promise<SmartRequest> {
const urlObject = new URL(req.url);
const clientVersionMatch = req.headers.get("x-stack-client-version")?.match(/^(\w+)\s+(@[\w\/]+)@([\d.]+)$/);
@ -270,7 +270,7 @@ export async function createSmartRequest(req: NextRequest, bodyBuffer: ArrayBuff
.map(([key, values]) => [key, values.map(([_, value]) => value)]),
),
query: Object.fromEntries(urlObject.searchParams.entries()),
params: options?.params ?? {},
params: await options?.params ?? {},
auth: await parseAuth(req),
clientVersion: clientVersionMatch ? {
platform: clientVersionMatch[1],

View File

@ -98,7 +98,9 @@ export function handleApiRequest(handler: (req: NextRequest, options: any, reque
console.log(`[ ERR] [${requestId}] ${req.method} ${req.url}: ${statusError.message}`);
if (!commonErrors.some(e => statusError instanceof e)) {
console.debug(`For the error above with request ID ${requestId}, the full error is:`, statusError);
// HACK: Log statusError.stack instead of statusError to get around buggy Next.js pretty-printing
// https://www.reddit.com/r/nextjs/comments/1gkxdqe/comment/m19kxgn/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button
console.debug(`For the error above with request ID ${requestId}, the full error is:`, statusError.stack);
}
const res = await createResponse(req, requestId, {

View File

@ -64,7 +64,7 @@
"@types/canvas-confetti": "^1.6.4",
"@types/node": "^20.8.10",
"@types/nodemailer": "^6.4.14",
"@types/react": "^18.3.12",
"@types/react": "link:@types/react@18.3.12",
"@types/react-dom": "^18",
"autoprefixer": "^10.4.17",
"glob": "^10.4.1",

View File

@ -2,6 +2,7 @@ declare module 'remark-heading-id';
declare namespace React {
// inert doesn't exist in React.HTMLAttributes, so we need to extend it
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface HTMLAttributes<T> {
inert?: '',
}

View File

@ -3,6 +3,7 @@ import { encodeBase64 } from "@stackframe/stack-shared/dist/utils/bytes";
import { generateSecureRandomString } from "@stackframe/stack-shared/dist/utils/crypto";
import { StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors";
import { filterUndefined } from "@stackframe/stack-shared/dist/utils/objects";
import { nicify } from "@stackframe/stack-shared/dist/utils/strings";
import * as jose from "jose";
import { expect } from "vitest";
import { Context, Mailbox, NiceRequestInit, NiceResponse, STACK_BACKEND_BASE_URL, STACK_INTERNAL_PROJECT_ADMIN_KEY, STACK_INTERNAL_PROJECT_CLIENT_KEY, STACK_INTERNAL_PROJECT_ID, STACK_INTERNAL_PROJECT_SERVER_KEY, createMailbox, localRedirectUrl, niceFetch, updateCookiesFromResponse } from "../helpers";
@ -100,7 +101,7 @@ export async function niceBackendFetch(url: string | URL, options?: Omit<NiceReq
}),
});
if (res.status >= 500 && res.status < 600) {
throw new StackAssertionError(`Unexpected internal server error: ${res.status} ${res.body}`);
throw new StackAssertionError(`Unexpected internal server error: ${res.status} ${typeof res.body === "string" ? res.body : nicify(res.body)}`);
}
if (res.headers.has("x-stack-known-error")) {
expect(res.status).toBeGreaterThanOrEqual(400);

View File

@ -1,10 +1,13 @@
import 'vitest';
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface CustomMatchers<R = unknown> {
toSatisfy: (predicate: (value: string) => boolean) => R,
}
declare module 'vitest' {
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface Assertion<T = any> extends CustomMatchers<T> {}
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface AsymmetricMatchersContaining extends CustomMatchers {}
}

View File

@ -65,7 +65,7 @@ services:
image: svix/svix-server
environment:
WAIT_FOR: 'true'
SVIX_REDIS_DSN: redis://placeholder-username:PASSWORD-PLACEHOLDER--oVn8GSD6b9@svix-redis:6379
SVIX_REDIS_DSN: redis://:PASSWORD-PLACEHOLDER--oVn8GSD6b9@svix-redis:6379
SVIX_DB_DSN: postgres://postgres:PASSWORD-PLACEHOLDER--KsoIMcchtp@svix-db:5432/svix
SVIX_CACHE_TYPE: memory
SVIX_JWT_SECRET: secret

View File

@ -10,14 +10,14 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@stackframe/stack": "workspace:*",
"next": "^14.1",
"react": "^18",
"react-dom": "^18",
"@stackframe/stack": "workspace:*"
"react-dom": "^18"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18.3.12",
"@types/react": "link:@types/react@18.3.12",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "14.2.3",

View File

@ -24,17 +24,11 @@
"react-icons": "^5.3.0"
},
"devDependencies": {
"@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react": "link:@types/react@18.3.12",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.47",
"rimraf": "^5.0.10",
"tailwindcss": "^3.4.14"
},
"pnpm": {
"overrides": {
"@types/react": "npm:types-react@19.0.0-rc.1\n",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1\n"
}
}
}

View File

@ -24,11 +24,11 @@
"react-icons": "^5.0.1"
},
"devDependencies": {
"rimraf": "^5.0.5",
"@types/react": "^18.3.12",
"@types/react": "link:@types/react@18.3.12",
"@types/react-dom": "^18",
"autoprefixer": "^10.4.17",
"postcss": "^8.4.35",
"rimraf": "^5.0.5",
"tailwindcss": "^3.4.1"
}
}

View File

@ -17,12 +17,12 @@
"react-dom": "^18"
},
"devDependencies": {
"rimraf": "^5.0.5",
"@types/node": "^20",
"@types/react": "^18.3.12",
"@types/react": "link:@types/react@18.3.12",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "14.2.5",
"rimraf": "^5.0.5",
"typescript": "^5"
}
}

View File

@ -10,14 +10,14 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@stackframe/stack": "workspace:*",
"next": "^14.2",
"react": "^18",
"react-dom": "^18",
"@stackframe/stack": "workspace:*"
"react-dom": "^18"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18.3.12",
"@types/react": "link:@types/react@18.3.12",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "14.2.3",

View File

@ -10,14 +10,14 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@stackframe/stack": "workspace:*",
"next": "14.3.0-canary.26",
"react": "^18",
"react-dom": "^18",
"@stackframe/stack": "workspace:*"
"react-dom": "^18"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18.3.12",
"@types/react": "link:@types/react@18.3.12",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "14.2.3",

View File

@ -13,14 +13,14 @@
"@supabase/ssr": "latest",
"@supabase/supabase-js": "latest",
"jose": "^5.2.2",
"next": "^15.0.3",
"react": "18.2.0",
"react-dom": "18.2.0"
"next": "^14.2.5",
"react": "^18.3.0",
"react-dom": "^18.3.0"
},
"devDependencies": {
"@types/node": "20.10.6",
"@types/react": "18.2.46",
"@types/react-dom": "18.2.18",
"@types/react": "link:@types/react@18.3.12",
"@types/react-dom": "^18.3.12",
"typescript": "5.3.3"
}
}

View File

@ -63,7 +63,10 @@
"wait-on": "^8.0.1"
},
"pnpm": {
"overrides": {}
"overrides": {
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0"
}
},
"engines": {
"npm": ">=10.0.0",

View File

@ -9,6 +9,7 @@ import { isValidUrl } from "./utils/urls";
import { isUuid } from "./utils/uuids";
declare module "yup" {
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface StringSchema<TType, TContext, TDefault, TFlags> {
nonEmpty(message?: string): StringSchema<TType, TContext, TDefault, TFlags>,
}

View File

@ -89,6 +89,8 @@
"rimraf": "^5.0.10",
"react": "^18.2",
"react-dom": "^18.2",
"@types/react": "^18.2.12",
"@types/react-dom": "^18.2.12",
"next": "^14.1.0"
}
}

View File

@ -3,10 +3,10 @@ import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMe
import { Column } from "@tanstack/react-table";
import { LucideIcon, EyeOff, ArrowUp, ArrowDown } from "lucide-react";
interface DataTableColumnHeaderProps<TData, TValue> extends React.HTMLAttributes<HTMLDivElement> {
type DataTableColumnHeaderProps<TData, TValue> = {
column: Column<TData, TValue>,
columnTitle: React.ReactNode,
}
} & React.HTMLAttributes<HTMLDivElement>
function Item(props: { icon: LucideIcon, onClick: () => void, children: React.ReactNode }) {
return (

View File

@ -15,7 +15,7 @@ import { Column } from "@tanstack/react-table";
import { ListFilter } from "lucide-react";
import React from "react";
interface DataTableFacetedFilterProps<TData, TValue> {
type DataTableFacetedFilterProps<TData, TValue> = {
column?: Column<TData, TValue>,
title?: string,
options: {

View File

@ -8,7 +8,7 @@ import {
} from "@stackframe/stack-ui";
import { Table } from "@tanstack/react-table";
interface DataTablePaginationProps<TData> {
type DataTablePaginationProps<TData> = {
table: Table<TData>,
}

View File

@ -8,7 +8,7 @@ import { download, generateCsv, mkConfig } from 'export-to-csv';
import { DownloadIcon } from "lucide-react";
import { DataTableViewOptions } from "./view-options";
interface DataTableToolbarProps<TData> {
type DataTableToolbarProps<TData> = {
table: Table<TData>,
toolbarRender?: (table: Table<TData>) => React.ReactNode,
showDefaultToolbar?: boolean,

View File

@ -12,7 +12,7 @@ import {
} from "@stackframe/stack-ui";
import { Table } from "@tanstack/react-table";
interface DataTableViewOptionsProps<TData> {
type DataTableViewOptionsProps<TData> = {
table: Table<TData>,
}

View File

@ -24,7 +24,7 @@ const Command: React.FC<React.ComponentPropsWithoutRef<typeof CommandPrimitive>>
));
Command.displayName = CommandPrimitive.displayName;
interface CommandDialogProps extends DialogProps {}
type CommandDialogProps = {} & DialogProps
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (

View File

@ -1,9 +1,9 @@
"use client";
import React from "react";
import { forwardRefIfNeeded } from "@stackframe/stack-shared/dist/utils/react";
import { CheckIcon } from "@radix-ui/react-icons";
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import { forwardRefIfNeeded } from "@stackframe/stack-shared/dist/utils/react";
import React from "react";
import { cn } from "../../lib/utils";

View File

@ -50,9 +50,7 @@ const sheetVariants = cva(
}
);
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> { hasCloseButton?: boolean }
type SheetContentProps = { hasCloseButton?: boolean } & React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content> & VariantProps<typeof sheetVariants>
const SheetContent = forwardRefIfNeeded<
React.ElementRef<typeof SheetPrimitive.Content>,

View File

@ -9,7 +9,7 @@ import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/
import { useAsyncCallback } from "@stackframe/stack-shared/dist/hooks/use-async-callback";
import { Spinner } from "./spinner";
interface OriginalSwitchProps extends React.ComponentProps<typeof SwitchPrimitives.Root> {}
type OriginalSwitchProps = {} & React.ComponentProps<typeof SwitchPrimitives.Root>
const OriginalSwitch = forwardRefIfNeeded<
React.ElementRef<typeof SwitchPrimitives.Root>,
@ -31,11 +31,11 @@ const OriginalSwitch = forwardRefIfNeeded<
</SwitchPrimitives.Root>
));
OriginalSwitch.displayName = SwitchPrimitives.Root.displayName;
interface AsyncSwitchProps extends OriginalSwitchProps {
type AsyncSwitchProps = {
onCheckedChange?: (checked: boolean) => Promise<void> | void,
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => Promise<void> | void,
loading?: boolean,
}
} & OriginalSwitchProps
const Switch = forwardRefIfNeeded<
React.ElementRef<typeof SwitchPrimitives.Root>,

View File

@ -27,9 +27,7 @@ const typographyVariants = cva("text-md", {
},
});
interface TypographyProps
extends React.HTMLAttributes<HTMLHeadingElement>,
VariantProps<typeof typographyVariants> {}
type TypographyProps = {} & React.HTMLAttributes<HTMLHeadingElement> & VariantProps<typeof typographyVariants>
const Typography = forwardRefIfNeeded<HTMLHeadingElement, TypographyProps>(
({ className, type, variant, ...props }, ref) => {

View File

@ -52,7 +52,7 @@ type Action =
toastId?: ToasterToast["id"],
}
interface State {
type State = {
toasts: ToasterToast[],
}

File diff suppressed because it is too large Load Diff