diff --git a/.vscode/settings.json b/.vscode/settings.json index f693212..84a28bd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,6 @@ { "editor.codeActionsOnSave": { - "quickfix.biome": "explicit", - "source.organizeImports.biome": "explicit" + "quickfix.biome": "explicit" }, "typescript.inlayHints.parameterNames.enabled": "all", "[typescript]": { diff --git a/bun.lock b/bun.lock index 38fc1e5..0591354 100644 --- a/bun.lock +++ b/bun.lock @@ -4,11 +4,11 @@ "": { "name": "signalstats", "dependencies": { - "@duskflower/signal-decrypt-backup-wasm": "^0.3.0", + "@duskflower/signal-decrypt-backup-wasm": "^0.2.1", "@kobalte/core": "^0.13.7", "@kobalte/tailwindcss": "^0.9.0", "@solid-primitives/refs": "^1.1.0", - "@solid-primitives/storage": "^4.3.1", + "@solid-primitives/storage": "^4.3.0", "@solid-primitives/upload": "^0.1.0", "@solid-primitives/workers": "^0.4.0", "@solidjs/meta": "^0.29.4", @@ -37,7 +37,7 @@ "@commitlint/cli": "^19.6.1", "@commitlint/config-conventional": "^19.6.0", "@tailwindcss/vite": "^4.0.0", - "@types/node": "^22.10.10", + "@types/node": "^22.10.9", "better-sqlite3": "^11.8.1", "husky": "^9.1.7", "kysely-codegen": "^0.17.0", @@ -141,7 +141,7 @@ "@corvu/utils": ["@corvu/utils@0.4.2", "", { "dependencies": { "@floating-ui/dom": "^1.6.11" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-Ox2kYyxy7NoXdKWdHeDEjZxClwzO4SKM8plAaVwmAJPxHMqA0rLOoAsa+hBDwRLpctf+ZRnAd/ykguuJidnaTA=="], - "@duskflower/signal-decrypt-backup-wasm": ["@duskflower/signal-decrypt-backup-wasm@0.3.0", "https://git.duskflower.dev/api/packages/duskflower/npm/%40duskflower%2Fsignal-decrypt-backup-wasm/-/0.3.0/signal-decrypt-backup-wasm-0.3.0.tgz", {}, "sha512-1IMNFGz4bV4o29eXVelNbancqovsljmM0iZRrtau81gSy8bFE1lgwGFN05NAOFWlX/aZYzeUEeZVBrjUYIM0/w=="], + "@duskflower/signal-decrypt-backup-wasm": ["@duskflower/signal-decrypt-backup-wasm@0.2.1", "https://git.duskflower.dev/api/packages/duskflower/npm/%40duskflower%2Fsignal-decrypt-backup-wasm/-/0.2.1/signal-decrypt-backup-wasm-0.2.1.tgz", {}, "sha512-lYwtubVOUU4kuJNw28Ns8N9Gh2nZB4j6BJz/vLpxF0Vc7zww3VIGnMCQToFWWSAwCs5+cABadeZMkv87SGfyQg=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.24.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA=="], diff --git a/package.json b/package.json index eac0529..64c8aaa 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "@commitlint/cli": "^19.6.1", "@commitlint/config-conventional": "^19.6.0", "@tailwindcss/vite": "^4.0.0", - "@types/node": "^22.10.10", + "@types/node": "^22.10.9", "better-sqlite3": "^11.8.1", "husky": "^9.1.7", "kysely-codegen": "^0.17.0", @@ -28,11 +28,11 @@ "vite-plugin-wasm": "^3.4.1" }, "dependencies": { - "@duskflower/signal-decrypt-backup-wasm": "^0.3.0", + "@duskflower/signal-decrypt-backup-wasm": "^0.2.1", "@kobalte/core": "^0.13.7", "@kobalte/tailwindcss": "^0.9.0", "@solid-primitives/refs": "^1.1.0", - "@solid-primitives/storage": "^4.3.1", + "@solid-primitives/storage": "^4.3.0", "@solid-primitives/upload": "^0.1.0", "@solid-primitives/workers": "^0.4.0", "@solidjs/meta": "^0.29.4", diff --git a/src/App.tsx b/src/App.tsx index cdf0433..4ae175b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,15 +1,16 @@ import { A, Route, Router, useNavigate } from "@solidjs/router"; -import { type Component, Show, Suspense, createEffect } from "solid-js"; -import { DmId, GroupId, Home, Overview, Privacy, preloadDmId } from "./pages"; +import { createEffect, Show, Suspense, type Component } from "solid-js"; +import { DmId, GroupId, Home, Overview, preloadDmId, Privacy } from "./pages"; + import "./app.css"; -import { ColorModeProvider, ColorModeScript, createLocalStorageManager } from "@kobalte/core"; import { MetaProvider } from "@solidjs/meta"; import { Portal } from "solid-js/web"; -import { Callout, CalloutContent, CalloutTitle } from "./components/ui/callout"; -import { ModeToggle } from "./components/ui/mode-toggle"; +import { Callout, CalloutTitle, CalloutContent } from "./components/ui/callout"; import { dbLoaded } from "./db"; import { hasCashedData } from "./lib/db-cache"; import { isWasmSupported } from "./lib/utils"; +import { ColorModeProvider, ColorModeScript, createLocalStorageManager } from "@kobalte/core"; +import { ModeToggle } from "./components/ui/mode-toggle"; const NO_DATA_NEEDED_PAGES = ["/", "/privacy"]; diff --git a/src/db/db-queries.ts b/src/db/db-queries.ts index 7193de8..4d1a87a 100644 --- a/src/db/db-queries.ts +++ b/src/db/db-queries.ts @@ -1,7 +1,7 @@ -import { type NotNull, sql } from "kysely"; -import type { MainToWorkerMsg, WorkerToMainMsg } from "~/lib/kysely-official-wasm-worker/type"; +import { sql, type NotNull } from "kysely"; +import { worker, kyselyDb, SELF_ID, DB_FILENAME, setDbLoaded } from "./db"; import { cached, clearDbCache } from "../lib/db-cache"; -import { DB_FILENAME, SELF_ID, kyselyDb, setDbLoaded, worker } from "./db"; +import type { MainToWorkerMsg, WorkerToMainMsg } from "~/lib/kysely-official-wasm-worker/type"; export const loadDb = async (statements: string[], progressCallback?: (percentage: number) => void): Promise => { // try to persist storage, https://web.dev/articles/persistent-storage#request_persistent_storage diff --git a/src/db/db.ts b/src/db/db.ts index b2168a7..584b868 100644 --- a/src/db/db.ts +++ b/src/db/db.ts @@ -1,8 +1,8 @@ import { Kysely } from "kysely"; -import { createSignal } from "solid-js"; -import { OfficialWasmWorkerDialect } from "~/lib/kysely-official-wasm-worker"; import type { DB } from "./db-schema"; +import { OfficialWasmWorkerDialect } from "~/lib/kysely-official-wasm-worker"; import WasmWorker from "./db-worker?worker"; +import { createSignal } from "solid-js"; export const SELF_ID = 2; diff --git a/src/lib/backup-decryptor.ts b/src/lib/decryptor.ts similarity index 80% rename from src/lib/backup-decryptor.ts rename to src/lib/decryptor.ts index 86c13e5..1ad2d8f 100644 --- a/src/lib/backup-decryptor.ts +++ b/src/lib/decryptor.ts @@ -1,4 +1,7 @@ -import { BackupDecryptor } from "@duskflower/signal-decrypt-backup-wasm"; +import { + BackupDecryptor, + type DecryptionResult, +} from "@duskflower/signal-decrypt-backup-wasm"; const CHUNK_SIZE = 1024 * 1024 * 40; // 40MB chunks @@ -6,8 +9,7 @@ export async function decryptBackup( file: File, passphrase: string, progressCallback: (progress: number) => void, - statementsCallback?: (statements: string[]) => void | Promise, -): Promise { +): Promise { const fileSize = file.size; const decryptor = new BackupDecryptor(); decryptor.set_progress_callback(fileSize, progressCallback); @@ -32,8 +34,6 @@ export async function decryptBackup( } } - await statementsCallback?.(decryptor.get_new_decrypted_statements()); - offset += CHUNK_SIZE; } diff --git a/src/lib/kysely-official-wasm-worker/index.ts b/src/lib/kysely-official-wasm-worker/index.ts index 9af4767..1886c71 100644 --- a/src/lib/kysely-official-wasm-worker/index.ts +++ b/src/lib/kysely-official-wasm-worker/index.ts @@ -1,4 +1,11 @@ -import type { DatabaseIntrospector, Dialect, DialectAdapter, Driver, Kysely, QueryCompiler } from "kysely"; +import type { + DatabaseIntrospector, + Dialect, + DialectAdapter, + Driver, + Kysely, + QueryCompiler, +} from "kysely"; import { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from "kysely"; import { OfficialWasmWorkerDriver } from "./driver"; import type { OfficialWasmWorkerDialectConfig } from "./type"; diff --git a/src/lib/messages-worker.ts b/src/lib/messages-worker.ts index 94dff12..d4cb339 100644 --- a/src/lib/messages-worker.ts +++ b/src/lib/messages-worker.ts @@ -1,5 +1,5 @@ -import type { MessageOverview, MessageStats, Recipients } from "~/types"; import { getHourList, getMonthList, getWeekdayList } from "./date"; +import type { MessageOverview, MessageStats, Recipients } from "~/types"; const hourNames = getHourList(); diff --git a/src/lib/messages.ts b/src/lib/messages.ts index f84d15a..51c1539 100644 --- a/src/lib/messages.ts +++ b/src/lib/messages.ts @@ -1,6 +1,6 @@ import type { MessageOverview, MessageStats, Recipients } from "~/types"; -import { getHourList, getMonthList, getWeekdayList } from "./date"; import { cached } from "./db-cache"; +import { getHourList, getMonthList, getWeekdayList } from "./date"; import MessageStatsWorker from "./messages-worker?worker"; export const hourNames = getHourList(); diff --git a/src/pages/dm/dm-id.tsx b/src/pages/dm/dm-id.tsx index 6ee94fd..91d884a 100644 --- a/src/pages/dm/dm-id.tsx +++ b/src/pages/dm/dm-id.tsx @@ -1,20 +1,21 @@ -import { Title } from "@solidjs/meta"; -import { type RoutePreloadFunc, type RouteSectionProps, createAsync } from "@solidjs/router"; -import { type Component, Suspense } from "solid-js"; -import { Flex } from "~/components/ui/flex"; -import { Grid } from "~/components/ui/grid"; -import { Heading } from "~/components/ui/heading"; -import { SELF_ID, dmPartnerRecipientQuery, threadMostUsedWordsQuery, threadSentMessagesOverviewQuery } from "~/db"; +import { Suspense, type Component } from "solid-js"; +import { createAsync, type RoutePreloadFunc, type RouteSectionProps } from "@solidjs/router"; + +import { dmPartnerRecipientQuery, threadMostUsedWordsQuery, threadSentMessagesOverviewQuery, SELF_ID } from "~/db"; import { getNameFromRecipient } from "~/lib/get-name-from-recipient"; -import { createMessageStatsSources } from "~/lib/messages"; -import type { MessageOverview } from "~/types"; +import { Heading } from "~/components/ui/heading"; +import { Grid } from "~/components/ui/grid"; +import { Title } from "@solidjs/meta"; import { DmMessagesPerDate } from "./dm-messages-per-date"; -import { DmMessagesPerDaytime } from "./dm-messages-per-daytime"; -import { DmMessagesPerMonth } from "./dm-messages-per-month"; -import { DmMessagesPerRecipient } from "./dm-messages-per-recipients"; -import { DmMessagesPerWeekday } from "./dm-messages-per-weekday"; import { DmOverview } from "./dm-overview"; import { DmWordCloud } from "./dm-wordcloud"; +import { DmMessagesPerMonth } from "./dm-messages-per-month"; +import { DmMessagesPerDaytime } from "./dm-messages-per-daytime"; +import { DmMessagesPerRecipient } from "./dm-messages-per-recipients"; +import { DmMessagesPerWeekday } from "./dm-messages-per-weekday"; +import type { MessageOverview } from "~/types"; +import { createMessageStatsSources } from "~/lib/messages"; +import { Flex } from "~/components/ui/flex"; const getDmIdData = (dmId: number) => { // the other person in the chat with name and id diff --git a/src/pages/dm/dm-messages-per-date.tsx b/src/pages/dm/dm-messages-per-date.tsx index 5e0a967..9d3aea7 100644 --- a/src/pages/dm/dm-messages-per-date.tsx +++ b/src/pages/dm/dm-messages-per-date.tsx @@ -1,5 +1,5 @@ +import { Show, type Accessor, type Component } from "solid-js"; import type { ChartData } from "chart.js"; -import { type Accessor, type Component, Show } from "solid-js"; import { LineChart } from "~/components/ui/charts"; import type { MessageStats, Recipients } from "~/types"; diff --git a/src/pages/dm/dm-messages-per-daytime.tsx b/src/pages/dm/dm-messages-per-daytime.tsx index 39765aa..1c12ff5 100644 --- a/src/pages/dm/dm-messages-per-daytime.tsx +++ b/src/pages/dm/dm-messages-per-daytime.tsx @@ -1,8 +1,8 @@ +import { Show, type Accessor, type Component } from "solid-js"; import type { ChartData } from "chart.js"; -import { type Accessor, type Component, Show } from "solid-js"; import { BarChart } from "~/components/ui/charts"; -import { hourNames } from "~/lib/messages"; import type { MessageStats, Recipients } from "~/types"; +import { hourNames } from "~/lib/messages"; export const DmMessagesPerDaytime: Component<{ daytimeStats: MessageStats["daytime"] | undefined; diff --git a/src/pages/dm/dm-messages-per-month.tsx b/src/pages/dm/dm-messages-per-month.tsx index 762b6a1..7bcceaf 100644 --- a/src/pages/dm/dm-messages-per-month.tsx +++ b/src/pages/dm/dm-messages-per-month.tsx @@ -1,5 +1,5 @@ +import { Show, type Accessor, type Component } from "solid-js"; import type { ChartData } from "chart.js"; -import { type Accessor, type Component, Show } from "solid-js"; import { RadarChart } from "~/components/ui/charts"; import { monthNames } from "~/lib/messages"; import type { MessageStats, Recipients } from "~/types"; diff --git a/src/pages/dm/dm-messages-per-recipients.tsx b/src/pages/dm/dm-messages-per-recipients.tsx index 4639183..8d43409 100644 --- a/src/pages/dm/dm-messages-per-recipients.tsx +++ b/src/pages/dm/dm-messages-per-recipients.tsx @@ -1,5 +1,5 @@ +import { Show, type Accessor, type Component } from "solid-js"; import type { ChartData } from "chart.js"; -import { type Accessor, type Component, Show } from "solid-js"; import { PieChart } from "~/components/ui/charts"; import type { MessageStats, Recipients } from "~/types"; diff --git a/src/pages/dm/dm-messages-per-weekday.tsx b/src/pages/dm/dm-messages-per-weekday.tsx index 1d66a81..4608298 100644 --- a/src/pages/dm/dm-messages-per-weekday.tsx +++ b/src/pages/dm/dm-messages-per-weekday.tsx @@ -1,5 +1,5 @@ +import { Show, type Accessor, type Component } from "solid-js"; import type { ChartData } from "chart.js"; -import { type Accessor, type Component, Show } from "solid-js"; import { RadarChart } from "~/components/ui/charts"; import { weekdayNames } from "~/lib/messages"; import type { MessageStats, Recipients } from "~/types"; diff --git a/src/pages/dm/dm-overview.tsx b/src/pages/dm/dm-overview.tsx index e2aa17b..1a1b464 100644 --- a/src/pages/dm/dm-overview.tsx +++ b/src/pages/dm/dm-overview.tsx @@ -1,7 +1,7 @@ -import { CalendarArrowDown, CalendarArrowUp, CalendarClock, MessagesSquare } from "lucide-solid"; -import { type Component, Show } from "solid-js"; +import { Show, type Component } from "solid-js"; import { Flex } from "~/components/ui/flex"; import { Grid } from "~/components/ui/grid"; +import { CalendarArrowDown, CalendarArrowUp, CalendarClock, MessagesSquare } from "lucide-solid"; import { getDistanceBetweenDatesInDays } from "~/lib/date"; import type { MessageOverview } from "~/types"; diff --git a/src/pages/dm/dm-wordcloud.tsx b/src/pages/dm/dm-wordcloud.tsx index e6237e8..bd24624 100644 --- a/src/pages/dm/dm-wordcloud.tsx +++ b/src/pages/dm/dm-wordcloud.tsx @@ -1,5 +1,5 @@ import type { ChartData } from "chart.js"; -import { type Accessor, type Component, Show } from "solid-js"; +import { Show, type Accessor, type Component } from "solid-js"; import { WordCloudChart } from "~/components/ui/charts"; import type { threadMostUsedWordsQuery } from "~/db"; diff --git a/src/pages/group/group-id.tsx b/src/pages/group/group-id.tsx index fa95ecc..3ac5993 100644 --- a/src/pages/group/group-id.tsx +++ b/src/pages/group/group-id.tsx @@ -1,5 +1,5 @@ -import type { RouteSectionProps } from "@solidjs/router"; import type { Component } from "solid-js"; +import type { RouteSectionProps } from "@solidjs/router"; export const GroupId: Component = (props) => { const groupId = () => Number(props.params.groupid); diff --git a/src/pages/home.tsx b/src/pages/home.tsx index 4d79e75..fa27c30 100644 --- a/src/pages/home.tsx +++ b/src/pages/home.tsx @@ -1,14 +1,16 @@ -import { createDropzone, createFileUploader } from "@solid-primitives/upload"; +import { useNavigate, type RouteSectionProps } from "@solidjs/router"; +import { createSignal, type JSX, Show, type Component } from "solid-js"; + import { Title } from "@solidjs/meta"; -import { type RouteSectionProps, useNavigate } from "@solidjs/router"; -import { type Component, type JSX, Show, createSignal } from "solid-js"; import { Portal } from "solid-js/web"; -import { Button } from "~/components/ui/button"; import { Flex } from "~/components/ui/flex"; + import { Progress, ProgressLabel, ProgressValueLabel } from "~/components/ui/progress"; -import { TextField, TextFieldInput, TextFieldLabel } from "~/components/ui/text-field"; import { loadDb } from "~/db"; -import { decryptBackup } from "~/lib/backup-decryptor"; +import { decryptBackup } from "~/lib/decryptor"; +import { createDropzone, createFileUploader } from "@solid-primitives/upload"; +import { Button } from "~/components/ui/button"; +import { TextField, TextFieldInput, TextFieldLabel } from "~/components/ui/text-field"; export const Home: Component = () => { const navigate = useNavigate(); @@ -32,8 +34,8 @@ export const Home: Component = () => { const [backupFile, setBackupFile] = createSignal(); const [decryptionProgress, setDecryptionProgress] = createSignal(); - const [totalStatements, setTotalStatements] = createSignal(0); - const [executedStatements, setExecutedStatements] = createSignal(0); + const [loadingProgress, setLoadingProgress] = createSignal(); + // const [isLoadingDatabase, setIsLoadingDatabase] = createSignal(false); const onSubmit: JSX.EventHandler = (event) => { event.preventDefault(); @@ -42,22 +44,28 @@ export const Home: Component = () => { const currentPassphrase = passphrase(); if (currentBackupFile && currentPassphrase) { - console.time(); - decryptBackup(currentBackupFile, currentPassphrase, setDecryptionProgress, async (statements) => { - const length = statements.length; - setTotalStatements((oldValue) => oldValue + length); + // const hashChunk = await currentBackupFile.slice(-1000).text(); + // const hash = hashString(hashChunk); - await loadDb(statements, (progress) => { - setExecutedStatements((oldValue) => Math.round(oldValue + (progress / 100) * length)); - }); + // if (hash === dbHash()) { + // return; + // } - setExecutedStatements((oldValue) => oldValue + length); - }) - .then(() => { + // setDbHash(hash); + + decryptBackup(currentBackupFile, currentPassphrase, setDecryptionProgress) + .then(async (decrypted) => { umami.track("Decrypt backup"); + setDecryptionProgress(undefined); + // setIsLoadingDatabase(true); + setLoadingProgress(0); + + await loadDb(decrypted.database_statements, setLoadingProgress); umami.track("Load database"); - console.timeEnd(); + // setIsLoadingDatabase(false); + setLoadingProgress(undefined); + navigate("/overview"); }) .catch((error) => { @@ -76,7 +84,7 @@ export const Home: Component = () => { class="fixed inset-0 gap-y-8 backdrop-blur-lg backdrop-filter" classList={{ // hidden: decryptionProgress() === undefined && !isLoadingDatabase(), - hidden: decryptionProgress() === undefined && totalStatements() === 0, + hidden: decryptionProgress() === undefined && loadingProgress() === undefined, }} > @@ -89,18 +97,20 @@ export const Home: Component = () => { class="w-[300px] space-y-1" >
- Decrypting... + Processing...
- + + {/*

Loading database

+

This can take some time

*/}

Loading database

`${value} of ${max}`} + maxValue={100} + getValueLabel={({ value }) => `${value}%`} class="w-[300px] space-y-1" >
diff --git a/src/pages/overview/index.tsx b/src/pages/overview/index.tsx index 423bf37..61e52b6 100644 --- a/src/pages/overview/index.tsx +++ b/src/pages/overview/index.tsx @@ -1,7 +1,9 @@ -import { Title } from "@solidjs/meta"; import type { RouteSectionProps } from "@solidjs/router"; -import { type Component, Show, createResource } from "solid-js"; -import { SELF_ID, allThreadsOverviewQuery, overallSentMessagesQuery } from "~/db"; +import { type Component, createResource, Show } from "solid-js"; + +import { allThreadsOverviewQuery, overallSentMessagesQuery, SELF_ID } from "~/db"; + +import { Title } from "@solidjs/meta"; import { getNameFromRecipient } from "~/lib/get-name-from-recipient"; import { OverviewTable, type RoomOverview } from "./overview-table"; diff --git a/src/pages/overview/overview-table.tsx b/src/pages/overview/overview-table.tsx index 1ae9656..d0533e0 100644 --- a/src/pages/overview/overview-table.tsx +++ b/src/pages/overview/overview-table.tsx @@ -1,29 +1,32 @@ +import { type Component, createSignal, For, Match, Show, Switch } from "solid-js"; import { useNavigate, usePreloadRoute } from "@solidjs/router"; + import { type ColumnFiltersState, - type FilterFn, - type SortDirection, - type SortingState, createColumnHelper, createSolidTable, + type FilterFn, flexRender, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, + type SortDirection, + type SortingState, } from "@tanstack/solid-table"; import { intlFormatDistance } from "date-fns"; import { ArrowDown, ArrowUp, ArrowUpDown } from "lucide-solid"; -import { type Component, For, Match, Show, Switch, createSignal } from "solid-js"; + import { Badge } from "~/components/ui/badge"; import { Button } from "~/components/ui/button"; import { Checkbox } from "~/components/ui/checkbox"; -import { Flex } from "~/components/ui/flex"; import { Label } from "~/components/ui/label"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table"; import { TextField, TextFieldInput } from "~/components/ui/text-field"; -import { dbLoaded, threadSentMessagesOverviewQuery } from "~/db"; + import { cn } from "~/lib/utils"; +import { Flex } from "~/components/ui/flex"; +import { dbLoaded, threadSentMessagesOverviewQuery } from "~/db"; export interface RoomOverview { threadId: number; diff --git a/vite.config.ts b/vite.config.ts index d086c34..c9270f0 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,8 +1,8 @@ import path from "path"; -import tailwind from "@tailwindcss/vite"; import { defineConfig } from "vite"; import solidPlugin from "vite-plugin-solid"; import wasm from "vite-plugin-wasm"; +import tailwind from "@tailwindcss/vite"; export default defineConfig({ plugins: [solidPlugin(), wasm(), tailwind()],