Verify-suite work (this session):

- M  package.json — added typecheck, test:e2e, verify scripts; added @playwright/test devDep
  - M  pnpm-lock.yaml
  - M  .gitignore — Playwright artifacts
  - M  tsconfig.json — auto-modified by Next.js include path
  - ??  e2e/ — config + fixtures + 3 specs
  - ??  playwright.config.ts
  - ??  scripts/verify.sh, scripts/db-test-truncate.sql
This commit is contained in:
2026-05-10 07:42:46 -04:00
parent 6fd8b0317e
commit 9e7d589c22
12 changed files with 355 additions and 9 deletions

25
e2e/admin.spec.ts Normal file
View File

@@ -0,0 +1,25 @@
import { expect, test } from "@playwright/test";
import { signInAs } from "./fixtures/auth";
test("admin signs in and reaches each admin section", async ({ page }) => {
await signInAs(page, "admin", "/admin");
await page.goto("/admin");
await expect(page).toHaveURL(/\/admin\/bookings$/);
for (const [label, urlRe] of [
["bookings", /\/admin\/bookings/],
["services", /\/admin\/services/],
["rooms", /\/admin\/rooms/],
["therapists", /\/admin\/therapists/],
] as const) {
await page.getByRole("link", { name: new RegExp(`^${label}$`, "i") }).click();
await expect(page).toHaveURL(urlRe);
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
}
});
test("non-admin (therapist) hitting /admin sees the deny screen", async ({ page }) => {
await signInAs(page, "therapist", "/admin");
await page.goto("/admin");
await expect(page.getByRole("heading", { name: /not authorized/i })).toBeVisible();
});

43
e2e/customer.spec.ts Normal file
View File

@@ -0,0 +1,43 @@
import { expect, test } from "@playwright/test";
import { signInAs } from "./fixtures/auth";
test("customer can browse and reach the booking confirm page", async ({ page }) => {
// /book/confirm requires an authenticated session, so sign in first.
await signInAs(page, "customer", "/");
await page.goto("/");
await expect(page.getByRole("heading", { name: /book a session/i })).toBeVisible();
// First service "Book" link → /book?serviceId=...
const firstBook = page.locator('a[href^="/book?serviceId="]').first();
await expect(firstBook).toBeVisible();
await firstBook.click();
await expect(page).toHaveURL(/\/book\?serviceId=/);
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
// Pick a date a few days out so slots are likely available.
const dateInput = page.locator('input[type="date"][name="date"]');
await expect(dateInput).toBeVisible();
const target = new Date();
target.setDate(target.getDate() + 3);
const iso = target.toISOString().slice(0, 10);
await dateInput.fill(iso);
await page.getByRole("button", { name: /show times/i }).click();
await expect(page).toHaveURL(new RegExp(`date=${iso}`));
// Grab the first slot's href and visit it directly.
const slot = page.locator('a[href^="/book/confirm?"]').first();
await expect(slot, "expected at least one available slot").toBeVisible();
const slotHref = await slot.getAttribute("href");
expect(slotHref).toMatch(/^\/book\/confirm\?/);
await page.goto(slotHref!);
// Already authenticated, so we land on the confirm form (not /login).
await expect(page).toHaveURL(/\/book\/confirm\?/);
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
// /account/bookings should be reachable while signed in.
await page.goto("/account/bookings");
await expect(page).toHaveURL(/\/account\/bookings/);
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
});

34
e2e/fixtures/auth.ts Normal file
View File

@@ -0,0 +1,34 @@
import { expect, type Page } from "@playwright/test";
import { clearMailpit, waitForMagicLink } from "./mailpit";
export const ROLE_EMAIL = {
admin: "admin@touchbase.local",
therapist: "mei@touchbase.local",
customer: "alex@example.com",
} as const;
export type RoleKey = keyof typeof ROLE_EMAIL;
export async function signInAs(page: Page, role: RoleKey, callbackUrl = "/"): Promise<void> {
await clearMailpit();
const email = ROLE_EMAIL[role];
await page.goto(`/login?callbackUrl=${encodeURIComponent(callbackUrl)}`);
await page.getByLabel("Email").fill(email);
// Auth.js v5 server-action signIn() redirects to either our /login/check-email
// page or its internal /api/auth/verify-request. Either is fine — the email
// is sent regardless. Just wait for any nav off /login.
await Promise.all([
page.waitForURL((url) => !/\/login(\?|$)/.test(url.pathname + url.search)),
page.getByRole("button", { name: /send sign-in link/i }).click(),
]);
const link = await waitForMagicLink(email);
await page.goto(link);
// After callback, NextAuth redirects to callbackUrl. Verify session cookie present.
await expect.poll(async () => {
const cookies = await page.context().cookies();
return cookies.some((c) => /authjs.*session-token/i.test(c.name) || /next-auth.*session-token/i.test(c.name));
}).toBeTruthy();
}

41
e2e/fixtures/mailpit.ts Normal file
View File

@@ -0,0 +1,41 @@
const MAILPIT = process.env.MAILPIT_URL ?? "http://localhost:8025";
export async function clearMailpit(): Promise<void> {
const res = await fetch(`${MAILPIT}/api/v1/messages`, { method: "DELETE" });
if (!res.ok) throw new Error(`Mailpit clear failed: ${res.status}`);
}
type MessageSummary = { ID: string; To: { Address: string }[]; Created: string };
async function listMessagesTo(email: string): Promise<MessageSummary[]> {
const url = `${MAILPIT}/api/v1/search?query=${encodeURIComponent(`to:${email}`)}`;
const res = await fetch(url);
if (!res.ok) throw new Error(`Mailpit search failed: ${res.status}`);
const data = (await res.json()) as { messages: MessageSummary[] };
return data.messages ?? [];
}
async function getMessageBody(id: string): Promise<{ text: string; html: string }> {
const res = await fetch(`${MAILPIT}/api/v1/message/${id}`);
if (!res.ok) throw new Error(`Mailpit fetch failed: ${res.status}`);
const data = (await res.json()) as { Text?: string; HTML?: string };
return { text: data.Text ?? "", html: data.HTML ?? "" };
}
export async function waitForMagicLink(email: string, opts: { timeoutMs?: number } = {}): Promise<string> {
const timeoutMs = opts.timeoutMs ?? 10_000;
const deadline = Date.now() + timeoutMs;
const linkRe = /https?:\/\/[^\s<>"]*\/api\/auth\/callback\/nodemailer[^\s<>"]*/i;
while (Date.now() < deadline) {
const messages = await listMessagesTo(email);
if (messages.length > 0) {
const newest = messages.sort((a, b) => b.Created.localeCompare(a.Created))[0];
const { text, html } = await getMessageBody(newest.ID);
const m = (text.match(linkRe) ?? html.match(linkRe))?.[0];
if (m) return m.replace(/&amp;/g, "&");
}
await new Promise((r) => setTimeout(r, 250));
}
throw new Error(`No magic link for ${email} within ${timeoutMs}ms`);
}

27
e2e/therapist.spec.ts Normal file
View File

@@ -0,0 +1,27 @@
import { expect, test } from "@playwright/test";
import { signInAs } from "./fixtures/auth";
test("therapist signs in and sees their dashboard", async ({ page }) => {
await signInAs(page, "therapist", "/therapist");
// /therapist redirects to /therapist/bookings.
await page.goto("/therapist");
await expect(page).toHaveURL(/\/therapist\/bookings$/);
// Top nav exposes "My schedule" → bookings, and "Availability".
await expect(page.getByRole("link", { name: /^my schedule$/i })).toBeVisible();
await expect(page.getByRole("link", { name: /^availability$/i })).toBeVisible();
await page.getByRole("link", { name: /^availability$/i }).click();
await expect(page).toHaveURL(/\/therapist\/availability/);
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
await page.getByRole("link", { name: /^my schedule$/i }).click();
await expect(page).toHaveURL(/\/therapist\/bookings/);
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
});
test("non-therapist (customer) hitting /therapist sees the deny screen", async ({ page }) => {
await signInAs(page, "customer", "/therapist");
await page.goto("/therapist");
await expect(page.getByRole("heading", { name: /therapists only/i })).toBeVisible();
});

View File

@@ -10,8 +10,11 @@
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "eslint", "lint": "eslint",
"typecheck": "tsc --noEmit",
"test": "vitest run", "test": "vitest run",
"test:watch": "vitest", "test:watch": "vitest",
"test:e2e": "playwright test",
"verify": "scripts/verify.sh",
"db:generate": "prisma generate", "db:generate": "prisma generate",
"db:migrate": "prisma migrate dev", "db:migrate": "prisma migrate dev",
"db:migrate:deploy": "prisma migrate deploy", "db:migrate:deploy": "prisma migrate deploy",
@@ -41,6 +44,7 @@
"zod": "^4.3.6" "zod": "^4.3.6"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.59.1",
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
"@types/node": "^22", "@types/node": "^22",
"@types/nodemailer": "^8.0.0", "@types/nodemailer": "^8.0.0",

35
playwright.config.ts Normal file
View File

@@ -0,0 +1,35 @@
import { defineConfig, devices } from "@playwright/test";
// Dedicated test port — keeps the e2e server isolated from any `pnpm dev`
// the user might have running, and pins it against the touchbase_test DB
// (loaded from .env.test in the webServer command below).
const PORT = Number(process.env.E2E_PORT ?? 3010);
const BASE_URL = process.env.E2E_BASE_URL ?? `http://localhost:${PORT}`;
export default defineConfig({
testDir: "./e2e",
fullyParallel: false,
retries: 0,
workers: 1,
reporter: [["list"]],
timeout: 30_000,
expect: { timeout: 10_000 },
use: {
baseURL: BASE_URL,
trace: "retain-on-failure",
screenshot: "only-on-failure",
video: "off",
},
webServer: {
// dotenv-cli loads .env.test (DATABASE_URL=touchbase_test) for the child.
command: `pnpm exec dotenv -e .env.test -- next dev -p ${PORT}`,
url: `${BASE_URL}/api/health`,
reuseExistingServer: !process.env.CI,
timeout: 60_000,
stdout: "pipe",
stderr: "pipe",
},
projects: [
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
],
});

49
pnpm-lock.yaml generated
View File

@@ -31,10 +31,10 @@ importers:
version: 3.2.0(date-fns@4.1.0) version: 3.2.0(date-fns@4.1.0)
next: next:
specifier: 16.2.4 specifier: 16.2.4
version: 16.2.4(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) version: 16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
next-auth: next-auth:
specifier: 5.0.0-beta.31 specifier: 5.0.0-beta.31
version: 5.0.0-beta.31(next@16.2.4(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(nodemailer@8.0.7)(react@19.2.4) version: 5.0.0-beta.31(next@16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(nodemailer@8.0.7)(react@19.2.4)
nodemailer: nodemailer:
specifier: ^8.0.7 specifier: ^8.0.7
version: 8.0.7 version: 8.0.7
@@ -57,6 +57,9 @@ importers:
specifier: ^4.3.6 specifier: ^4.3.6
version: 4.3.6 version: 4.3.6
devDependencies: devDependencies:
'@playwright/test':
specifier: ^1.59.1
version: 1.59.1
'@tailwindcss/postcss': '@tailwindcss/postcss':
specifier: ^4 specifier: ^4
version: 4.2.4 version: 4.2.4
@@ -682,6 +685,11 @@ packages:
'@panva/hkdf@1.2.1': '@panva/hkdf@1.2.1':
resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==} resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==}
'@playwright/test@1.59.1':
resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==}
engines: {node: '>=18'}
hasBin: true
'@polka/url@1.0.0-next.29': '@polka/url@1.0.0-next.29':
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
@@ -1785,6 +1793,11 @@ packages:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'} engines: {node: '>=14'}
fsevents@2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
fsevents@2.3.3: fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -2435,6 +2448,16 @@ packages:
pkg-types@2.3.1: pkg-types@2.3.1:
resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==}
playwright-core@1.59.1:
resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==}
engines: {node: '>=18'}
hasBin: true
playwright@1.59.1:
resolution: {integrity: sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==}
engines: {node: '>=18'}
hasBin: true
possible-typed-array-names@1.1.0: possible-typed-array-names@1.1.0:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -3475,6 +3498,10 @@ snapshots:
'@panva/hkdf@1.2.1': {} '@panva/hkdf@1.2.1': {}
'@playwright/test@1.59.1':
dependencies:
playwright: 1.59.1
'@polka/url@1.0.0-next.29': {} '@polka/url@1.0.0-next.29': {}
'@prisma/adapter-pg@7.8.0': '@prisma/adapter-pg@7.8.0':
@@ -4733,6 +4760,9 @@ snapshots:
cross-spawn: 7.0.6 cross-spawn: 7.0.6
signal-exit: 4.1.0 signal-exit: 4.1.0
fsevents@2.3.2:
optional: true
fsevents@2.3.3: fsevents@2.3.3:
optional: true optional: true
@@ -5160,15 +5190,15 @@ snapshots:
natural-compare@1.4.0: {} natural-compare@1.4.0: {}
next-auth@5.0.0-beta.31(next@16.2.4(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(nodemailer@8.0.7)(react@19.2.4): next-auth@5.0.0-beta.31(next@16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(nodemailer@8.0.7)(react@19.2.4):
dependencies: dependencies:
'@auth/core': 0.41.2(nodemailer@8.0.7) '@auth/core': 0.41.2(nodemailer@8.0.7)
next: 16.2.4(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next: 16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react: 19.2.4 react: 19.2.4
optionalDependencies: optionalDependencies:
nodemailer: 8.0.7 nodemailer: 8.0.7
next@16.2.4(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): next@16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies: dependencies:
'@next/env': 16.2.4 '@next/env': 16.2.4
'@swc/helpers': 0.5.15 '@swc/helpers': 0.5.15
@@ -5187,6 +5217,7 @@ snapshots:
'@next/swc-linux-x64-musl': 16.2.4 '@next/swc-linux-x64-musl': 16.2.4
'@next/swc-win32-arm64-msvc': 16.2.4 '@next/swc-win32-arm64-msvc': 16.2.4
'@next/swc-win32-x64-msvc': 16.2.4 '@next/swc-win32-x64-msvc': 16.2.4
'@playwright/test': 1.59.1
sharp: 0.34.5 sharp: 0.34.5
transitivePeerDependencies: transitivePeerDependencies:
- '@babel/core' - '@babel/core'
@@ -5345,6 +5376,14 @@ snapshots:
exsolve: 1.0.8 exsolve: 1.0.8
pathe: 2.0.3 pathe: 2.0.3
playwright-core@1.59.1: {}
playwright@1.59.1:
dependencies:
playwright-core: 1.59.1
optionalDependencies:
fsevents: 2.3.2
possible-typed-array-names@1.1.0: {} possible-typed-array-names@1.1.0: {}
postcss@8.4.31: postcss@8.4.31:

BIN
ruvector.db Normal file

Binary file not shown.

View File

@@ -0,0 +1,22 @@
-- Truncate all Prisma-managed application tables in touchbase_test.
-- Used by `pnpm verify` to give Playwright a clean fixture-only DB state
-- without triggering Prisma's AI-invocation guard on `migrate reset`.
-- pg-boss tables (schema "pgboss") are intentionally left alone.
TRUNCATE TABLE
"AuditLog",
"Notification",
"Payment",
"Booking",
"AvailabilityOverride",
"WorkingHours",
"ServiceTherapist",
"Service",
"RoomBlock",
"RoomTag",
"Room",
"TherapistTag",
"Therapist",
"Customer",
"VerificationToken",
"User"
RESTART IDENTITY CASCADE;

67
scripts/verify.sh Executable file
View File

@@ -0,0 +1,67 @@
#!/usr/bin/env bash
# Run the full verify suite: lint, typecheck, vitest, Playwright e2e.
# Uses the dedicated touchbase_test DB (reset + seeded) and a Next.js
# server on E2E_PORT (3010) so it never touches the dev DB or dev server.
set -euo pipefail
cd "$(dirname "$0")/.."
bold() { printf "\n\033[1m==> %s\033[0m\n" "$*"; }
fail() { printf "\033[31merror:\033[0m %s\n" "$*" >&2; exit 1; }
E2E_PORT="${E2E_PORT:-3010}"
export E2E_PORT
# --- Pre-flight ---------------------------------------------------------------
bold "Pre-flight"
curl -sf http://localhost:5432 -o /dev/null || true # informational
curl -sf "http://localhost:8025/api/v1/info" >/dev/null \
|| fail "Mailpit not reachable on http://localhost:8025. Run: docker compose up -d mailpit"
if ! docker ps --format '{{.Names}}' | grep -q '^touchbase-postgres-1$'; then
fail "Postgres container 'touchbase-postgres-1' not running. Run: docker compose up -d postgres"
fi
# Next 16 enforces one `next dev` per project. If one is running it'll block
# the e2e test server from starting on E2E_PORT.
if pgrep -f "next dev" >/dev/null 2>&1; then
fail "Another \`next dev\` is running (Next 16 enforces one per project).
Stop it before running verify: pkill -f 'next dev'
Then re-run: pnpm verify"
fi
# --- Lint ---------------------------------------------------------------------
bold "Lint"
pnpm lint
# --- Typecheck ----------------------------------------------------------------
bold "Typecheck"
pnpm exec tsc --noEmit
# --- Test DB: ensure schema is current ---------------------------------------
bold "Apply migrations to test DB"
pnpm exec dotenv -e .env.test -- prisma migrate deploy
# --- Vitest -------------------------------------------------------------------
# Vitest tests load .env.test via test/setup.ts and manage their own data;
# we don't need to reset before this step.
bold "Vitest"
pnpm test
# --- Truncate + seed test DB before Playwright ------------------------------
# Vitest may have left rows around. Playwright fixtures (admin/therapist/
# customer accounts, services, rooms) need a known seeded state.
# Truncate via psql (faster than `migrate reset`, avoids Prisma's AI guard).
bold "Truncate + seed test DB"
docker exec -i touchbase-postgres-1 \
psql -U touchbase -d touchbase_test -v ON_ERROR_STOP=1 -q \
< scripts/db-test-truncate.sql
pnpm exec dotenv -e .env.test -- tsx prisma/seed.ts
# --- Playwright e2e -----------------------------------------------------------
# playwright.config.ts has a webServer block that boots `next dev` on
# E2E_PORT against .env.test, so we don't manage the server here.
bold "Playwright e2e (port $E2E_PORT)"
pnpm exec playwright test
bold "All green"

View File

@@ -1,7 +1,11 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2017", "target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"], "lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
@@ -19,7 +23,9 @@
} }
], ],
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": [
"./src/*"
]
} }
}, },
"include": [ "include": [
@@ -28,7 +34,10 @@
"**/*.tsx", "**/*.tsx",
".next/types/**/*.ts", ".next/types/**/*.ts",
".next/dev/types/**/*.ts", ".next/dev/types/**/*.ts",
"**/*.mts" "**/*.mts",
".next/dev/dev/types/**/*.ts"
], ],
"exclude": ["node_modules"] "exclude": [
"node_modules"
]
} }