Lucia Authentication #8

Merged
DanMihailescu merged 18 commits from undoPretty into master 2024-12-19 20:06:45 -05:00
54 changed files with 622 additions and 217 deletions
Showing only changes of commit 7ecfb15818 - Show all commits

4
.env
View file

@ -1,2 +1,2 @@
APP_VERSION=1.0.0-alpha
DATABASE_URL="file:./dev.db"
VITE_APP_VERSION=1.0.0-alpha
DATABASE_URL="file:./dev.db"

View file

@ -1,14 +0,0 @@
name: PR Checks
on: [pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: install
run: bun install
- name: build
run: bun run clean && bun run build

20
.gitignore vendored
View file

@ -1,2 +1,20 @@
test-results
node_modules
build
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
*storybook.log

1
.npmrc Normal file
View file

@ -0,0 +1 @@
engine-strict=true

4
.prettierignore Normal file
View file

@ -0,0 +1,4 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock

15
.prettierrc Normal file
View file

@ -0,0 +1,15 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}

16
.storybook/main.js Normal file
View file

@ -0,0 +1,16 @@
/** @type { import('@storybook/sveltekit').StorybookConfig } */
const config = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|ts|svelte)'],
addons: [
'@storybook/addon-svelte-csf',
'@storybook/addon-essentials',
'@chromatic-com/storybook',
'@storybook/addon-interactions',
'storybook-dark-mode'
],
framework: {
name: '@storybook/sveltekit',
options: {}
}
};
export default config;

13
.storybook/preview.js Normal file
View file

@ -0,0 +1,13 @@
/** @type { import('@storybook/svelte').Preview } */
const preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i
}
}
}
};
export default preview;

View file

@ -1,22 +1,38 @@
# Hestia
# sv
To install dependencies:
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
bun install
# create a new project in the current directory
bunx sv create
# create a new project in my-app
bunx sv create my-app
```
To run:
## Developing
Once you've created a project and installed dependencies with `bun install`, start a development server:
```bash
bun run src/index.ts
bun run dev
# or start the server and open the app in a new browser tab
bun run dev -- --open
```
## Stack
## Building
- **Bun** Package manager
- **Yoga** GraphQL Server
- **Pothos** GraphQL Schema Builder
- **Prisma** Database ORM
- **Pino** Logger
- **Zod** Schema validation
To create a production version of your app:
```bash
bun run build
```
You can preview the production build with `bun run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.

BIN
bun.lockb

Binary file not shown.

6
e2e/demo.test.ts Normal file
View file

@ -0,0 +1,6 @@
import { expect, test } from '@playwright/test';
test('home page has expected h1', async ({ page }) => {
await page.goto('/');
await expect(page.locator('h1')).toBeVisible();
});

34
eslint.config.js Normal file
View file

@ -0,0 +1,34 @@
import prettier from 'eslint-config-prettier';
import js from '@eslint/js';
import { includeIgnoreFile } from '@eslint/compat';
import svelte from 'eslint-plugin-svelte';
import globals from 'globals';
import { fileURLToPath } from 'node:url';
import ts from 'typescript-eslint';
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
export default ts.config(
includeIgnoreFile(gitignorePath),
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs['flat/recommended'],
prettier,
...svelte.configs['flat/prettier'],
{
languageOptions: {
globals: {
...globals.browser,
...globals.node
}
}
},
{
files: ['**/*.svelte'],
languageOptions: {
parserOptions: {
parser: ts.parser
}
}
}
);

View file

@ -1,14 +0,0 @@
// @ts-check
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
// @ts-expect-error No type-def
import eslintConfigPrettier from 'eslint-config-prettier';
export default tseslint.config(
{ files: ['{app,src}/**/*.{js,mjs,ts}'] },
{ ignores: ['build/*'] },
eslint.configs.recommended,
tseslint.configs.recommended,
eslintConfigPrettier
);

View file

@ -1,36 +1,66 @@
{
"name": "hestia",
"module": "src/index.ts",
"version": "0.0.1",
"type": "module",
"scripts": {
"build": "bun build ./src/index.ts --outdir ./build",
"clean": "rm -rf ./build",
"dev": "bun --watch src/index.ts | pino-pretty",
"format": "prettier . --write",
"lint": "",
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"format": "prettier --write .",
"lint": "prettier --check . && eslint .",
"test:unit": "vitest",
"test": "npm run test:unit -- --run && npm run test:e2e",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"test:e2e": "playwright test",
"prisma:generate": "prisma generate"
},
"devDependencies": {
"@eslint/js": "^9.16.0",
"@types/bun": "latest",
"eslint": "^9.16.0",
"@chromatic-com/storybook": "^3.2.2",
"@eslint/compat": "^1.2.3",
"@playwright/test": "^1.45.3",
"@storybook/addon-essentials": "^8.4.7",
"@storybook/addon-interactions": "^8.4.7",
"@storybook/addon-svelte-csf": "^5.0.0-next.13",
"@storybook/blocks": "^8.4.7",
"@storybook/svelte": "^8.4.7",
"@storybook/sveltekit": "^8.4.7",
"@storybook/test": "^8.4.7",
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.9.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"autoprefixer": "^10.4.20",
"eslint": "^9.7.0",
"eslint-config-prettier": "^9.1.0",
"prettier": "3.4.1",
"eslint-plugin-svelte": "^2.36.0",
"globals": "^15.0.0",
"prettier": "^3.3.2",
"prettier-plugin-svelte": "^3.2.6",
"prettier-plugin-tailwindcss": "^0.6.5",
"prisma": "^6.0.1",
"typescript-eslint": "^8.17.0"
},
"peerDependencies": {
"typescript": "^5.0.0"
"storybook": "^8.4.7",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwindcss": "^3.4.9",
"typescript": "^5.0.0",
"typescript-eslint": "^8.0.0",
"vite": "^6.0.0",
"vitest": "^2.0.4"
},
"dependencies": {
"@pothos/core": "^4.3.0",
"@pothos/plugin-prisma": "^4.4.0",
"@prisma/client": "6.0.1",
"@tailwindcss/typography": "^0.5.15",
"@types/bun": "^1.1.14",
"graphql": "^16.9.0",
"graphql-yoga": "^5.10.4",
"lucia": "^3.2.2",
"pino": "^9.5.0",
"pino-pretty": "^13.0.0",
"zod": "^3.23.8"
"storybook-dark-mode": "^4.0.2",
"zod": "^3.24.0"
}
}
}

10
playwright.config.ts Normal file
View file

@ -0,0 +1,10 @@
import { defineConfig } from '@playwright/test';
export default defineConfig({
webServer: {
command: 'npm run build && npm run preview',
port: 4173
},
testDir: 'e2e'
});

6
postcss.config.js Normal file
View file

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};

View file

@ -1,13 +0,0 @@
/**
* @see https://prettier.io/docs/en/configuration.html
* @type {import("prettier").Config}
*/
const config = {
trailingComma: 'es5',
tabWidth: 4,
useTabs: true,
semi: true,
singleQuote: true,
};
export default config;

Binary file not shown.

Binary file not shown.

View file

@ -28,4 +28,4 @@ model Post {
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
}
}

3
src/app.css Normal file
View file

@ -0,0 +1,3 @@
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';

13
src/app.d.ts vendored Normal file
View file

@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

12
src/app.html Normal file
View file

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

7
src/demo.spec.ts Normal file
View file

@ -0,0 +1,7 @@
import { describe, it, expect } from 'vitest';
describe('sum test', () => {
it('adds 1 + 2 to equal 3', () => {
expect(1 + 2).toBe(3);
});
});

View file

@ -1,15 +0,0 @@
import { logger } from '@lib/logger';
import { yoga } from './yoga';
const server = Bun.serve({
fetch: yoga.fetch,
error: (error) => {
logger.error(error.message);
return new Response('', {
status: 500,
statusText: 'You fucked the goose',
});
},
});
logger.info(`Server is running on: ${server.url}${yoga.graphqlEndpoint}`);

View file

@ -0,0 +1,30 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import Button from './Button.svelte';
import { fn } from '@storybook/test';
const { Story } = defineMeta({
title: 'Button',
component: Button,
tags: ['autodocs'],
args: {
onClick: fn()
},
argTypes: {
size: {
control: 'select',
options: ['small', 'normal', 'large'],
defaultValue: 'normal'
},
backgroundColor: {
control: 'color'
},
primary: {
control: 'boolean',
defaultValue: true
}
}
});
</script>
<Story name="Default" args={{ label: 'Button', size: 'normal', primary: true }} />

View file

@ -0,0 +1,51 @@
<script lang="ts">
import type { HTMLButtonAttributes } from 'svelte/elements';
let {
type = 'button',
onClick,
label,
size = 'normal',
backgroundColor,
primary = false
}: {
type: HTMLButtonAttributes['type'];
onClick: () => void;
label: string;
size: 'small' | 'normal' | 'large';
backgroundColor: string;
primary: boolean;
} = $props();
</script>
<button
{type}
onclick={onClick}
class={`button button--${size}`}
style:background-color={backgroundColor}
class:button--primary={primary}
class:button--secondary={!primary}
>
{label}
</button>
<style>
.button {
@apply inline-block cursor-pointer rounded-full border-0 font-semibold leading-none transition-colors hover:opacity-80 hover:shadow-lg;
}
.button--small {
@apply px-2 py-0.5 text-sm;
}
.button--normal {
@apply px-2 py-1 text-base;
}
.button--large {
@apply px-2.5 py-1 text-xl;
}
.button--primary {
@apply bg-red-600 text-white;
}
.button--secondary {
@apply bg-blue-600 text-white;
}
</style>

View file

@ -0,0 +1,12 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import Loader from './Loader.svelte';
const { Story } = defineMeta({
title: 'Loader',
component: Loader,
tags: ['autodocs']
});
</script>
<Story name="Default" args={{}} />

View file

@ -0,0 +1,51 @@
<script lang="ts"></script>
<span class="loader"></span>
<style>
.loader {
width: 48px;
height: 48px;
border: 3px dotted #fff;
border-style: solid solid dotted dotted;
border-radius: 50%;
display: inline-block;
position: relative;
box-sizing: border-box;
animation: rotation 2s linear infinite;
}
.loader::after {
content: '';
box-sizing: border-box;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
border: 3px dotted #ff3d00;
border-style: solid solid dotted;
width: 24px;
height: 24px;
border-radius: 50%;
animation: rotationBack 1s linear infinite;
transform-origin: center center;
}
@keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes rotationBack {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(-360deg);
}
}
</style>

View file

@ -0,0 +1,12 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import Navbar from './Navbar.svelte';
const { Story } = defineMeta({
title: 'Navbar',
component: Navbar,
tags: ['autodocs']
});
</script>
<Story name="Default" args={{ title: 'Storybook' }} />

View file

@ -0,0 +1,31 @@
<script lang="ts">
let { title }: { title: string } = $props();
</script>
<header>
<div class="navbar">
<div>
<h2>Hestia</h2>
</div>
<div>
<h1>{title}</h1>
</div>
<div>
<p>Welcome!</p>
</div>
</div>
</header>
<style>
.navbar {
@apply flex items-center justify-between border-slate-200 bg-slate-100 px-6 py-2 font-display drop-shadow;
}
.navbar h1 {
@apply text-2xl;
}
.navbar h2 {
@apply text-xl;
}
</style>

View file

@ -0,0 +1 @@
export * from './Navbar.svelte';

View file

@ -1,4 +1,4 @@
import { logger } from '@lib/logger';
import { logger } from '$lib/logger';
import { z } from 'zod';
export interface Configuration {
@ -8,15 +8,17 @@ export interface Configuration {
export const LoadConfig = (): Configuration => {
const { success, data, error } = z
.object({
APP_VERSION: z.string().default('development'),
VITE_APP_VERSION: z.string().default('development')
})
.safeParse(process.env);
.safeParse(import.meta.env);
if (!success) {
logger.error(error.message);
}
return {
app_version: data!.APP_VERSION,
app_version: data!.VITE_APP_VERSION
};
};
export const Config = LoadConfig();

2
src/lib/index.ts Normal file
View file

@ -0,0 +1,2 @@
// place files you want to import through the `$lib` alias in this folder.
export * from './components';

View file

@ -19,5 +19,5 @@ export const yogaLogger: YogaLogger = {
error(...args) {
// @ts-expect-error types dont match
logger.error(...args);
},
};
}
};

65
src/lib/pothos/index.ts Normal file
View file

@ -0,0 +1,65 @@
import { prisma } from '$lib/prisma';
import { Context } from '$lib/yoga/context';
import SchemaBuilder from '@pothos/core';
import PrismaPlugin, { type PrismaTypesFromClient } from '@pothos/plugin-prisma';
type ContextType = ReturnType<typeof Context>;
export const builder = new SchemaBuilder<{
Context: ContextType;
PrismaTypes: PrismaTypesFromClient<typeof prisma>;
}>({
plugins: [PrismaPlugin],
prisma: {
client: prisma,
// defaults to false, uses /// comments from prisma schema as descriptions
// for object types, relations and exposed fields.
// descriptions can be omitted by setting description to false
exposeDescriptions: false,
// use where clause from prismaRelatedConnection for totalCount (defaults to true)
filterConnectionTotalCount: true,
// warn when not using a query parameter correctly
onUnusedQuery: process.env.NODE_ENV === 'production' ? null : 'warn'
}
});
const User = builder.prismaObject('User', {
fields: (t) => ({
id: t.exposeID('id'),
email: t.exposeString('email'),
name: t.exposeString('name'),
posts: t.relation('posts')
})
});
const Post = builder.prismaObject('Post', {
fields: (t) => ({
id: t.exposeID('id'),
title: t.exposeString('title'),
content: t.exposeString('content'),
published: t.exposeBoolean('published'),
author: t.relation('author')
})
});
builder.queryType({
fields: (t) => ({
version: t.string({
resolve: (parent, args, context) => context.config.app_version
}),
users: t.prismaField({
type: [User],
resolve: async () => {
return await prisma.user.findMany();
}
}),
posts: t.prismaField({
type: [Post],
resolve: async () => {
return await prisma.post.findMany();
}
})
})
});
export const Schema = builder.toSchema();

View file

@ -1,3 +1,3 @@
import { PrismaClient } from '@prisma/client';
export const prisma = new PrismaClient();
export const prisma = new PrismaClient();

7
src/lib/yoga/context.ts Normal file
View file

@ -0,0 +1,7 @@
import { Config } from '$lib/config';
import type { YogaInitialContext } from 'graphql-yoga';
export const Context = (initialContext: YogaInitialContext) => ({
...initialContext,
config: Config
});

14
src/lib/yoga/index.ts Normal file
View file

@ -0,0 +1,14 @@
import { yogaLogger } from '$lib/logger';
import { Schema } from '$lib/pothos';
import type { RequestEvent } from '@sveltejs/kit';
import { createYoga } from 'graphql-yoga';
import { Context } from './context';
export const Yoga = createYoga<RequestEvent>({
context: Context,
schema: Schema,
graphqlEndpoint: '/api/graphql',
// Let Yoga use sveltekit's Response object
fetchAPI: { Response },
logging: yogaLogger
});

View file

@ -0,0 +1,6 @@
<script lang="ts">
import '../app.css';
let { children } = $props();
</script>
{@render children()}

27
src/routes/+page.svelte Normal file
View file

@ -0,0 +1,27 @@
<script lang="ts">
import { goto } from '$app/navigation';
import Loader from '$lib/components/Loader.svelte';
$effect(() => {
const id = setTimeout(() => {
goto('/app');
}, 1500);
return () => {
clearTimeout(id);
};
});
</script>
<div class="site-loader">
<h1>Hestia</h1>
<Loader />
</div>
<style>
.site-loader {
@apply flex h-screen w-screen flex-col items-center justify-center gap-6 bg-slate-100;
}
.site-loader h1 {
@apply font-display text-4xl;
}
</style>

View file

@ -0,0 +1,3 @@
import { Yoga } from '$lib/yoga';
export { Yoga as GET, Yoga as POST };

View file

@ -0,0 +1,8 @@
<script lang="ts">
import Navbar from '$lib/components/Navbar.svelte';
let { children } = $props();
</script>
<Navbar title="Svelte" />
{@render children()}

View file

View file

@ -1,29 +0,0 @@
import type { Configuration } from '@app/config';
import { prisma } from '@app/prisma';
import SchemaBuilder from '@pothos/core';
import PrismaPlugin, {
type PrismaTypesFromClient,
} from '@pothos/plugin-prisma';
import type { YogaInitialContext } from 'graphql-yoga';
type Context = YogaInitialContext & {
config: Configuration;
};
export const builder = new SchemaBuilder<{
Context: Context;
PrismaTypes: PrismaTypesFromClient<typeof prisma>;
}>({
plugins: [PrismaPlugin],
prisma: {
client: prisma,
// defaults to false, uses /// comments from prisma schema as descriptions
// for object types, relations and exposed fields.
// descriptions can be omitted by setting description to false
exposeDescriptions: false,
// use where clause from prismaRelatedConnection for totalCount (defaults to true)
filterConnectionTotalCount: true,
// warn when not using a query parameter correctly
onUnusedQuery: process.env.NODE_ENV === 'production' ? null : 'warn',
},
});

View file

@ -1,10 +0,0 @@
import { LoadConfig } from '@app/config';
import type { YogaInitialContext } from 'graphql-yoga';
export const context = (initialContext: YogaInitialContext) => {
const config = LoadConfig();
return {
...initialContext,
config,
};
};

View file

@ -1,10 +0,0 @@
import { yogaLogger } from "@lib/logger";
import { createYoga } from "graphql-bun ";
import { context } from "./context";
import { schema } from "./schema";
export const yoga = createYoga({
schema,
context: context,
logging: yogaLogger,
});

View file

@ -1,43 +0,0 @@
import { prisma } from '@app/prisma';
import { builder } from './builder';
const User = builder.prismaObject('User', {
fields: (t) => ({
id: t.exposeID('id'),
email: t.exposeString('email'),
name: t.exposeString('name'),
posts: t.relation('posts'),
}),
});
const Post = builder.prismaObject('Post', {
fields: (t) => ({
id: t.exposeID('id'),
title: t.exposeString('title'),
content: t.exposeString('content'),
published: t.exposeBoolean('published'),
author: t.relation('author'),
}),
});
builder.queryType({
fields: (t) => ({
version: t.string({
resolve: (parent, args, context) => context.config.app_version,
}),
users: t.prismaField({
type: [User],
resolve: async () => {
return await prisma.user.findMany();
},
}),
posts: t.prismaField({
type: [Post],
resolve: async () => {
return await prisma.post.findMany();
},
}),
}),
});
export const schema = builder.toSchema();

BIN
static/favicon.png Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

18
svelte.config.js Normal file
View file

@ -0,0 +1,18 @@
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://svelte.dev/docs/kit/integrations
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter()
}
};
export default config;

16
tailwind.config.ts Normal file
View file

@ -0,0 +1,16 @@
import typography from '@tailwindcss/typography';
import type { Config } from 'tailwindcss';
export default {
content: ['./src/**/*.{html,js,svelte,ts}'],
theme: {
extend: {
fontFamily: {
display: ['Baskervville SC']
}
}
},
plugins: [typography]
} satisfies Config;

View file

@ -1,36 +1,19 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
// Enable latest features
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false,
// Path mapping
"baseUrl": ".",
"paths": {
"@app": ["./src"],
"@app/*": ["./src/*"],
"@lib": ["./lib"],
"@lib/*": ["./lib/*"]
}
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

10
vite.config.ts Normal file
View file

@ -0,0 +1,10 @@
import { defineConfig } from 'vitest/config';
import { sveltekit } from '@sveltejs/kit/vite';
export default defineConfig({
plugins: [sveltekit()],
test: {
include: ['src/**/*.{test,spec}.{js,ts}']
}
});