This commit is contained in:
stjet
2024-11-12 01:24:53 +00:00
commit 75883a3e8a
27 changed files with 4321 additions and 0 deletions

21
.gitignore vendored Normal file
View File

@@ -0,0 +1,21 @@
node_modules
# Output
.output
.vercel
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

1
.npmrc Normal file
View File

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

38
README.md Normal file
View File

@@ -0,0 +1,38 @@
# create-svelte
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npx sv create
# create a new project in my-app
npx sv create my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.

3484
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

28
package.json Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "test",
"version": "0.0.1",
"type": "module",
"scripts": {
"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"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/adapter-cloudflare": "^4.7.4",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"typescript": "^5.0.0",
"vite": "^5.0.3"
},
"dependencies": {
"banani": "^1.0.3",
"banani-bns": "^0.0.6",
"mongodb": "^6.10.0",
"qrcode": "^1.5.4"
}
}

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

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

14
src/app.html Normal file
View File

@@ -0,0 +1,14 @@
<!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" />
<script src="/bns-browser.js"></script>
<link rel="stylesheet" href="/global.css">
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

42
src/lib/Declare.svelte Normal file
View File

@@ -0,0 +1,42 @@
<script lang="ts">
import { browser } from "$app/environment";
import { Progress } from "$lib/types";
let { seed, bsf_seed, domain, progress = $bindable() } = $props();
let resolve_to: String = $state("");
async function declare() {
resolve_to = resolve_to.trim();
if (browser && resolve_to.startsWith("ban_") && resolve_to.length === 64) {
const rpc = new window.bns.banani.RPC("https://kaliumapi.appditto.com/api");
const wallet = new window.bns.banani.Wallet(rpc, seed);
await wallet.receive_all();
const domain_manager = new window.bns.DomainAccountManager(rpc, wallet);
await domain_manager.declare_domain_resolve_to(resolve_to);
progress = Progress.Done;
}
}
</script>
<p>One last step! Just enter your main Banano address below, so anyone sending to {domain}.ban can know to send it to you.</p>
<div>
<input bind:value={resolve_to} type="text" placeholder="ban_..."/>
<br><br>
<button class="button" onclick={declare}>Declare Address to Resolve to</button>
</div>
<style>
input {
border: 1px solid gray;
padding: 5px;
min-width: 40%;
border-radius: 10px;
}
div {
margin: 15px 0;
}
</style>

65
src/lib/Payment.svelte Normal file
View File

@@ -0,0 +1,65 @@
<script lang="ts">
import { browser } from "$app/environment";
import { toDataURL } from "qrcode";
import { Progress } from "$lib/types";
import { get_price, seconds_to_time, ALLOWED } from "$lib/utils";
let { domain, payment_address, send_to_pub_key, progress = $bindable() } = $props();
let time_left: number = $state(5 * 60);
let payment_qr_promise: Promise<String> = $derived(toDataURL(`ban:${payment_address}?amount=${String(window.bns.banani.whole_to_raw(String(price)))}`));
let error: String = $state(undefined);
let price = $state(undefined);
if (domain.length > 3 && domain.split("").every((c) => ALLOWED.includes(c)) && domain !== undefined) {
price = get_price(domain.length);
}
setInterval(() => time_left -= 1, 1000);
async function check_for_payment() {
let { send_hash, message } = await (await fetch("/api/check_payment", {
method: "POST",
body: JSON.stringify({
domain,
send_to_pub_key,
}),
})).json();
if (!send_hash) {
error = "Have you sent the payment?";
} else {
console.log(send_hash);
progress = Progress.Declare;
}
}
</script>
{#if error}
<span class="error">{error}</span>
{/if}
{#if isNaN(price)}
<p>Invalid domain name (too short / disallowed characters)</p>
{:else}
<p>Send {price} BAN to <code>{payment_address}</code></p>
<p><a href="https://thebananostand.com?request=send&address={payment_address}&amount={price}" target="_blank">Open in Bananostand</a></p>
<p><a href="ban:{payment_address}?amount={String(window.bns.banani.whole_to_raw(String(price)))}" target="_blank">Open in Kalium</a></p>
{#await payment_qr_promise}
<p>Loading QR...</p>
{:then payment_qr}
<img alt="Payment QR code" src="{payment_qr}">
{/await}
<br>
<p>{seconds_to_time(time_left)}</p>
<br>
<button onclick={check_for_payment} class="button">I've sent the payment</button>
{/if}
<style>
p {
margin-bottom: 15px;
}
</style>

21
src/lib/Seed.svelte Normal file
View File

@@ -0,0 +1,21 @@
<script lang="ts">
import { Progress } from "$lib/types";
let { bsf_seed, progress = $bindable() } = $props();
function continue_to_payment() {
progress = Progress.Payment;
}
</script>
<p>Save the seed and continue.</p>
<p>Seed (in BNS Seed Format): <code>{bsf_seed}</code></p>
<button class="button" onclick={continue_to_payment}>Continue</button>
<style>
p {
margin-bottom: 15px;
}
</style>

65
src/lib/db.ts Normal file
View File

@@ -0,0 +1,65 @@
import type { MongoClient } from "mongodb";
import type { Path } from "$lib/types";
import { get_price } from "$lib/utils";
export async function is_domain_already_issued(db: MongoClient, domain: string): Promise<boolean> {
const issued = db.db("bns_backend").collection("issued");
if (await issued.findOne({ domain })) {
return true;
} else {
return false;
}
}
export async function add_domain_to_issued(db: MongoClient, domain: string, issued_hash: string, price: number) {
const issued = db.db("bns_backend").collection("issued");
await issued.insertOne({
domain,
issued_hash,
price, //cause price may change in future, record price at time of sale
});
}
//in the last 5 minutes
export async function payment_already_pending(db: MongoClient, domain: string): Promise<boolean> {
const payments = db.db("bns_backend").collection("payments");
if (await payments.findOne({
domain,
timestamp: {
$gt: Date.now() - 5 * 60 * 1000,
},
})) {
return true;
} else {
return false;
}
}
//todo: return an actual type
export async function find_payment(db: MongoClient, domain: string, send_to: string): Promise<any> {
const payments = db.db("bns_backend").collection("payments");
return await payments.findOne({
domain,
send_to,
timestamp: {
$gt: Date.now() - 5 * 60 * 1000,
},
});
}
//todo: technically possible for there to be race condition with payment_already_pending
export async function create_payment(db: MongoClient, domain: string, send_to: string, receive_seed: string) {
const price = get_price(domain.length);
const payments = db.db("bns_backend").collection("payments");
await payments.insertOne({
domain,
receive_seed, //seed to receive payment from
send_to, //Domain Address (banano address) to send domain to after payment received
price,
timestamp: Date.now(),
});
}
//

1
src/lib/index.ts Normal file
View File

@@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

10
src/lib/mongo.ts Normal file
View File

@@ -0,0 +1,10 @@
import { MongoClient } from "mongodb";
import { MONGODB_URI } from "$env/static/private";
//const MONGODB_URI = process.env["MONGODB_URI"];
const client = new MongoClient(MONGODB_URI);
export const client_promise = client.connect();

8
src/lib/types.ts Normal file
View File

@@ -0,0 +1,8 @@
export enum Progress {
Seed = "seed",
Payment = "payment",
Declare = "declare",
Done = "done",
Failed = "failed",
}

37
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,37 @@
export const ALLOWED = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "_", "backspace", "arrowleft", "arrowtop", "arrowbottom", "arrowright"];
export function get_price(dl: number): number {
if (dl === 4) {
return 4200;
} else if (dl === 5) {
return 1900;
} else if (dl === 6) {
return 1200;
} else if (dl === 7) {
return 900;
} else if (dl === 8) {
return 690;
} else if (dl === 9) {
return 420;
} else if (dl > 9) {
return 1;//190;
} else {
return 99999; //currently <4 length domains not buyable, but just in case...
}
}
export function is_domain_name_allowed(domain: string): boolean {
return domain.split("").every((c) => ALLOWED.includes(c));
}
const HEX_CHARS = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"];
export function is_valid_public_key(public_key: string): boolean {
return public_key.length === 64 && public_key.split("").every((c) => HEX_CHARS.includes(c));
}
export function seconds_to_time(seconds: number): string {
if (seconds < 0) return "0:00";
return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, "0")}`;
}

221
src/routes/+page.svelte Normal file
View File

@@ -0,0 +1,221 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { get_price, ALLOWED } from "$lib/utils";
let domain_content: String = $state("");
let error: String = $state("");
let price: String = $state("");
function domain_keydown(event: KeyboardEvent) {
let key = event.key.toLowerCase();
if (!ALLOWED.includes(key)) {
event.preventDefault();
} else {
price = "";
if (domain_content.length > 3) {
price = `Price: ${get_price(domain_content.length)} BAN`;
}
}
}
function domain_keyup() {
domain_content = domain_content.toLowerCase();
}
async function domain_next() {
domain_content = domain_content.toLowerCase();
if (domain_content.length < 4) {
error = "Domain name must be more than 3 characters";
} else {
const resp = await (await fetch("/api/domain_issued?domain=" + domain_content)).json();
if (resp.issued) {
error = "Domain name already issued, choose another one";
} else {
error = "";
goto("/register?domain=" + domain_content);
}
}
}
</script>
<svelte:head>
<title>Get your .ban</title>
</svelte:head>
<div>
<div id="front">
<div id="pitch" class="half">
<h1>.ban: It's Healthy</h1>
<div id="nutrition">
<h2><b>Nutrition Facts</b></h2>
<hr>
<span>1 serving per domain</span>
<br>
<span><b>Serving size</b></span> <span class="right"><b>starting at 190 $BAN (or 1 kg of bananas)</b></span>
<hr class="thickest">
<span><b>Amount per serving</b></span>
<br>
<span class="calories"><b>Calories</b></span> <span class="right calories"><b>0</b></span>
<hr class="thick">
<span></span> <span class="right"><b><small>% Daily Value</small></b></span>
<hr>
<span><b>Decentralisation</b> 25g</span> <span class="right"><b>100%</b></span>
<hr>
<span class="stagger">On-chain?</span> <span class="right"><b>Yep</b></span>
<hr>
<span class="stagger">Censorable?</span> <span class="right"><b>Nope</b></span>
<hr>
<span class="stagger">Revocable?</span> <span class="right"><b>Hell no</b></span>
<hr>
<span><b>Utility</b> 17g</span> <span class="right"><b>100%</b></span>
<hr>
<span class="stagger">Fast and Feeless?</span> <span class="right"><b>Duh</b></span>
<hr>
<span class="stagger">Wait, no renewal fees?</span> <span class="right"><b>Yesss</b></span>
<hr>
<span class="stagger">Mine, forever?</span> <span class="right"><b>Bingo</b></span>
<hr>
<span class="stagger">Transferable?</span> <span class="right"><b>Si</b></span>
<hr>
<span class="stagger">Resolves to Banano address?</span> <span class="right"><b>Correct</b></span>
<hr>
<span class="stagger">Can be associated with arbitrary metadata?</span> <span class="right"><b>Yeah...</b></span>
<hr class="thickest">
<span>Potassium 190g 55882%</span> - <span class="right">Thorium 0g 0%</span>
<hr>
<span>Unicorn Horn powder 10mg 41%</span> - <span class="right">Typescript 1kg 1%</span>
<hr class="thick">
</div>
<p id="bottom-1">Have <code>yourname.ban</code> resolve to <code>ban_1area11yrea11yrea11y1ongdifficu1ttorememberaddress11hcd8a7c9</code>! Other usecases like decentralised websites coming soon™</p>
</div>
<div id="get" class="half">
<div id="get-child">
<div id="input-container">
<input placeholder="Get your .ban on" maxlength="48" onkeydown={domain_keydown} onkeyup={domain_keyup} bind:value={domain_content} type="text"/><span>.ban</span><input onclick={domain_next} type="button" value="-->"/>
</div>
<span id="price">{price}</span>
<span class="error">{error}</span>
<div>
<h2>Supported by:</h2>
<div>
<span>Bananostand</span>
</div>
</div>
<p id="bottom-2">.ban is the first publicly available top level domain (TLD) for the <a href="https://github.com/stjet/bns/blob/master/bns_protocol.md">Banano Name Service protocol (BNS)</a></p>
</div>
</div>
</div>
</div>
<style>
#front {
display: grid;
grid-template-columns: 50vw 50vw;
height: 100vh;
}
#pitch {
background-color: var(--green);
}
.half {
padding: 2em;
}
.right {
float: right;
}
.stagger {
margin-left: 2em;
}
h1 {
font-size: xxx-large;
}
h2 {
font-size: xx-large;
}
.calories {
font-size: x-large;
}
#bottom-1 {
word-break: break-all;
}
hr {
margin: 1px 0;
}
.thickest {
border-width: 7px;
}
.thick {
border-width: 4px;
}
#get {
background-color: var(--yellow);
}
#get-child {
width: 100%;
height: 100%;
position: relative;
}
#bottom-2 {
position: absolute;
bottom: 0;
}
#input-container {
width: 100%;
height: 30px;
border: 1px solid gray;
border-radius: 10px;
margin: auto;
background-color: white;
}
#input-container input {
border: none;
margin: 0;
padding: 0;
height: 100%;
}
#input-container input[type="text"] {
padding-left: 3px;
width: calc(65% - 3px);
border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
}
#input-container span {
display: inline-block;
width: 10%;
text-align: center;
}
#input-container input[type="button"] {
width: 25%;
border-top-right-radius: 10px;
border-bottom-right-radius: 10px;
cursor: pointer;
color: white;
background-color: var(--grey2);
}
#logo {
height: 16px;
width: 16px;
margin-top: 2px;
}
</style>

View File

@@ -0,0 +1,45 @@
import { error, json } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
import { Wallet, RPC, get_address_from_public_key, raw_to_whole } from "banani";
import { TLDAccountManager } from "banani-bns";
import { client_promise } from "$lib/mongo";
import { add_domain_to_issued, is_domain_already_issued, find_payment } from "$lib/db";
import { is_valid_public_key } from "$lib/utils";
import { TLD_SEED, STORAGE_ADDRESS } from "$env/static/private";
const rpc = new RPC("https://kaliumapi.appditto.com/api");
const sleep = ms => new Promise(r => setTimeout(r, ms));
export const POST: RequestHandler = async ({ request }) => {
const { domain, send_to_pub_key } = await request.json();
if (!domain || !send_to_pub_key) {
return error(400, "Missing one or more of the required fields `domain` and `send_to_pub_key`");
} else if (!is_valid_public_key(send_to_pub_key)) {
return error(400, "`send_to_pub_key` is invalid");
}
const db = await client_promise;
if (await is_domain_already_issued(db, domain)) {
return error(500, "Domain already issued");
}
const found = await find_payment(db, domain, get_address_from_public_key(send_to_pub_key));
if (!found) {
return error(500, "Payment request expired or never made");
}
const receive_wallet = new Wallet(rpc, found.receive_seed);
await receive_wallet.receive_all();
await sleep(1500);
const balance = Number(raw_to_whole((await rpc.get_account_balance(receive_wallet.address)).balance));
if (balance < found.price) {
return error(500, `Need to be sent ${found.price}, only got ${balance}`);
}
receive_wallet.send_all(STORAGE_ADDRESS);
const tld_manager = new TLDAccountManager(rpc, new Wallet(rpc, TLD_SEED));
const send_hash = await tld_manager.issue_domain_name(domain, found.send_to);
await add_domain_to_issued(db, domain, send_hash, found.price);
return json({
send_hash,
});
};

View File

@@ -0,0 +1,17 @@
import { error, json } from '@sveltejs/kit';
import type { RequestHandler } from "./$types";
import { client_promise } from "$lib/mongo";
import { is_domain_already_issued } from "$lib/db";
export const GET: RequestHandler = async ({ url }) => {
const domain = url.searchParams.get("domain");
if (!domain) {
return error(400, "Missing URL query param `domain`");
}
let db = await client_promise;
return json({
issued: await is_domain_already_issued(db, domain),
});
}

View File

@@ -0,0 +1,31 @@
import { error, json } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
import { Wallet, get_address_from_public_key } from "banani";
import { client_promise } from "$lib/mongo";
import { is_domain_already_issued, payment_already_pending, create_payment } from "$lib/db";
import { is_domain_name_allowed, is_valid_public_key } from "$lib/utils";
export const POST: RequestHandler = async ({ request }) => {
const { domain, send_to_pub_key } = await request.json();
if (!domain || !send_to_pub_key) {
return error(400, "Missing one or more of the required fields `domain` and `send_to_pub_key`");
} else if (!is_domain_name_allowed(domain) || domain.length < 4) {
return error(400, "Domain name has disallowed characters or is shorter than 4 characters");
} else if (!is_valid_public_key(send_to_pub_key)) {
return error(400, "`send_to_pub_key` is invalid");
}
const db = await client_promise;
if (await is_domain_already_issued(db, domain)) {
return error(500, "Domain already issued");
} else if (await payment_already_pending(db, domain)) {
return error(500, "Payment for domain already pending, wait 5 minutes or so");
}
const payment_wallet = Wallet.gen_random_wallet();
await create_payment(db, domain, get_address_from_public_key(send_to_pub_key), payment_wallet.seed);
return json({
payment_address: payment_wallet.address,
});
};

View File

@@ -0,0 +1,64 @@
<script lang="ts">
import { page } from "$app/stores";
import { browser } from "$app/environment";
import { Progress } from "$lib/types";
import Seed from "$lib/Seed.svelte";
import Payment from "$lib/Payment.svelte";
import Declare from "$lib/Declare.svelte";
let progress: Progress = $state(Progress.Seed);
let message: String = $state(undefined); //error
let payment_address: String = $state(undefined);
let domain = $page.url.searchParams.get("domain");
let wallet, send_to_pub_key, bsf_seed;
if (browser) {
wallet = window.bns.banani.Wallet.gen_random_wallet();
send_to_pub_key = wallet.public_key;
bsf_seed = window.bns.hex_to_bns_seed_format(wallet.seed);
}
$effect(async () => {
if (progress === Progress.Payment) {
({ payment_address, message } = await (await fetch("/api/start_payment", {
method: "POST",
body: JSON.stringify({
domain,
send_to_pub_key,
}),
})).json());
if (!payment_address) {
progress = Progress.Failed;
}
}
});
</script>
<div id="main">
<div class="middle">
{#if progress === Progress.Seed}
<Seed bind:progress {bsf_seed}/>
{:else if progress === Progress.Payment}
<Payment bind:progress {domain} {payment_address} {send_to_pub_key}/>
{:else if progress === Progress.Declare}
<Declare bind:progress {domain} seed={wallet.seed} {bsf_seed}/>
{:else if progress === Progress.Done}
<p>You are all set! Try your new domain out by sending yourself a Banano or two in Bananostand at {domain}.ban!</p>
<p>You can make further changes to your BNS domain by entering the seed and clicking the "Create Domain Account" in the <a href="https://bns.prussia.dev/browser_test">BNS Web Wallet</a>.</p>
<p>As a reminder, the seed you need to save is <code>{bsf_seed}</code></p>
{:else if progress === Progress.Failed}
<span class="error">{message}</span>
{/if}
</div>
</div>
<style>
#main {
height: 100vh;
box-sizing: border-box;
padding: 10px;
background-color: var(--green);
}
</style>

11
static/bns-browser.js Normal file

File diff suppressed because one or more lines are too long

BIN
static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
static/fonts/Arimo.ttf Normal file

Binary file not shown.

40
static/global.css Normal file
View File

@@ -0,0 +1,40 @@
@font-face {
font-family: Arimo;
src: local("Arimo"), url("/fonts/Arimo.ttf");
}
:root {
--yellow: #FBDD11;
--green: #4CBF4B;
--grey1: #2A2A2E;
--grey2: #212124;
}
*:not(code) {
font-family: Arimo;
margin: 0;
color: var(--grey1);
}
.error {
color: red;
background-color: var(--yellow);
}
.middle {
text-align: center;
}
.button {
background-color: var(--yellow);
border: 1px solid black;
border-radius: 15px;
padding: 15px;
font-weight: bold;
}
.button:hover {
cursor: pointer;
background-color: #eacf1e;
}

19
svelte.config.js Normal file
View File

@@ -0,0 +1,19 @@
/*
import adapter from '@sveltejs/adapter-auto';
*/
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
import adapter from '@sveltejs/adapter-cloudflare';
export default {
// Consult https://svelte.dev/docs/kit/integrations#preprocessors
// 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(),
}
};

19
tsconfig.json Normal file
View File

@@ -0,0 +1,19 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"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
}

6
vite.config.ts Normal file
View File

@@ -0,0 +1,6 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()]
});