Compare commits

..

6 commits

Author SHA1 Message Date
9a6b8094da
fix: add generic item type to Autocomplete 2024-04-05 00:35:07 +02:00
9b7e6fdbd4
fix: fetch Autocomplete items on select
doc: add some documentation to Autocomplete
2024-04-04 17:52:43 +02:00
c459450abc
fix: cannot create execution 2024-04-04 17:13:53 +02:00
96e55c52a5
chore: update dependencies 2024-04-04 17:06:47 +02:00
b9b8f55c28
fix: search/filter error with slash/backslash/escaped quote in query 2024-03-18 03:08:17 +01:00
5afc7da40d
fix: search/filter error with slash/backslash/escaped quote in query
now replacing ^ -> ^^, / with ^1 and \ with ^2
2024-03-18 02:08:49 +01:00
21 changed files with 690 additions and 649 deletions

View file

@ -14,10 +14,16 @@ module.exports = {
parser: "svelte-eslint-parser", parser: "svelte-eslint-parser",
parserOptions: { parserOptions: {
parser: "@typescript-eslint/parser", parser: "@typescript-eslint/parser",
svelteFeatures: {
experimentalGenerics: true,
},
}, },
}, },
], ],
settings: {}, settings: {},
globals: {
$$Generic: "readonly",
},
parserOptions: { parserOptions: {
sourceType: "module", sourceType: "module",
ecmaVersion: 2020, ecmaVersion: 2020,

2
.gitignore vendored
View file

@ -8,5 +8,3 @@ node_modules
!.env.example !.env.example
vite.config.js.timestamp-* vite.config.js.timestamp-*
vite.config.ts.timestamp-* vite.config.ts.timestamp-*
sveltekit-zero-api.d.ts
/.vscode

3
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"eslint.validate": ["javascript", "typescript", "svelte"]
}

View file

@ -17,13 +17,13 @@
"test:e2e": "playwright test" "test:e2e": "playwright test"
}, },
"dependencies": { "dependencies": {
"@auth/core": "^0.27.0", "@auth/core": "^0.28.2",
"@floating-ui/core": "^1.6.0", "@floating-ui/core": "^1.6.0",
"@mdi/js": "^7.4.47", "@mdi/js": "^7.4.47",
"@prisma/client": "^5.10.2", "@prisma/client": "^5.12.1",
"diff": "^5.2.0", "diff": "^5.2.0",
"isomorphic-dompurify": "^2.4.0", "isomorphic-dompurify": "^2.6.0",
"marked": "^12.0.0", "marked": "^12.0.1",
"set-cookie-parser": "^2.6.0", "set-cookie-parser": "^2.6.0",
"svelte-floating-ui": "^1.5.8", "svelte-floating-ui": "^1.5.8",
"zod": "^3.22.4", "zod": "^3.22.4",
@ -31,40 +31,40 @@
}, },
"devDependencies": { "devDependencies": {
"@faker-js/faker": "^8.4.1", "@faker-js/faker": "^8.4.1",
"@playwright/test": "^1.42.0", "@playwright/test": "^1.42.1",
"@sveltejs/adapter-node": "^4.0.1", "@sveltejs/adapter-node": "^5.0.1",
"@sveltejs/kit": "^2.5.2", "@sveltejs/kit": "^2.5.5",
"@sveltejs/vite-plugin-svelte": "^3.0.2", "@sveltejs/vite-plugin-svelte": "^3.0.2",
"@tailwindcss/typography": "^0.5.10", "@tailwindcss/typography": "^0.5.12",
"@trpc/client": "^10.45.1", "@trpc/client": "^10.45.2",
"@trpc/server": "^10.45.1", "@trpc/server": "^10.45.2",
"@types/diff": "^5.0.9", "@types/diff": "^5.0.9",
"@types/node": "^20.11.21", "@types/node": "^20.12.4",
"@types/set-cookie-parser": "^2.4.7", "@types/set-cookie-parser": "^2.4.7",
"@typescript-eslint/eslint-plugin": "^7.1.0", "@typescript-eslint/eslint-plugin": "^7.5.0",
"@typescript-eslint/parser": "^7.1.0", "@typescript-eslint/parser": "^7.5.0",
"autoprefixer": "^10.4.17", "autoprefixer": "^10.4.19",
"daisyui": "^4.7.2", "daisyui": "^4.9.0",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-no-relative-import-paths": "^1.5.3", "eslint-plugin-no-relative-import-paths": "^1.5.3",
"eslint-plugin-svelte": "^2.35.1", "eslint-plugin-svelte": "^2.35.1",
"postcss-import": "^16.0.1", "postcss-import": "^16.1.0",
"postcss-nesting": "^12.0.4", "postcss-nesting": "^12.1.1",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"prettier-plugin-svelte": "^3.2.2", "prettier-plugin-svelte": "^3.2.2",
"prisma": "^5.10.2", "prisma": "^5.12.1",
"svelte": "^4.2.12", "svelte": "^4.2.12",
"svelte-check": "^3.6.5", "svelte-check": "^3.6.9",
"sveltekit-superforms": "^2.6.2", "sveltekit-superforms": "^2.12.2",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.3",
"trpc-sveltekit": "^3.5.27", "trpc-sveltekit": "^3.6.1",
"tslib": "^2.6.2", "tslib": "^2.6.2",
"tsx": "^4.7.1", "tsx": "^4.7.2",
"typescript": "^5.3.3", "typescript": "^5.4.3",
"vite": "^5.1.4", "vite": "^5.2.8",
"vitest": "^1.3.1" "vitest": "^1.4.0"
}, },
"type": "module" "type": "module"
} }

File diff suppressed because it is too large Load diff

View file

@ -10,33 +10,46 @@
import { createFloatingActions } from "svelte-floating-ui"; import { createFloatingActions } from "svelte-floating-ui";
import { shift } from "svelte-floating-ui/dom"; import { shift } from "svelte-floating-ui/dom";
import outclick from "$lib/actions/outclick"; import outclick from "$lib/actions/outclick";
import type { BaseItem, SelectionOrText } from "./types"; import type { BaseItem } from "./types";
import Icon from "$lib/components/ui/Icon.svelte"; import Icon from "$lib/components/ui/Icon.svelte";
import { onMount } from "svelte";
type T = $$Generic<BaseItem>;
type OnSelectResult = { newValue: string; close: boolean }; type OnSelectResult = { newValue: string; close: boolean };
export let items: BaseItem[] | (() => Promise<BaseItem[]>); /** List of items to choose from (or an async function fetching them) */
export let selection: SelectionOrText | null = null; export let items: T[] | (() => Promise<T[]>);
/** Current selection of the autocomplete field */
export let selection: T | null = null;
/** Set of item IDs that should be hidden from the list */
export let hiddenIds: Set<string | number> = new Set(); export let hiddenIds: Set<string | number> = new Set();
export let cache: { [key: string]: BaseItem[] } = {}; /** Object to cache fetched items in */
export let cache: { [key: string]: T[] } = {};
/** Key in cache object under which fetched items are stored */
export let cacheKey: string | undefined = undefined; export let cacheKey: string | undefined = undefined;
/** Input field placeholder */
export let placeholder: string | undefined = undefined; export let placeholder: string | undefined = undefined;
/** Add horizontal padding to the input element */
export let padding = true; export let padding = true;
/** CSS classes of the container */
export let cls = ""; export let cls = "";
/** CSS classes of the input element */
export let inputCls = "w-full bg-transparent outline-none"; export let inputCls = "w-full bg-transparent outline-none";
export let asTextInput = false; /**
* Enable filter bar optimizations:
* - Clear input on mount
*/
export let partOfFilterbar = false;
/** Dont select item automatically on close if only 1 item is shown */ /** Dont select item automatically on close if only 1 item is shown */
export let noAutoselect1 = false; export let noAutoselect1 = false;
/** Name of the form input. This sets a hidden field to the ID of the selected element
* (to allow Svelte Superforms to work) */
export let idInputName: string | undefined = undefined; export let idInputName: string | undefined = undefined;
export let filterFn: (item: BaseItem) => boolean = () => true; /** Function to filter shown items */
export let filterFn: (item: T) => boolean = () => true;
/** Selection callback. Returns the new input value after selection */ /** Selection callback. Returns the new input value after selection */
export let onSelect: (item: SelectionOrText, kb: boolean) => OnSelectResult = ( export let onSelect: (item: T, kb: boolean) => OnSelectResult | void = () => {};
item,
kb
) => {
return { newValue: item.name ?? "", close: true };
};
export let onUnselect = () => {}; export let onUnselect = () => {};
export let onClose = (kb: boolean) => {}; export let onClose = (kb: boolean) => {};
export let onBackspace = () => {}; export let onBackspace = () => {};
@ -44,10 +57,11 @@
let opened = false; let opened = false;
let highlightIndex = 0; let highlightIndex = 0;
let isLoading = false; let isLoading = false;
let srcItems: BaseItem[]; let srcItems: T[];
let filteredItems: BaseItem[] = []; let filteredItems: T[] = [];
$: if (hiddenIds || selection) updateSearch(); // Reload search items if hidden ids are changed
$: if (srcItems && hiddenIds) updateSearch();
// HTML elements // HTML elements
let inputElm: HTMLInputElement | undefined; let inputElm: HTMLInputElement | undefined;
@ -92,7 +106,7 @@
if (i !== -1) { if (i !== -1) {
highlightIndex = i; highlightIndex = i;
} }
if (asTextInput) setInputValue(selection.name ?? ""); if (!partOfFilterbar) setInputValue(selection.name ?? "");
} else { } else {
highlightIndex = 0; highlightIndex = 0;
setInputValue(selection.name ?? ""); setInputValue(selection.name ?? "");
@ -142,30 +156,34 @@
if (inputElm) inputElm.focus(); if (inputElm) inputElm.focus();
} }
export function close(kb: boolean) { export function close(kb = true) {
if (opened) { if (opened) {
onClose(kb); onClose(kb);
} }
opened = false; opened = false;
} }
function selectListItem(item: BaseItem | undefined, kb: boolean) { function selectListItem(item: T | undefined, kb: boolean) {
selection = { id: item?.id, name: item?.name }; if (item) {
const selRes = onSelect(item ?? { name: inputValue() }, kb); selection = item;
setInputValue(selRes.newValue); const selRes = onSelect(item, kb);
if (selRes.close) { setInputValue(selRes ? selRes.newValue : item.name);
close(kb); if (!selRes || selRes.close) {
close(kb);
} else {
updateSearch();
}
} else { } else {
updateSearch(); selection = null;
} }
} }
function onKeyDown(e: KeyboardEvent) { function onKeyDown(e: KeyboardEvent) {
let key = e.key; let key = e.key;
if (key === "Tab" && e.shiftKey) key = "ShiftTab"; if (key === "Tab" && e.shiftKey) key = "ShiftTab";
const fnmap = { const fnmap: { [key: string]: () => void } = {
Tab: close, Tab: () => close,
ShiftTab: close, ShiftTab: () => close,
ArrowDown: () => { ArrowDown: () => {
open(); open();
if (highlightIndex < filteredItems.length - 1) { if (highlightIndex < filteredItems.length - 1) {
@ -184,7 +202,7 @@
e.stopPropagation(); e.stopPropagation();
if (opened) { if (opened) {
if (inputElm) inputElm.focus(); if (inputElm) inputElm.focus();
close(true); close();
} }
}, },
Backspace: () => { Backspace: () => {
@ -195,10 +213,9 @@
} }
}, },
}; };
// @ts-expect-error unknown map type
const fn = fnmap[key]; const fn = fnmap[key];
if (typeof fn === "function") { if (typeof fn === "function") {
fn(e); fn();
} }
} }
@ -250,6 +267,14 @@
placement: "bottom-start", placement: "bottom-start",
middleware: [shift()], middleware: [shift()],
}); });
if (!partOfFilterbar) {
onMount(() => {
if (selection?.name) {
setInputValue(selection.name);
}
});
}
</script> </script>
<div class="flex-grow {cls}" use:outclick on:outclick={close}> <div class="flex-grow {cls}" use:outclick on:outclick={close}>

View file

@ -24,7 +24,7 @@
/** True if a separate search field should be displayed */ /** True if a separate search field should be displayed */
export let search = false; export let search = false;
let autocomplete: Autocomplete | undefined; let autocomplete: Autocomplete<BaseItem> | undefined;
let activeFilters: FilterData[] = []; let activeFilters: FilterData[] = [];
let cache: { [key: string]: BaseItem[] } = {}; let cache: { [key: string]: BaseItem[] } = {};
let searchVal = ""; let searchVal = "";
@ -216,6 +216,7 @@
activeFilters = activeFilters; activeFilters = activeFilters;
updateFilter(); updateFilter();
}} }}
partOfFilterbar
/> />
<button <button
class="btn btn-sm btn-circle btn-ghost absolute bottom-0 right-0" class="btn btn-sm btn-circle btn-ghost absolute bottom-0 right-0"

View file

@ -12,7 +12,7 @@
export let onRemove = () => {}; export let onRemove = () => {};
export let onSelection = (selection: SelectionOrText, kb: boolean) => {}; export let onSelection = (selection: SelectionOrText, kb: boolean) => {};
let autocomplete: Autocomplete | undefined; let autocomplete: Autocomplete<BaseItem> | undefined;
function startEditing() { function startEditing() {
fdata.editing = true; fdata.editing = true;
@ -76,7 +76,9 @@ gap-1 pl-1"
hiddenIds={hids} hiddenIds={hids}
{cache} {cache}
cacheKey={filter.id} cacheKey={filter.id}
selection={fdata.selection} selection={fdata.selection?.id
? { id: fdata.selection?.id, name: fdata.selection?.name ?? "" }
: null}
padding={false} padding={false}
onSelect={(item) => { onSelect={(item) => {
// Accept the selection if this is a free text field or the user selected a variant // Accept the selection if this is a free text field or the user selected a variant
@ -89,6 +91,7 @@ gap-1 pl-1"
}} }}
{onClose} {onClose}
onBackspace={onRemove} onBackspace={onRemove}
partOfFilterbar
/> />
{:else} {:else}
<!-- svelte-ignore a11y-autofocus --> <!-- svelte-ignore a11y-autofocus -->

View file

@ -68,16 +68,12 @@
}} }}
selection={patient?.room} selection={patient?.room}
onSelect={(item) => { onSelect={(item) => {
// @ts-expect-error room items have station attr
station = item.station; station = item.station;
// @ts-expect-error ids are always numeric
$form.room_id = item.id; $form.room_id = item.id;
return { newValue: item.name ?? "", close: true };
}} }}
onUnselect={() => { onUnselect={() => {
$form.room_id = null; $form.room_id = null;
}} }}
asTextInput
idInputName="room_id" idInputName="room_id"
/> />
</FormField> </FormField>

View file

@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import FormField from "./FormField.svelte";
import Markdown from "./Markdown.svelte"; import Markdown from "./Markdown.svelte";
import type { InputConstraint } from "sveltekit-superforms"; import type { InputConstraint } from "sveltekit-superforms";

View file

@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation";
import { mdiChevronLeft, mdiChevronRight, mdiPageFirst, mdiPageLast } from "@mdi/js"; import { mdiChevronLeft, mdiChevronRight, mdiPageFirst, mdiPageLast } from "@mdi/js";
import type { Pagination, PaginationRequest } from "$lib/shared/model"; import type { Pagination, PaginationRequest } from "$lib/shared/model";

View file

@ -117,10 +117,10 @@ export async function newEntryVersion(
// Check if there are any updates // Check if there are any updates
if ( if (
cver.text === updatedVersion.text && cver?.text === updatedVersion.text &&
cver.date.getTime() === updatedVersion.date.getTime() && cver?.date.getTime() === updatedVersion.date.getTime() &&
cver.category_id === updatedVersion.category_id && cver?.category_id === updatedVersion.category_id &&
cver.priority === updatedVersion.priority cver?.priority === updatedVersion.priority
) { ) {
return cver.id; return cver.id;
} }
@ -160,7 +160,7 @@ export async function newEntryExecution(
} }
// Check if there are any updates // Check if there are any updates
if (execution.text === cex.text) { if (execution.text === cex?.text) {
return cex.id; return cex.id;
} }

View file

@ -87,15 +87,18 @@ class SearchQueryComponents {
*/ */
export function parseSearchQuery(q: string): SearchQueryComponents { export function parseSearchQuery(q: string): SearchQueryComponents {
const regexpParts = /(-)?(?:"([^"]*)"|([^"\s]+))(?:\s|$)/g; const regexpParts = /(-)?(?:"([^"]*)"|([^"\s]+))(?:\s|$)/g;
const components = Array.from(q.replaceAll("'", '"').matchAll(regexpParts), (m) => { const components = Array.from(
const negative = m[1] === "-"; q.replaceAll("'", '"').replaceAll("\\", "\\\\").matchAll(regexpParts),
// Exact (m) => {
if (m[2]) { const negative = m[1] === "-";
return new SearchQueryComponent(m[2], QueryComponentType.Exact, negative); // Exact
} else { if (m[2]) {
return new SearchQueryComponent(m[3], QueryComponentType.Normal, negative); return new SearchQueryComponent(m[2], QueryComponentType.Exact, negative);
} else {
return new SearchQueryComponent(m[3], QueryComponentType.Normal, negative);
}
} }
}); );
if ( if (
components.length > 0 && components.length > 0 &&

View file

@ -74,7 +74,7 @@ export function formatBool(val: boolean): string {
export function getQueryUrl(q: EntityQuery, basePath: string): string { export function getQueryUrl(q: EntityQuery, basePath: string): string {
if (Object.values(q).filter((q) => q !== undefined).length === 0) return basePath; if (Object.values(q).filter((q) => q !== undefined).length === 0) return basePath;
return basePath + "/" + JSON.stringify(q); return encodeURI(basePath + "/" + encodeURIComponent(JSON.stringify(q)));
} }
export function gotoEntityQuery(q: EntityQuery, basePath: string) { export function gotoEntityQuery(q: EntityQuery, basePath: string) {

View file

@ -51,14 +51,12 @@
}} }}
selection={data.entry.current_version.category} selection={data.entry.current_version.category}
onSelect={(item) => { onSelect={(item) => {
// @ts-expect-error ids are always numeric
$form.category_id = item.id; $form.category_id = item.id;
return { newValue: item.name ?? "", close: true }; return { newValue: item.name ?? "", close: true };
}} }}
onUnselect={() => { onUnselect={() => {
$form.category_id = null; $form.category_id = null;
}} }}
asTextInput
idInputName="category_id" idInputName="category_id"
/> />
</FormField> </FormField>

View file

@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import CategoryField from "$lib/components/table/CategoryField.svelte";
import UserField from "$lib/components/table/UserField.svelte"; import UserField from "$lib/components/table/UserField.svelte";
import { formatBool, formatDate } from "$lib/shared/util"; import { formatDate } from "$lib/shared/util";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
import Header from "$lib/components/ui/Header.svelte"; import Header from "$lib/components/ui/Header.svelte";
import { page } from "$app/stores"; import { page } from "$app/stores";

View file

@ -27,14 +27,12 @@
return await trpc().room.list.query(); return await trpc().room.list.query();
}} }}
onSelect={(item) => { onSelect={(item) => {
// @ts-expect-error ids are always numeric
$form.room_id = item.id; $form.room_id = item.id;
return { newValue: item.name ?? "", close: true }; return { newValue: item.name ?? "", close: true };
}} }}
onUnselect={() => { onUnselect={() => {
$form.room_id = null; $form.room_id = null;
}} }}
asTextInput
idInputName="room_id" idInputName="room_id"
/> />
</FormField> </FormField>
@ -46,10 +44,8 @@
return await trpc().patient.getNames.query(); return await trpc().patient.getNames.query();
}} }}
onSelect={(item) => { onSelect={(item) => {
// @ts-expect-error patient id
$form.patient_id = item.id; $form.patient_id = item.id;
trpc() trpc()
// @ts-expect-error patient id
.patient.get.query(item.id) .patient.get.query(item.id)
.then((p) => { .then((p) => {
$form.patient_first_name = p.first_name; $form.patient_first_name = p.first_name;
@ -64,11 +60,9 @@
$form.patient_last_name = null; $form.patient_last_name = null;
$form.patient_age = null; $form.patient_age = null;
}} }}
asTextInput
noAutoselect1 noAutoselect1
idInputName="patient_id" idInputName="patient_id"
filterFn={(itm) => { filterFn={(itm) => {
// @ts-expect-error patient items have room attr
return $form.room_id === null || itm.room_id === $form.room_id; return $form.room_id === null || itm.room_id === $form.room_id;
}} }}
placeholder="Neuer Patient" placeholder="Neuer Patient"
@ -119,14 +113,12 @@
return await trpc().category.list.query(); return await trpc().category.list.query();
}} }}
onSelect={(item) => { onSelect={(item) => {
// @ts-expect-error ids are always numeric
$form.category_id = item.id; $form.category_id = item.id;
return { newValue: item.name ?? "", close: true }; return { newValue: item.name ?? "", close: true };
}} }}
onUnselect={() => { onUnselect={() => {
$form.category_id = null; $form.category_id = null;
}} }}
asTextInput
idInputName="category_id" idInputName="category_id"
/> />
</FormField> </FormField>

View file

@ -1,4 +1,6 @@
import { ZPatientNew, ZUrlEntityId } from "$lib/shared/model/validation"; import { z } from "zod";
import { ZEntriesQuery, ZPatientNew, ZUrlEntityId } from "$lib/shared/model/validation";
import { trpc } from "$lib/shared/trpc"; import { trpc } from "$lib/shared/trpc";
import { loadWrap } from "$lib/shared/util"; import { loadWrap } from "$lib/shared/util";
import { superValidate } from "sveltekit-superforms"; import { superValidate } from "sveltekit-superforms";
@ -8,10 +10,9 @@ import type { PageLoad } from "./$types";
export const load: PageLoad = async (event) => { export const load: PageLoad = async (event) => {
const id = ZUrlEntityId.parse(event.params.id); const id = ZUrlEntityId.parse(event.params.id);
// eslint-disable-next-line @typescript-eslint/no-explicit-any let query: z.infer<typeof ZEntriesQuery> = {};
let query: any = {};
if (event.params.query) { if (event.params.query) {
query = JSON.parse(event.params.query); query = ZEntriesQuery.parse(JSON.parse(decodeURIComponent(event.params.query)));
} }
if (!query.filter) query.filter = {}; if (!query.filter) query.filter = {};
query.filter.patient = [{ id }]; query.filter.patient = [{ id }];

View file

@ -9,7 +9,7 @@ export const load: PageLoad = async (event) => {
let query: z.infer<typeof ZPatientsQuery> = {}; let query: z.infer<typeof ZPatientsQuery> = {};
if (event.params.query) { if (event.params.query) {
query = ZPatientsQuery.parse(JSON.parse(event.params.query)); query = ZPatientsQuery.parse(JSON.parse(decodeURIComponent(event.params.query)));
} }
const patients = await loadWrap(async () => trpc(event).patient.list.query(query)); const patients = await loadWrap(async () => trpc(event).patient.list.query(query));

View file

@ -1,12 +1,15 @@
import { z } from "zod";
import { ZEntriesQuery } from "$lib/shared/model/validation";
import { trpc } from "$lib/shared/trpc"; import { trpc } from "$lib/shared/trpc";
import { loadWrap } from "$lib/shared/util"; import { loadWrap } from "$lib/shared/util";
import type { PageLoad } from "./$types"; import type { PageLoad } from "./$types";
export const load: PageLoad = async (event) => { export const load: PageLoad = async (event) => {
let query = {}; let query: z.infer<typeof ZEntriesQuery> = {};
if (event.params.query) { if (event.params.query) {
query = JSON.parse(event.params.query); query = ZEntriesQuery.parse(JSON.parse(decodeURIComponent(event.params.query)));
} }
const entries = await loadWrap(() => trpc(event).entry.list.query(query)); const entries = await loadWrap(() => trpc(event).entry.list.query(query));

View file

@ -9,9 +9,7 @@ const config = {
kit: { kit: {
// See https://kit.svelte.dev/docs/adapters for more information about adapters. // See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter({ adapter: adapter(),
precompress: true,
}),
alias: { alias: {
$api: "./src/api", $api: "./src/api",