nextjs-hackathon-stack 0.1.23 → 0.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/package.json +1 -1
  2. package/template/.cursor/agents/security-researcher.md +1 -1
  3. package/template/.cursor/rules/general.mdc +1 -1
  4. package/template/.cursor/rules/nextjs.mdc +3 -3
  5. package/template/.cursor/rules/supabase.mdc +5 -0
  6. package/template/.cursor/skills/security-audit/SKILL.md +1 -1
  7. package/template/AGENTS.md +21 -0
  8. package/template/CLAUDE.md +15 -0
  9. package/template/README.md +2 -2
  10. package/template/eslint.config.ts +5 -1
  11. package/template/next.config.ts +3 -0
  12. package/template/package.json.tmpl +4 -3
  13. package/template/{middleware.ts → proxy.ts} +3 -2
  14. package/template/src/app/(protected)/page.tsx +8 -5
  15. package/template/src/app/__tests__/protected-layout.test.tsx +1 -1
  16. package/template/src/app/__tests__/protected-page.test.tsx +21 -1
  17. package/template/src/features/auth/__tests__/login-form.test.tsx +1 -1
  18. package/template/src/features/auth/__tests__/login.action.test.ts +1 -1
  19. package/template/src/features/auth/__tests__/logout.action.test.ts +1 -1
  20. package/template/src/features/auth/actions/login.action.ts +1 -1
  21. package/template/src/features/auth/components/login-form.tsx +1 -1
  22. package/template/src/features/todos/__tests__/add-todo-form.test.tsx +1 -1
  23. package/template/src/features/todos/__tests__/todo-list.test.tsx +2 -2
  24. package/template/src/features/todos/__tests__/todos.action.test.ts +1 -1
  25. package/template/src/features/todos/actions/todos.action.ts +2 -1
  26. package/template/src/features/todos/components/add-todo-form.tsx +3 -1
  27. package/template/src/features/todos/components/todo-list.tsx +6 -4
  28. package/template/src/shared/__tests__/{middleware.test.ts → proxy.test.ts} +8 -8
  29. package/template/src/shared/__tests__/supabase-middleware.test.ts +19 -14
  30. package/template/src/shared/__tests__/supabase-server.test.ts +10 -6
  31. package/template/src/shared/__tests__/utils.test.ts +5 -2
  32. package/template/src/shared/components/ui/button.tsx +2 -1
  33. package/template/src/shared/components/ui/spinner.tsx +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nextjs-hackathon-stack",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "Scaffold a full-stack Next.js hackathon starter",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,7 +12,7 @@ readonly: true
12
12
  ### Authentication & Authorization
13
13
  - [ ] Supabase RLS enabled on all tables
14
14
  - [ ] Auth check in every protected API route
15
- - [ ] Session validation in middleware
15
+ - [ ] Session validation in proxy
16
16
  - [ ] No hardcoded credentials or API keys
17
17
 
18
18
  ### Input Validation
@@ -6,7 +6,7 @@ alwaysApply: true
6
6
  # Stack Overview
7
7
 
8
8
  ## Technology Stack
9
- - **Framework**: Next.js 15 (App Router)
9
+ - **Framework**: Next.js 16 (App Router)
10
10
  - **Database**: Supabase (PostgreSQL) with Drizzle ORM for schema/migrations
11
11
  - **Auth**: Supabase Auth via `@supabase/ssr`
12
12
  - **ORM**: Drizzle + `drizzle-zod` (schema + migrations ONLY — no runtime queries)
@@ -1,9 +1,9 @@
1
1
  ---
2
- description: Next.js 15 App Router patterns.
2
+ description: Next.js 16 App Router patterns.
3
3
  globs: ["src/app/**"]
4
4
  ---
5
5
 
6
- # Next.js 15 Rules
6
+ # Next.js 16 Rules
7
7
 
8
8
  ## App Router Conventions
9
9
  - `page.tsx` — public route page component
@@ -11,7 +11,7 @@ globs: ["src/app/**"]
11
11
  - `loading.tsx` — Suspense boundary UI
12
12
  - `error.tsx` — Error boundary UI
13
13
  - `route.ts` — API route handler
14
- - `middleware.ts` — Edge middleware (project root)
14
+ - `proxy.ts` — Request proxy (project root, Node.js runtime)
15
15
 
16
16
  ## Route Groups
17
17
  - `(auth)` — unauthenticated routes
@@ -34,3 +34,8 @@ const users = await db.select().from(usersTable); // WRONG — bypasses RLS
34
34
  - Enable RLS on ALL tables in Supabase dashboard
35
35
  - Every table must have explicit policies
36
36
  - Test RLS policies in migrations
37
+
38
+ ## Environment Variables
39
+ - `NEXT_PUBLIC_SUPABASE_URL` + `NEXT_PUBLIC_SUPABASE_ANON_KEY` — client-safe, used in browser + server
40
+ - `DATABASE_URL` — server-only, used by Drizzle for migrations (`drizzle.config.ts`)
41
+ - See `.env.example` for all required variables
@@ -29,7 +29,7 @@ For each table in `src/shared/db/schema.ts`:
29
29
  - Confirm explicit policies exist
30
30
 
31
31
  ### 4. Auth Coverage
32
- - Verify `middleware.ts` protects all non-public routes
32
+ - Verify `proxy.ts` protects all non-public routes
33
33
  - Check every `route.ts` in protected features has auth check
34
34
  - Verify `app/(protected)/layout.tsx` has server-side auth check
35
35
 
@@ -0,0 +1,21 @@
1
+ <!-- BEGIN:nextjs-agent-rules -->
2
+
3
+ # Next.js: ALWAYS read docs before coding
4
+
5
+ Before any Next.js work, find and read the relevant doc in `node_modules/next/dist/docs/`. Your training data is outdated — the docs are the source of truth.
6
+
7
+ <!-- END:nextjs-agent-rules -->
8
+
9
+ # Environment variables
10
+
11
+ Read `.env.example` for the full list. Key vars:
12
+
13
+ | Variable | Visibility | Where to get it |
14
+ |---|---|---|
15
+ | `NEXT_PUBLIC_SUPABASE_URL` | Client + Server | Supabase > Project Settings > API |
16
+ | `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Client + Server | Supabase > Project Settings > API |
17
+ | `DATABASE_URL` | Server only | Supabase > Project Settings > Database > Connection string (Session mode) |
18
+
19
+ - `NEXT_PUBLIC_*` vars are safe to expose to the browser — they are public by design
20
+ - Never add `NEXT_PUBLIC_` prefix to secret keys (`DATABASE_URL`, `AI_API_KEY`)
21
+ - `.env.local` is gitignored — never commit real credentials
@@ -0,0 +1,15 @@
1
+ @AGENTS.md
2
+
3
+ # Architecture
4
+
5
+ Feature-based structure: `src/features/* → src/shared/*` (never reverse).
6
+
7
+ - **Drizzle** = schema + migrations ONLY. Never use Drizzle for runtime queries.
8
+ - **supabase-js** = all runtime queries. RLS is always active.
9
+ - **Zod schemas** for DB types: auto-generate via `drizzle-zod`, never write manually.
10
+
11
+ # Testing
12
+
13
+ - 100% coverage required — `pnpm test:coverage` must pass
14
+ - AAA pattern (Arrange / Act / Assert) in every test
15
+ - Mock only external boundaries (Supabase, HTTP, DB) — never mock internal code
@@ -1,12 +1,12 @@
1
1
  # {{projectName}}
2
2
 
3
- Full-stack Next.js 15 hackathon starter.
3
+ Full-stack Next.js 16 hackathon starter.
4
4
 
5
5
  ## Stack
6
6
 
7
7
  | Layer | Tool |
8
8
  |---|---|
9
- | Framework | Next.js 15 + App Router |
9
+ | Framework | Next.js 16 + App Router |
10
10
  | Database | Supabase (PostgreSQL) |
11
11
  | Auth | Supabase Auth |
12
12
  | ORM | Drizzle (schema + migrations) |
@@ -61,6 +61,10 @@ export default tseslint.config(
61
61
  },
62
62
  },
63
63
  {
64
- ignores: [".next/**", "node_modules/**", "dist/**", "eslint.config.ts", "next-env.d.ts", "next.config.ts", "playwright.config.ts", "drizzle.config.ts", "vitest.config.ts"],
64
+ files: ["**/error.tsx"],
65
+ rules: { "no-console": "off" },
66
+ },
67
+ {
68
+ ignores: [".next/**", "node_modules/**", "dist/**", "eslint.config.ts", "next-env.d.ts", "next.config.ts", "playwright.config.ts", "drizzle.config.ts", "vitest.config.ts", "postcss.config.mjs"],
65
69
  }
66
70
  );
@@ -2,6 +2,9 @@ import type { NextConfig } from "next";
2
2
 
3
3
  const nextConfig: NextConfig = {
4
4
  typedRoutes: true,
5
+ logging: {
6
+ browserToTerminal: true,
7
+ },
5
8
  turbopack: {
6
9
  root: import.meta.dirname,
7
10
  },
@@ -3,7 +3,7 @@
3
3
  "version": "0.1.0",
4
4
  "private": true,
5
5
  "scripts": {
6
- "dev": "next dev --turbopack",
6
+ "dev": "next dev",
7
7
  "build": "next build",
8
8
  "start": "next start",
9
9
  "lint": "eslint . --max-warnings 0",
@@ -21,7 +21,7 @@
21
21
  "prepare": "husky"
22
22
  },
23
23
  "dependencies": {
24
- "next": "^15",
24
+ "next": "^16.2",
25
25
  "@supabase/supabase-js": "^2",
26
26
  "@supabase/ssr": "^0.6",
27
27
  "drizzle-orm": "^0.44",
@@ -45,6 +45,7 @@
45
45
  "@types/node": "^22",
46
46
  "@types/react": "^19",
47
47
  "@types/react-dom": "^19",
48
+ "@next/env": "^16",
48
49
  "drizzle-kit": "^0.30",
49
50
  "vitest": "^3",
50
51
  "@vitejs/plugin-react": "^4",
@@ -60,7 +61,7 @@
60
61
  "eslint": "^9",
61
62
  "@eslint/js": "^9",
62
63
  "typescript-eslint": "^8",
63
- "@next/eslint-plugin-next": "^15",
64
+ "@next/eslint-plugin-next": "^16",
64
65
  "eslint-plugin-react": "^7",
65
66
  "eslint-plugin-react-hooks": "^5",
66
67
  "eslint-plugin-import-x": "^4",
@@ -1,7 +1,8 @@
1
- import { type NextRequest } from "next/server";
1
+ import type { NextRequest } from "next/server";
2
+
2
3
  import { updateSession } from "@/shared/lib/supabase/middleware";
3
4
 
4
- export async function middleware(request: NextRequest) {
5
+ export async function proxy(request: NextRequest) {
5
6
  return updateSession(request);
6
7
  }
7
8
 
@@ -1,17 +1,20 @@
1
- import { TodoList } from "@/features/todos/components/todo-list";
2
- import { AddTodoForm } from "@/features/todos/components/add-todo-form";
1
+ import { eq } from "drizzle-orm";
2
+ import { redirect } from "next/navigation";
3
+
3
4
  import { logoutAction } from "@/features/auth/actions/logout.action";
4
- import { createClient } from "@/shared/lib/supabase/server";
5
+ import { AddTodoForm } from "@/features/todos/components/add-todo-form";
6
+ import { TodoList } from "@/features/todos/components/todo-list";
5
7
  import { db } from "@/shared/db";
6
8
  import { todos } from "@/shared/db/schema";
7
- import { eq } from "drizzle-orm";
9
+ import { createClient } from "@/shared/lib/supabase/server";
8
10
 
9
11
  export default async function HomePage() {
10
12
  const supabase = await createClient();
11
13
  const {
12
14
  data: { user },
13
15
  } = await supabase.auth.getUser();
14
- const items = await db.select().from(todos).where(eq(todos.userId, user!.id));
16
+ if (!user) redirect("/login");
17
+ const items = await db.select().from(todos).where(eq(todos.userId, user.id));
15
18
 
16
19
  return (
17
20
  <main className="container mx-auto max-w-2xl p-8">
@@ -12,7 +12,7 @@ vi.mock("@/shared/lib/supabase/server", () => ({
12
12
  }));
13
13
 
14
14
  vi.mock("next/navigation", () => ({
15
- redirect: (url: string) => mockRedirect(url),
15
+ redirect: (url: string): unknown => mockRedirect(url),
16
16
  }));
17
17
 
18
18
  describe("ProtectedLayout", () => {
@@ -1,7 +1,8 @@
1
- import { describe, it, expect, vi } from "vitest";
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
2
 
3
3
  const mockGetUser = vi.fn();
4
4
  const mockSelect = vi.fn();
5
+ const mockRedirect = vi.fn();
5
6
 
6
7
  vi.mock("@/shared/lib/supabase/server", () => ({
7
8
  createClient: vi.fn(() =>
@@ -29,7 +30,26 @@ vi.mock("@/features/todos/components/add-todo-form", () => ({
29
30
  AddTodoForm: () => <div data-testid="add-todo-form" />,
30
31
  }));
31
32
 
33
+ vi.mock("next/navigation", () => ({
34
+ redirect: (url: string): unknown => mockRedirect(url),
35
+ }));
36
+
32
37
  describe("HomePage (protected)", () => {
38
+ beforeEach(() => {
39
+ vi.clearAllMocks();
40
+ mockRedirect.mockImplementation(() => { throw new Error("NEXT_REDIRECT"); });
41
+ });
42
+
43
+ it("redirects to /login when user is not authenticated", async () => {
44
+ // Arrange
45
+ mockGetUser.mockResolvedValue({ data: { user: null } });
46
+ const { default: HomePage } = await import("../(protected)/page");
47
+
48
+ // Act + Assert
49
+ await expect(HomePage()).rejects.toThrow("NEXT_REDIRECT");
50
+ expect(mockRedirect).toHaveBeenCalledWith("/login");
51
+ });
52
+
33
53
  it("renderiza la página con las tareas del usuario", async () => {
34
54
  // Arrange
35
55
  mockGetUser.mockResolvedValue({ data: { user: { id: "u1", email: "user@example.com" } } });
@@ -7,7 +7,7 @@ import { LoginForm } from "../components/login-form";
7
7
  const mockLoginAction = vi.fn().mockResolvedValue({ status: "idle" });
8
8
 
9
9
  vi.mock("../actions/login.action", () => ({
10
- loginAction: (...args: unknown[]) => mockLoginAction(...args),
10
+ loginAction: (...args: unknown[]): unknown => mockLoginAction(...args),
11
11
  }));
12
12
 
13
13
  describe("LoginForm", () => {
@@ -12,7 +12,7 @@ vi.mock("@/shared/lib/supabase/server", () => ({
12
12
  }));
13
13
 
14
14
  vi.mock("next/navigation", () => ({
15
- redirect: (url: string) => mockRedirect(url),
15
+ redirect: (url: string): unknown => mockRedirect(url),
16
16
  }));
17
17
 
18
18
  describe("loginAction", () => {
@@ -12,7 +12,7 @@ vi.mock("@/shared/lib/supabase/server", () => ({
12
12
  }));
13
13
 
14
14
  vi.mock("next/navigation", () => ({
15
- redirect: (url: string) => mockRedirect(url),
15
+ redirect: (url: string): unknown => mockRedirect(url),
16
16
  }));
17
17
 
18
18
  describe("logoutAction", () => {
@@ -25,7 +25,7 @@ export async function loginAction(
25
25
  });
26
26
 
27
27
  if (!parsed.success) {
28
- return { status: "error", message: parsed.error.issues[0]!.message };
28
+ return { status: "error", message: (parsed.error.issues[0] as { message: string }).message };
29
29
  }
30
30
 
31
31
  const supabase = await createClient();
@@ -35,7 +35,7 @@ export function LoginForm() {
35
35
  const fd = new FormData();
36
36
  fd.set("email", data.email);
37
37
  fd.set("password", data.password);
38
- startTransition(() => formAction(fd));
38
+ startTransition(() => { formAction(fd); });
39
39
  });
40
40
 
41
41
  return (
@@ -7,7 +7,7 @@ import { AddTodoForm } from "../components/add-todo-form";
7
7
  const mockAdd = vi.fn().mockResolvedValue(undefined);
8
8
 
9
9
  vi.mock("../actions/todos.action", () => ({
10
- addTodoAction: (...args: unknown[]) => mockAdd(...args),
10
+ addTodoAction: (...args: unknown[]): unknown => mockAdd(...args),
11
11
  }));
12
12
 
13
13
  describe("AddTodoForm", () => {
@@ -8,8 +8,8 @@ const mockToggle = vi.fn().mockResolvedValue(undefined);
8
8
  const mockDelete = vi.fn().mockResolvedValue(undefined);
9
9
 
10
10
  vi.mock("../actions/todos.action", () => ({
11
- toggleTodoAction: (...args: unknown[]) => mockToggle(...args),
12
- deleteTodoAction: (...args: unknown[]) => mockDelete(...args),
11
+ toggleTodoAction: (...args: unknown[]): unknown => mockToggle(...args),
12
+ deleteTodoAction: (...args: unknown[]): unknown => mockDelete(...args),
13
13
  }));
14
14
 
15
15
  const baseTodo = {
@@ -8,7 +8,7 @@ const mockRevalidatePath = vi.fn();
8
8
  const mockGetUser = vi.fn();
9
9
 
10
10
  vi.mock("next/cache", () => ({
11
- revalidatePath: (path: string) => mockRevalidatePath(path),
11
+ revalidatePath: (path: string): unknown => mockRevalidatePath(path),
12
12
  }));
13
13
 
14
14
  vi.mock("@/shared/lib/supabase/server", () => ({
@@ -1,7 +1,8 @@
1
1
  "use server";
2
2
 
3
- import { revalidatePath } from "next/cache";
4
3
  import { eq } from "drizzle-orm";
4
+ import { revalidatePath } from "next/cache";
5
+
5
6
  import { db } from "@/shared/db";
6
7
  import { todos } from "@/shared/db/schema";
7
8
  import { createClient } from "@/shared/lib/supabase/server";
@@ -1,14 +1,16 @@
1
1
  "use client";
2
2
 
3
3
  import { useRef, useTransition } from "react";
4
+
4
5
  import { addTodoAction } from "../actions/todos.action";
6
+
5
7
  import { Button } from "@/shared/components/ui/button";
6
8
 
7
9
  export function AddTodoForm() {
8
10
  const [isPending, startTransition] = useTransition();
9
11
  const inputRef = useRef<HTMLInputElement>(null);
10
12
 
11
- function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
13
+ function handleSubmit(e: React.SyntheticEvent<HTMLFormElement>) {
12
14
  e.preventDefault();
13
15
  const formData = new FormData(e.currentTarget);
14
16
  startTransition(async () => {
@@ -1,9 +1,11 @@
1
1
  "use client";
2
2
 
3
3
  import { useTransition } from "react";
4
- import { toggleTodoAction, deleteTodoAction } from "../actions/todos.action";
5
- import type { SelectTodo } from "@/shared/db/schema";
4
+
5
+ import { deleteTodoAction, toggleTodoAction } from "../actions/todos.action";
6
+
6
7
  import { Spinner } from "@/shared/components/ui/spinner";
8
+ import type { SelectTodo } from "@/shared/db/schema";
7
9
 
8
10
  interface TodoListProps {
9
11
  items: SelectTodo[];
@@ -33,7 +35,7 @@ export function TodoList({ items }: TodoListProps) {
33
35
  checked={todo.completed}
34
36
  disabled={isPending}
35
37
  onChange={() => {
36
- startTransition(() => toggleTodoAction(todo.id));
38
+ startTransition(() => { void toggleTodoAction(todo.id); });
37
39
  }}
38
40
  aria-label={`Marcar "${todo.title}" como completada`}
39
41
  />
@@ -46,7 +48,7 @@ export function TodoList({ items }: TodoListProps) {
46
48
  type="button"
47
49
  disabled={isPending}
48
50
  onClick={() => {
49
- startTransition(() => deleteTodoAction(todo.id));
51
+ startTransition(() => { void deleteTodoAction(todo.id); });
50
52
  }}
51
53
  className="rounded px-2 py-1 text-sm text-red-500 hover:bg-red-50"
52
54
  aria-label={`Eliminar "${todo.title}"`}
@@ -1,22 +1,22 @@
1
- import { describe, it, expect, vi } from "vitest";
2
1
  import { NextRequest } from "next/server";
2
+ import { describe, it, expect, vi } from "vitest";
3
3
 
4
4
  const mockUpdateSession = vi.fn();
5
5
 
6
6
  vi.mock("@/shared/lib/supabase/middleware", () => ({
7
- updateSession: (...args: unknown[]) => mockUpdateSession(...args),
7
+ updateSession: (...args: unknown[]): unknown => mockUpdateSession(...args),
8
8
  }));
9
9
 
10
- describe("middleware", () => {
10
+ describe("proxy", () => {
11
11
  it("delegates to updateSession", async () => {
12
12
  // Arrange
13
13
  const mockResponse = new Response(null, { status: 200 });
14
14
  mockUpdateSession.mockResolvedValue(mockResponse);
15
- const { middleware } = await import("../../middleware");
15
+ const mod = await import("../../../proxy") as { proxy: (req: NextRequest) => Promise<Response> };
16
16
  const req = new NextRequest("http://localhost/dashboard");
17
17
 
18
18
  // Act
19
- const response = await middleware(req);
19
+ const response = await mod.proxy(req);
20
20
 
21
21
  // Assert
22
22
  expect(mockUpdateSession).toHaveBeenCalledWith(req);
@@ -25,10 +25,10 @@ describe("middleware", () => {
25
25
 
26
26
  it("exports a matcher config", async () => {
27
27
  // Arrange + Act
28
- const { config } = await import("../../middleware");
28
+ const mod = await import("../../../proxy") as { config: { matcher: string[] } };
29
29
 
30
30
  // Assert
31
- expect(config.matcher).toBeDefined();
32
- expect(Array.isArray(config.matcher)).toBe(true);
31
+ expect(mod.config.matcher).toBeDefined();
32
+ expect(Array.isArray(mod.config.matcher)).toBe(true);
33
33
  });
34
34
  });
@@ -1,31 +1,34 @@
1
+ import type * as NextServerTypes from "next/server";
1
2
  import { describe, it, expect, vi, beforeEach } from "vitest";
2
3
 
3
4
  const mockGetUser = vi.fn();
4
5
  const mockCreateServerClient = vi.fn();
5
6
  const mockNextResponseNext = vi.fn();
6
7
  const mockNextResponseRedirect = vi.fn();
8
+ const mockRequestGetAll = vi.fn(() => []);
9
+ const mockRequestSet = vi.fn();
7
10
 
8
11
  vi.mock("@supabase/ssr", () => ({
9
- createServerClient: (...args: unknown[]) => mockCreateServerClient(...args),
12
+ createServerClient: (...args: unknown[]): unknown => mockCreateServerClient(...args),
10
13
  }));
11
14
 
12
15
  vi.mock("next/server", async () => {
13
- const actual = await vi.importActual<typeof import("next/server")>("next/server");
16
+ const actual = await vi.importActual<typeof NextServerTypes>("next/server");
14
17
  return {
15
18
  ...actual,
16
19
  NextResponse: {
17
- next: (...args: unknown[]) => mockNextResponseNext(...args),
18
- redirect: (...args: unknown[]) => mockNextResponseRedirect(...args),
20
+ next: (...args: unknown[]): unknown => mockNextResponseNext(...args),
21
+ redirect: (...args: unknown[]): unknown => mockNextResponseRedirect(...args),
19
22
  },
20
23
  };
21
24
  });
22
25
 
23
- type CookieOptions = { path?: string; maxAge?: number };
24
- type CookieItem = { name: string; value: string; options: CookieOptions };
25
- type CookieCallbacks = {
26
+ interface CookieOptions { path?: string; maxAge?: number }
27
+ interface CookieItem { name: string; value: string; options: CookieOptions }
28
+ interface CookieCallbacks {
26
29
  getAll: () => CookieItem[];
27
30
  setAll: (cookies: CookieItem[]) => void;
28
- };
31
+ }
29
32
 
30
33
  function makeRequest(pathname: string) {
31
34
  return {
@@ -33,10 +36,10 @@ function makeRequest(pathname: string) {
33
36
  pathname,
34
37
  clone: () => ({ pathname, toString: () => `http://localhost${pathname}` }),
35
38
  },
36
- cookies: { getAll: vi.fn(() => []), set: vi.fn() },
39
+ cookies: { getAll: mockRequestGetAll, set: mockRequestSet },
37
40
  headers: new Headers(),
38
41
  url: `http://localhost${pathname}`,
39
- } as unknown as import("next/server").NextRequest;
42
+ } as unknown as NextServerTypes.NextRequest;
40
43
  }
41
44
 
42
45
  describe("supabase middleware (updateSession)", () => {
@@ -85,10 +88,11 @@ describe("supabase middleware (updateSession)", () => {
85
88
  await updateSession(req);
86
89
 
87
90
  // Act
88
- capturedCookieCallbacks!.getAll();
91
+ if (!capturedCookieCallbacks) throw new Error("capturedCookieCallbacks not set");
92
+ capturedCookieCallbacks.getAll();
89
93
 
90
94
  // Assert
91
- expect(req.cookies.getAll).toHaveBeenCalled();
95
+ expect(mockRequestGetAll).toHaveBeenCalled();
92
96
  });
93
97
 
94
98
  it("invokes setAll cookie callback and updates request + response cookies", async () => {
@@ -99,12 +103,13 @@ describe("supabase middleware (updateSession)", () => {
99
103
  await updateSession(req);
100
104
 
101
105
  // Act
102
- capturedCookieCallbacks!.setAll([
106
+ if (!capturedCookieCallbacks) throw new Error("capturedCookieCallbacks not set");
107
+ capturedCookieCallbacks.setAll([
103
108
  { name: "sb-token", value: "abc", options: { path: "/" } },
104
109
  ]);
105
110
 
106
111
  // Assert
107
- expect(req.cookies.set).toHaveBeenCalledWith("sb-token", "abc");
112
+ expect(mockRequestSet).toHaveBeenCalledWith("sb-token", "abc");
108
113
  expect(fakeResponse.cookies.set).toHaveBeenCalledWith("sb-token", "abc", { path: "/" });
109
114
  });
110
115
 
@@ -5,7 +5,7 @@ const mockGetAll = vi.fn().mockReturnValue([]);
5
5
  const mockCreateServerClient = vi.fn();
6
6
 
7
7
  vi.mock("@supabase/ssr", () => ({
8
- createServerClient: (...args: unknown[]) => mockCreateServerClient(...args),
8
+ createServerClient: (...args: unknown[]): unknown => mockCreateServerClient(...args),
9
9
  }));
10
10
 
11
11
  vi.mock("next/headers", () => ({
@@ -43,7 +43,8 @@ describe("supabase server", () => {
43
43
 
44
44
  // Act
45
45
  await createClient();
46
- capturedGetAll!();
46
+ if (!capturedGetAll) throw new Error("capturedGetAll not set");
47
+ capturedGetAll();
47
48
 
48
49
  // Assert
49
50
  expect(mockGetAll).toHaveBeenCalled();
@@ -64,7 +65,8 @@ describe("supabase server", () => {
64
65
  await createClient();
65
66
 
66
67
  // Act
67
- capturedSetAll!([{ name: "token", value: "abc", options: { path: "/" } }]);
68
+ if (!capturedSetAll) throw new Error("capturedSetAll not set");
69
+ capturedSetAll([{ name: "token", value: "abc", options: { path: "/" } }]);
68
70
 
69
71
  // Assert
70
72
  expect(mockSet).toHaveBeenCalledWith("token", "abc", { path: "/" });
@@ -88,8 +90,10 @@ describe("supabase server", () => {
88
90
  await createClient();
89
91
 
90
92
  // Act + Assert
91
- expect(() =>
92
- capturedSetAll!([{ name: "token", value: "abc", options: {} }])
93
- ).not.toThrow();
93
+ if (!capturedSetAll) throw new Error("capturedSetAll not set");
94
+ const setAll = capturedSetAll;
95
+ expect(() => {
96
+ setAll([{ name: "token", value: "abc", options: {} }]);
97
+ }).not.toThrow();
94
98
  });
95
99
  });
@@ -9,8 +9,11 @@ describe("cn", () => {
9
9
  });
10
10
 
11
11
  it("omits falsy conditional classes", () => {
12
- // Arrange + Act + Assert
13
- expect(cn("foo", false && "bar", "baz")).toBe("foo baz");
12
+ // Arrange
13
+ const falsy = false as boolean;
14
+
15
+ // Act + Assert
16
+ expect(cn("foo", falsy && "bar", "baz")).toBe("foo baz");
14
17
  });
15
18
 
16
19
  it("handles undefined values", () => {
@@ -2,9 +2,10 @@ import { Slot } from "@radix-ui/react-slot"
2
2
  import { cva, type VariantProps } from "class-variance-authority"
3
3
  import * as React from "react"
4
4
 
5
- import { cn } from "@/shared/lib/utils"
6
5
  import { Spinner } from "./spinner"
7
6
 
7
+ import { cn } from "@/shared/lib/utils"
8
+
8
9
  const buttonVariants = cva(
9
10
  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
10
11
  {
@@ -1,4 +1,5 @@
1
1
  import { LoaderCircle } from "lucide-react";
2
+
2
3
  import { cn } from "@/shared/lib/utils";
3
4
 
4
5
  export function Spinner({ className }: { className?: string }) {