web_lib

Common web application libraries
git clone https://radroots.dev/git/web_lib.git
Log | Files | Refs | LICENSE

commit 490c5e4eb628ca6b0b08bfeed314022a45464320
parent 67f445fc60cf19ece5e0569cde2dcddf27f51adf
Author: triesap <triesap@radroots.dev>
Date:   Sun, 21 Dec 2025 01:34:56 +0000

apps-lib: refactored store and environment utilities, added blob url file path support, and hardened indexeddb key-value wrapper typing

Diffstat:
Mapps-lib/src/lib/stores/carousel.ts | 16++++++++--------
Mapps-lib/src/lib/stores/ndk.ts | 14+++++++-------
Mapps-lib/src/lib/types/lib.ts | 2+-
Mapps-lib/src/lib/utils/_env.ts | 2+-
Mapps-lib/src/lib/utils/app/carousel.ts | 26+++++++++++++-------------
Mapps-lib/src/lib/utils/app/lib.ts | 5+++--
Mapps-lib/src/lib/utils/i18n.ts | 12++++++------
Mapps-lib/src/lib/utils/keyval/idb.ts | 210++++++++++++++++++++++++-------------------------------------------------------
Mapps-lib/src/lib/utils/keyval/lib.ts | 4++--
Mapps-lib/src/lib/utils/nostr/lib.ts | 20++++++++++----------
Mapps-lib/svelte.config.js | 1-
11 files changed, 113 insertions(+), 199 deletions(-)

diff --git a/apps-lib/src/lib/stores/carousel.ts b/apps-lib/src/lib/stores/carousel.ts @@ -20,17 +20,17 @@ const create_carousel_num = (num_i: number, num_min: number) => { export const casl_num = create_carousel_num(1, 1); export const casl_inc = async (opts?: 'noflow'): Promise<void> => { - const $casl_i = get_store(casl_i); - const $casl_imax = get_store(casl_imax); - if (opts === 'noflow' && $casl_i < $casl_imax) casl_i.set($casl_i + 1); - else casl_i.set(($casl_i + 1) % ($casl_imax + 1)); + const casl_i_val = get_store(casl_i); + const casl_imax_val = get_store(casl_imax); + if (opts === 'noflow' && casl_i_val < casl_imax_val) casl_i.set(casl_i_val + 1); + else casl_i.set((casl_i_val + 1) % (casl_imax_val + 1)); }; export const casl_dec = async (opts?: 'noflow'): Promise<void> => { - const $casl_i = get_store(casl_i); - const $casl_imax = get_store(casl_imax); - if (opts === 'noflow' && $casl_i > 0) casl_i.set($casl_i - 1); - else casl_i.set(($casl_i - 1 + ($casl_imax + 1)) % ($casl_imax + 1)); + const casl_i_val = get_store(casl_i); + const casl_imax_val = get_store(casl_imax); + if (opts === 'noflow' && casl_i_val > 0) casl_i.set(casl_i_val - 1); + else casl_i.set((casl_i_val - 1 + (casl_imax_val + 1)) % (casl_imax_val + 1)); }; export const casl_init = (index_curr: number, index_max: number): void => { diff --git a/apps-lib/src/lib/stores/ndk.ts b/apps-lib/src/lib/stores/ndk.ts @@ -1,18 +1,18 @@ -import { _envLib } from "$lib/utils/_env"; +import { _env_lib } from "$lib/utils/_env"; import { type NDKCacheAdapter, type NDKUser } from "@nostr-dev-kit/ndk"; import NDKCacheAdapterDexie from "@nostr-dev-kit/ndk-cache-dexie"; import NDKSvelte from "@nostr-dev-kit/ndk-svelte"; import { writable } from "svelte/store"; let cache_adapter: NDKCacheAdapter | undefined; -if (typeof window !== `undefined`) cache_adapter = new NDKCacheAdapterDexie({ dbName: _envLib.NDK_CACHE }); +if (typeof window !== `undefined`) cache_adapter = new NDKCacheAdapterDexie({ dbName: _env_lib.NDK_CACHE }); let cache_adapter_global: NDKCacheAdapter | undefined; -if (typeof window !== `undefined`) cache_adapter_global = new NDKCacheAdapterDexie({ dbName: `${_envLib.NDK_CACHE}-global` }); +if (typeof window !== `undefined`) cache_adapter_global = new NDKCacheAdapterDexie({ dbName: `${_env_lib.NDK_CACHE}-global` }); -const _ndk = new NDKSvelte({ cacheAdapter: cache_adapter, clientName: _envLib.NDK_CLIENT, explicitRelayUrls: [_envLib.RADROOTS_RELAY], autoConnectUserRelays: true, autoFetchUserMutelist: true }); -export const ndk = writable<NDKSvelte>(_ndk); +const ndk_i = new NDKSvelte({ cacheAdapter: cache_adapter, clientName: _env_lib.NDK_CLIENT, explicitRelayUrls: [_env_lib.RADROOTS_RELAY], autoConnectUserRelays: true, autoFetchUserMutelist: true }); +export const ndk = writable<NDKSvelte>(ndk_i); export const ndk_user = writable<NDKUser>(); -const _ndk_global = new NDKSvelte({ cacheAdapter: cache_adapter_global, clientName: _envLib.NDK_CLIENT, autoConnectUserRelays: true, autoFetchUserMutelist: true }); -export const ndk_global = writable<NDKSvelte>(_ndk_global); +const ndk_global_i = new NDKSvelte({ cacheAdapter: cache_adapter_global, clientName: _env_lib.NDK_CLIENT, autoConnectUserRelays: true, autoFetchUserMutelist: true }); +export const ndk_global = writable<NDKSvelte>(ndk_global_i); diff --git a/apps-lib/src/lib/types/lib.ts b/apps-lib/src/lib/types/lib.ts @@ -166,7 +166,7 @@ export type GeometryGlyphDimension = export type LayerGlyphBasisKind = `_a` | `_d` | `_pl`; export type LoadingBlades = 8 | 12; -export type LoadingDimension = GeometryDimension | `glyph-send-button`; //@todo remove +export type LoadingDimension = GeometryDimension | `glyph-send-button`; // @todo remove export type ElementCallbackValue = CallbackPromiseGeneric<{ value: string; pass: boolean; }>; export type ElementCallbackValueKeydown<T extends HTMLElement> = CallbackPromiseGeneric<{ key: string; key_s: boolean; el: T }>; diff --git a/apps-lib/src/lib/utils/_env.ts b/apps-lib/src/lib/utils/_env.ts @@ -12,7 +12,7 @@ if (!RADROOTS_RELAY || typeof RADROOTS_RELAY !== 'string') throw new Error('Miss const PROD = import.meta.env.MODE === 'production'; -export const _envLib = { +export const _env_lib = { PROD, KEYVAL_NAME, NDK_CACHE, diff --git a/apps-lib/src/lib/utils/app/carousel.ts b/apps-lib/src/lib/utils/app/carousel.ts @@ -22,16 +22,16 @@ const get_slide_item = <T extends string>(view: T): Element | undefined => { const carousel_dec_handler = async <T extends string>( view: T, ): Promise<void> => { - const $casl_active = get_store(casl_active); - if ($casl_active) return; + const casl_active_val = get_store(casl_active); + if (casl_active_val) return; casl_active.set(true); const slide_item = get_slide_item<T>(view); const slide_container = get_slide_container<T>(view); if (slide_container && slide_item) { const slide_w = slide_item?.clientWidth || 0; slide_container.scrollLeft -= slide_w; - const $casl_i = get_store(casl_i); - casl_i.set(Math.max($casl_i - 1, 0)); + const casl_i_val = get_store(casl_i); + casl_i.set(Math.max(casl_i_val - 1, 0)); } casl_active.set(false); }; @@ -39,18 +39,18 @@ const carousel_dec_handler = async <T extends string>( const carousel_inc_handler = async <T extends string>( view: T, ): Promise<void> => { - const $casl_active = get_store(casl_active); - if ($casl_active) return; + const casl_active_val = get_store(casl_active); + if (casl_active_val) return; casl_active.set(true); const slide_item = get_slide_item<T>(view); const slide_container = get_slide_container<T>(view); if (slide_container && slide_item) { const slide_w = slide_item?.clientWidth || 0; slide_container.scrollLeft += slide_w; - const $casl_i = get_store(casl_i); - const $casl_imax = get_store(casl_imax); + const casl_i_val = get_store(casl_i); + const casl_i_valmax = get_store(casl_imax); casl_i.set( - Math.min($casl_i + 1, $casl_imax), + Math.min(casl_i_val + 1, casl_i_valmax), ); } casl_active.set(false); @@ -60,9 +60,9 @@ export const carousel_inc = async <T extends string>( view: T, duration: number = CAROUSEL_DELAY_MS ): Promise<void> => { - const $casl_num = get_store(casl_num); + const casl_num_val = get_store(casl_num); casl_num.set(1); - await exe_iter(async () => carousel_inc_handler(view), $casl_num, duration); + await exe_iter(async () => carousel_inc_handler(view), casl_num_val, duration); }; @@ -70,9 +70,9 @@ export const carousel_dec = async <T extends string>( view: T, duration: number = CAROUSEL_DELAY_MS ): Promise<void> => { - const $casl_num = get_store(casl_num); + const casl_num_val = get_store(casl_num); casl_num.set(1); - await exe_iter(async () => carousel_dec_handler(view), $casl_num, duration); + await exe_iter(async () => carousel_dec_handler(view), casl_num_val, duration); }; export const carousel_init = async <T extends string>(view: T, num_max: number): Promise<void> => { diff --git a/apps-lib/src/lib/utils/app/lib.ts b/apps-lib/src/lib/utils/app/lib.ts @@ -3,7 +3,7 @@ import { goto } from '$app/navigation'; import { win_h, win_w } from '$lib/stores/app'; import type { CallbackRoute, NavigationParamTuple, NavigationRouteParamKey, NavigationRouteParamTuple, } from '$lib/types/lib'; import type { ThemeLayer, ThemeMode } from '@radroots/themes'; -import type { FilePath } from '@radroots/utils'; +import type { WebFilePath } from '@radroots/utils'; import { getContext, setContext } from "svelte"; import { get } from "svelte/store"; @@ -177,7 +177,8 @@ export const to_arr_buf = (u8: Uint8Array): ArrayBuffer => { return u8.slice().buffer; }; -export const parse_file_path = (file_path: string): FilePath | undefined => { +export const parse_file_path = (file_path: string): WebFilePath | undefined => { + if (file_path.startsWith("blob:")) return { blob_path: file_path, blob_name: file_path.replaceAll("blob:", "").replaceAll("http://", "") }; const file_path_spl = file_path.split(`/`); const file_path_file = file_path_spl[file_path_spl.length - 1] || ``; const [file_name, mime_type] = file_path_file.split(`.`); diff --git a/apps-lib/src/lib/utils/i18n.ts b/apps-lib/src/lib/utils/i18n.ts @@ -24,10 +24,10 @@ export const i18n_conf = <T extends string>(opts: { translations: Record<T, any>; loaders: Loader.LoaderModule[] }) => { - const { default_locale: initLocale, translations, loaders } = opts; + const { default_locale, translations, loaders } = opts; const config: Config<any> = { - initLocale, - fallbackLocale: initLocale, + initLocale: default_locale, + fallbackLocale: default_locale, translations, loaders, }; @@ -39,10 +39,10 @@ export const i18n_conf_icu = <T extends string>(opts: { translations: Record<T, any>; loaders: Loader.LoaderModule[] }): i18n<ParserIcu.Params<LanguageConfig>> => { - const { default_locale: initLocale, translations, loaders } = opts; + const { default_locale, translations, loaders } = opts; const config: ConfigIcu<LanguageConfig> = { - initLocale, - fallbackLocale: initLocale, + initLocale: default_locale, + fallbackLocale: default_locale, translations, parser: parser_icu(), loaders, diff --git a/apps-lib/src/lib/utils/keyval/idb.ts b/apps-lib/src/lib/utils/keyval/idb.ts @@ -1,51 +1,27 @@ - export class IdbKeyval { - /** - * An IDBKeyRange that has no upper or lower bounding. - */ static readonly unbound = IDBKeyRange.lowerBound(Number.MIN_SAFE_INTEGER); - /** - * Returns an IDBKeyRange that matches all keys that start - * with the specified string prefix. - */ static prefix(prefix: string) { return IDBKeyRange.bound(prefix, prefix + "\uFFFF"); } - /** - * @returns An array of strings that contain the names of all - * IdbKeyval-created IndexedDB databases. - */ static async each() { const databases = await indexedDB.databases(); return databases - .map(db => db.name) - .filter((s): s is string => !!s && s.startsWith(this.kv_prefix)); + .map((db) => db.name) + .filter((name): name is string => !!name && name.startsWith(this.kv_prefix)); } - /** - * Deletes IdbKeyval-created IndexedDB databases with the - * specified names. - * - * @param names The names of the databases to delete. - * If no names are provided, all IdbKeyval IndexedDB databases - * are deleted. - */ static async delete(...names: string[]) { - names = names.length ? - names.map(n => n.startsWith(this.kv_prefix) ? n : this.kv_prefix + n) : - await this.each(); + const resolved_names = names.length + ? names.map((name) => (name.startsWith(this.kv_prefix) ? name : this.kv_prefix + name)) + : await this.each(); - Promise.all(names.map(n => this.as_promise(indexedDB.deleteDatabase(n)))); + Promise.all(resolved_names.map((name) => this.as_promise(indexedDB.deleteDatabase(name)))); } - /** Stores the prefix that is added to every IndexedDB database created by IdbKeyval. */ private static readonly kv_prefix = "radroots-web-keyval"; - /** - * Creates a new IndexedDB-backed database - */ constructor(options: IdbKeyval.IConstructorOptions = {}) { const idx = options.indexes || []; this.indexes = (Array.isArray(idx) ? idx : [idx]).sort(); @@ -55,95 +31,67 @@ export class IdbKeyval { private readonly indexes: string[]; private readonly name: string; - /** - * Get a value by its key. - * @param key The key of the value to get. - */ get<T = any>(key: IdbKeyval.Key): Promise<T>; - /** - * Get a series of values from the keys specified. - * @param keys The key of the value to get. - */ get<T = any>(keys: IdbKeyval.Key[]): Promise<T[]>; - /** */ async get(k: IdbKeyval.Key | IdbKeyval.Key[]) { const store = await this.get_store("readonly"); - return Array.isArray(k) ? - Promise.all(k.map(key => IdbKeyval.as_promise(store.get(key)))) : - IdbKeyval.as_promise(store.get(k)); + return Array.isArray(k) + ? Promise.all(k.map((key) => IdbKeyval.as_promise(store.get(key)))) + : IdbKeyval.as_promise(store.get(k)); } - /** - * Gets all keys and values from the IdbKeyval database. - * @param key The key of the value to get. - */ each<T = any>(): Promise<[IdbKeyval.Key, T][]>; - /** - * Gets a series of keys and values that match the specified - * set of options. - */ each<T = any>(options: IdbKeyval.IQuery): Promise<[IdbKeyval.Key, T][]>; - /** - * Gets a series of keys only that match the specified set of options. - */ each(options: IdbKeyval.IQuery, only: "keys"): Promise<IdbKeyval.Key[]>; - /** - * Gets a series of values only that match the specified set of options. - */ each<T = any>(options: IdbKeyval.IQuery, only: "values"): Promise<T[]>; - /** */ async each(options: IdbKeyval.IQuery = {}, only?: "keys" | "values"): Promise<any> { const store = await this.get_store("readonly"); const target = options.index ? store.index(options.index) : store; const limit = options.limit; const range = options.range; - if (only === "keys") + if (only === "keys") { return IdbKeyval.as_promise(target.getAllKeys(range, limit)); + } - if (only === "values") + if (only === "values") { return IdbKeyval.as_promise(target.getAll(range, limit)); + } - let keys: IdbKeyval.Key[] = []; - let values: any[] = []; + const keys: IdbKeyval.Key[] = []; + const values: any[] = []; await Promise.allSettled([ - new Promise<void>(async r => { + new Promise<void>(async (resolve) => { const results = await IdbKeyval.as_promise(target.getAllKeys(range, limit)); - keys.push(...results as IdbKeyval.Key[]); - r(); + keys.push(...(results as IdbKeyval.Key[])); + resolve(); }), - new Promise<void>(async r => { + new Promise<void>(async (resolve) => { const results = await IdbKeyval.as_promise(target.getAll(range, limit)); values.push(...results); - r(); + resolve(); }), ]); const tuples: [IdbKeyval.Key, any][] = []; - for (let i = -1; ++i < keys.length;) + for (let i = -1; ++i < keys.length;) { tuples.push([keys[i], values[i]]); + } return tuples; } - /** - * Set a value with a key. - */ async set(key: IdbKeyval.Key, value: any): Promise<void>; - /** - * Set multiple values at once. This is faster than calling set() multiple times. - * It's also atomic – if one of the pairs can't be added, none will be added. - * @param entries Array of entries, where each entry is an array of `[key, value]`. - */ async set(entries: [IdbKeyval.Key, any][]): Promise<void>; async set(a: any, b?: any) { const store = await this.get_store("readwrite"); if (Array.isArray(a)) { - for (const entry of (a as [IdbKeyval.Key, any][])) + for (const entry of a as [IdbKeyval.Key, any][]) { store.put(entry[1], entry[0]); + } return IdbKeyval.as_promise(store.transaction); } @@ -152,82 +100,73 @@ export class IdbKeyval { return IdbKeyval.as_promise(store.transaction); } - /** - * Deletes all objects from this IdbKeyval database - * (but keeps the IdbKeyval database itself is kept). - */ async delete(): Promise<void>; - /** - * Delete a single object from the store with the specified key. - */ async delete(range: IDBKeyRange): Promise<void>; - /** - * Delete a single object from the store with the specified key. - */ async delete(key: IdbKeyval.Key): Promise<void>; - /** - * Delete a series of objects from the store at once, with the specified keys. - */ async delete(keys: IdbKeyval.Key[]): Promise<void>; async delete(arg?: IdbKeyval.Key | IdbKeyval.Key[] | IDBKeyRange) { const store = await this.get_store("readwrite"); - arg ??= IdbKeyval.unbound; + const delete_arg = arg ?? IdbKeyval.unbound; - if (Array.isArray(arg)) { - for (const key of arg) + if (Array.isArray(delete_arg)) { + for (const key of delete_arg) { store.delete(key); + } + } else { + store.delete(delete_arg); } - else store.delete(arg); return IdbKeyval.as_promise(store.transaction); } - /** */ private async get_store(mode: IDBTransactionMode) { const db = await this.get_database(); return db.transaction(this.name, mode).objectStore(this.name); } - /** */ private async get_database() { if (!this.database) { await this.maybe_fix_safari(); let quit = false; let version: number | undefined; - let indexNamesAdded: string[] = []; - let indexNamesRemoved: string[] = []; + let index_names_added: string[] = []; + let index_names_removed: string[] = []; - for (; ;) { + for (;;) { const request = indexedDB.open(this.name, version); request.onupgradeneeded = () => { const db = request.result; - const tx = request.transaction!; + const tx = request.transaction as IDBTransaction; - const store = tx.objectStoreNames.contains(this.name) ? - tx.objectStore(this.name) : - db.createObjectStore(this.name); + const store = tx.objectStoreNames.contains(this.name) + ? tx.objectStore(this.name) + : db.createObjectStore(this.name); - for (const index of indexNamesAdded) + for (const index of index_names_added) { store.createIndex(index, index); + } - for (const index of indexNamesRemoved) + for (const index of index_names_removed) { store.deleteIndex(index); + } }; this.database = await IdbKeyval.as_promise(request); - if (quit) + if (quit) { break; + } const tx = this.database.transaction(this.name, "readonly"); const store = tx.objectStore(this.name); - const indexNames = Array.from(store.indexNames).sort(); + const index_names = Array.from(store.indexNames).sort(); tx.abort(); - indexNamesAdded = this.indexes.filter(n => !indexNames.includes(n)); - indexNamesRemoved = indexNames.filter(n => !this.indexes.includes(n)); + index_names_added = this.indexes.filter((name) => !index_names.includes(name)); + index_names_removed = index_names.filter((name) => !this.indexes.includes(name)); - if (indexNamesAdded.length + indexNamesRemoved.length === 0) + if (index_names_added.length + index_names_removed.length === 0) { break; + } quit = true; this.database.close(); @@ -237,71 +176,46 @@ export class IdbKeyval { return this.database; } + private database: IDBDatabase | null = null; - /** - * Works around a Safari 14 bug. - * - * Safari has a bug where IDB requests can hang while the browser is - * starting up. https://bugs.webkit.org/show_bug.cgi?id=226547 - * The only solution is to keep nudging it until it's awake. - */ private async maybe_fix_safari() { - if (!/Version\/14\.\d*\s*Safari\//.test(navigator.userAgent)) + if (!/Version\/14\.\d*\s*Safari\//.test(navigator.userAgent)) { return; + } - let id: any = 0; - return new Promise<void>(resolve => { + let id: ReturnType<typeof setInterval> | undefined; + return new Promise<void>((resolve) => { const hit = () => indexedDB.databases().finally(resolve); id = setInterval(hit, 50); hit(); - }) - .finally(() => clearInterval(id)); + }).finally(() => { + if (id) { + clearInterval(id); + } + }); } - /** */ private static as_promise<T = undefined>(request: IDBRequest<T> | IDBTransaction) { return new Promise<T>((resolve, reject) => { - // @ts-ignore - request.oncomplete = request.onsuccess = () => resolve(request.result); - - // @ts-ignore - request.onabort = request.onerror = () => reject(request.error); + const req = request as IDBRequest<T> & IDBTransaction; + req.oncomplete = req.onsuccess = () => resolve(req.result); + req.onabort = req.onerror = () => reject(req.error); }); } } -namespace IdbKeyval { - /** */ +export namespace IdbKeyval { export interface IConstructorOptions { - /** - * Defines the name of the IndexedDB database as it is stored in the browser. - * Note that the name is prefixed with the IdbKeyval database prefix constant. - */ name?: string | number; - - /** - * Defines the name or names of the index or indexes to define on the database. - */ indexes?: string | string[]; } - /** */ export interface IQuery { - /** - * A standard IDBKeyRange to use for the query. Worth noting that the methods - * in the static IdbKeyval.* namespace contain utility functions to ease the creation - * of IDBKeyRange objects. - */ range?: IDBKeyRange; - - /** The name of the index to use for the query. */ index?: string; - - /** A number which indicates the maximum number of objects to return from a query. */ limit?: number; } - /** */ export type Key = string | number | Date | BufferSource; } diff --git a/apps-lib/src/lib/utils/keyval/lib.ts b/apps-lib/src/lib/utils/keyval/lib.ts @@ -1,10 +1,10 @@ import { browser } from "$app/environment"; -import { _envLib } from "../_env"; +import { _env_lib } from "../_env"; import { fmt_id } from "../app/lib"; import { IdbKeyval } from "./idb"; export let idb_kv: IdbKeyval; -if (browser) idb_kv = new IdbKeyval({ name: _envLib.KEYVAL_NAME }); +if (browser) idb_kv = new IdbKeyval({ name: _env_lib.KEYVAL_NAME }); export const idb_kv_init = async (): Promise<void> => { if (!browser) return; diff --git a/apps-lib/src/lib/utils/nostr/lib.ts b/apps-lib/src/lib/utils/nostr/lib.ts @@ -16,28 +16,28 @@ export type NostrLoginOptions = export const nostr_logout = async (): Promise<void> => { - const $ndk = get_store(ndk); - $ndk.activeUser = undefined; + const ndk_val = get_store(ndk); + ndk_val.activeUser = undefined; document.cookie = "radroots_npub="; console.log(`logged out (nostr)`) }; export const nostr_login = async (opts: NostrLoginOptions): Promise<void> => { - const $ndk = get_store(ndk); + const ndk_val = get_store(ndk); let user: NDKUser | null = null; if (`nip07` in opts) { const signer = new NDKNip07Signer(); - $ndk.signer = signer; - user = await $ndk.signer.user(); - user.ndk = $ndk; + ndk_val.signer = signer; + user = await ndk_val.signer.user(); + user.ndk = ndk_val; } else if (`nostr_key` in opts) { const signer = new NDKPrivateKeySigner(opts.nostr_key); - $ndk.signer = signer; - user = await $ndk.signer.user(); - user.ndk = $ndk; + ndk_val.signer = signer; + user = await ndk_val.signer.user(); + user.ndk = ndk_val; } if (!user) return; ndk_user.set(user); - $ndk.activeUser = user; + ndk_val.activeUser = user; }; diff --git a/apps-lib/svelte.config.js b/apps-lib/svelte.config.js @@ -1,7 +1,6 @@ import adapter from '@sveltejs/adapter-auto'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; -/** @type {import('@sveltejs/kit').Config} */ const config = { preprocess: vitePreprocess(), kit: {