add convex for svelte

This commit is contained in:
Aman Varshney
2025-04-30 13:17:19 +05:30
parent 62af82cc29
commit 065f862752
8 changed files with 480 additions and 186 deletions

View File

@@ -0,0 +1,5 @@
---
"create-better-t-stack": minor
---
add convex for svelte

View File

@@ -91,7 +91,9 @@ export const dependencyVersionMap = {
convex: "^1.23.0",
"@convex-dev/react-query": "^0.0.0-alpha.8",
"convex-svelte": "^0.0.11",
"@tanstack/svelte-query": "^5.74.4",
"@tanstack/react-query-devtools": "^5.69.0",
"@tanstack/react-query": "^5.69.0",
} as const;

View File

@@ -1,5 +1,4 @@
import path from "node:path";
import consola from "consola"; // Import consola
import fs from "fs-extra";
import type { AvailableDependencies } from "../constants";
import type { ProjectConfig, ProjectFrontend } from "../types";
@@ -14,16 +13,16 @@ export async function setupApi(config: ProjectConfig): Promise<void> {
const webDirExists = await fs.pathExists(webDir);
const nativeDirExists = await fs.pathExists(nativeDir);
const hasReactWeb = frontend.some((f) =>
["tanstack-router", "react-router", "tanstack-start", "next"].includes(f),
);
const hasNuxtWeb = frontend.includes("nuxt");
const hasSvelteWeb = frontend.includes("svelte");
if (!isConvex && api !== "none") {
const serverDir = path.join(projectDir, "apps/server");
const serverDirExists = await fs.pathExists(serverDir);
const hasReactWeb = frontend.some((f) =>
["tanstack-router", "react-router", "tanstack-start", "next"].includes(f),
);
const hasNuxtWeb = frontend.includes("nuxt");
const hasSvelteWeb = frontend.includes("svelte");
if (serverDirExists) {
if (api === "orpc") {
await addPackageDependency({
@@ -81,6 +80,7 @@ export async function setupApi(config: ProjectConfig): Promise<void> {
"@orpc/svelte-query",
"@orpc/client",
"@orpc/server",
"@tanstack/svelte-query",
],
projectDir: webDir,
});
@@ -153,7 +153,6 @@ export async function setupApi(config: ProjectConfig): Promise<void> {
} else {
}
}
} else if (needsReactQuery && isConvex) {
}
if (isConvex) {
@@ -165,6 +164,9 @@ export async function setupApi(config: ProjectConfig): Promise<void> {
if (frontend.includes("tanstack-start")) {
webDepsToAdd.push("@convex-dev/react-query");
}
if (hasSvelteWeb) {
webDepsToAdd.push("convex-svelte");
}
await addPackageDependency({
dependencies: webDepsToAdd,

View File

@@ -578,32 +578,28 @@ export async function setupExamplesTemplate(
}
}
} else if (hasNuxtWeb) {
if (context.api === "orpc") {
const exampleWebNuxtSrc = path.join(exampleBaseDir, "web/nuxt");
if (await fs.pathExists(exampleWebNuxtSrc)) {
await processAndCopyFiles(
"**/*",
exampleWebNuxtSrc,
webAppDir,
context,
false,
);
} else {
}
const exampleWebNuxtSrc = path.join(exampleBaseDir, "web/nuxt");
if (await fs.pathExists(exampleWebNuxtSrc)) {
await processAndCopyFiles(
"**/*",
exampleWebNuxtSrc,
webAppDir,
context,
false,
);
} else {
}
} else if (hasSvelteWeb) {
if (context.api === "orpc") {
const exampleWebSvelteSrc = path.join(exampleBaseDir, "web/svelte");
if (await fs.pathExists(exampleWebSvelteSrc)) {
await processAndCopyFiles(
"**/*",
exampleWebSvelteSrc,
webAppDir,
context,
false,
);
} else {
}
const exampleWebSvelteSrc = path.join(exampleBaseDir, "web/svelte");
if (await fs.pathExists(exampleWebSvelteSrc)) {
await processAndCopyFiles(
"**/*",
exampleWebSvelteSrc,
webAppDir,
context,
false,
);
} else {
}
}
}

View File

@@ -1,150 +0,0 @@
<script lang="ts">
import { orpc } from '$lib/orpc';
import { createQuery, createMutation } from '@tanstack/svelte-query';
let newTodoText = $state('');
const todosQuery = createQuery(orpc.todo.getAll.queryOptions());
const addMutation = createMutation(
orpc.todo.create.mutationOptions({
onSuccess: () => {
$todosQuery.refetch();
newTodoText = '';
},
onError: (error) => {
console.error('Failed to create todo:', error?.message ?? error);
},
})
);
const toggleMutation = createMutation(
orpc.todo.toggle.mutationOptions({
onSuccess: () => {
$todosQuery.refetch();
},
onError: (error) => {
console.error('Failed to toggle todo:', error?.message ?? error);
},
})
);
const deleteMutation = createMutation(
orpc.todo.delete.mutationOptions({
onSuccess: () => {
$todosQuery.refetch();
},
onError: (error) => {
console.error('Failed to delete todo:', error?.message ?? error);
},
})
);
function handleAddTodo(event: SubmitEvent) {
event.preventDefault();
const text = newTodoText.trim();
if (text) {
$addMutation.mutate({ text });
}
}
function handleToggleTodo(id: number, completed: boolean) {
$toggleMutation.mutate({ id, completed: !completed });
}
function handleDeleteTodo(id: number) {
$deleteMutation.mutate({ id });
}
const isAdding = $derived($addMutation.isPending);
const canAdd = $derived(!isAdding && newTodoText.trim().length > 0);
const isLoadingTodos = $derived($todosQuery.isLoading);
const todos = $derived($todosQuery.data ?? []);
const hasTodos = $derived(todos.length > 0);
</script>
<div class="p-4">
<h1 class="text-xl mb-4">Todos</h1>
<form onsubmit={handleAddTodo} class="flex gap-2 mb-4">
<input
type="text"
bind:value={newTodoText}
placeholder="New task..."
disabled={isAdding}
class=" p-1 flex-grow"
/>
<button
type="submit"
disabled={!canAdd}
class="bg-blue-500 text-white px-3 py-1 rounded disabled:opacity-50"
>
{#if isAdding}Adding...{:else}Add{/if}
</button>
</form>
{#if isLoadingTodos}
<p>Loading...</p>
{:else if !hasTodos}
<p>No todos yet.</p>
{:else}
<ul class="space-y-1">
{#each todos as todo (todo.id)}
{@const isToggling = $toggleMutation.isPending && $toggleMutation.variables?.id === todo.id}
{@const isDeleting = $deleteMutation.isPending && $deleteMutation.variables?.id === todo.id}
{@const isDisabled = isToggling || isDeleting}
<li
class="flex items-center justify-between p-2 "
class:opacity-50={isDisabled}
>
<div class="flex items-center gap-2">
<input
type="checkbox"
id={`todo-${todo.id}`}
checked={todo.completed}
onchange={() => handleToggleTodo(todo.id, todo.completed)}
disabled={isDisabled}
/>
<label
for={`todo-${todo.id}`}
class:line-through={todo.completed}
>
{todo.text}
</label>
</div>
<button
type="button"
onclick={() => handleDeleteTodo(todo.id)}
disabled={isDisabled}
aria-label="Delete todo"
class="text-red-500 px-1 disabled:opacity-50"
>
{#if isDeleting}Deleting...{:else}X{/if}
</button>
</li>
{/each}
</ul>
{/if}
{#if $todosQuery.isError}
<p class="mt-4 text-red-500">
Error loading: {$todosQuery.error?.message ?? 'Unknown error'}
</p>
{/if}
{#if $addMutation.isError}
<p class="mt-4 text-red-500">
Error adding: {$addMutation.error?.message ?? 'Unknown error'}
</p>
{/if}
{#if $toggleMutation.isError}
<p class="mt-4 text-red-500">
Error updating: {$toggleMutation.error?.message ?? 'Unknown error'}
</p>
{/if}
{#if $deleteMutation.isError}
<p class="mt-4 text-red-500">
Error deleting: {$deleteMutation.error?.message ?? 'Unknown error'}
</p>
{/if}
</div>

View File

@@ -0,0 +1,357 @@
{{#if (eq backend "convex")}}
<script lang="ts">
import { useQuery, useConvexClient } from 'convex-svelte';
import { api } from '@{{projectName}}/backend/convex/_generated/api.js';
import type { Id } from '@{{projectName}}/backend/convex/_generated/dataModel.js';
let newTodoText = $state('');
let isAdding = $state(false);
let addError = $state<Error | null>(null);
let togglingId = $state<Id<'todos'> | null>(null);
let toggleError = $state<Error | null>(null);
let deletingId = $state<Id<'todos'> | null>(null);
let deleteError = $state<Error | null>(null);
const client = useConvexClient();
const todosQuery = useQuery(api.todos.getAll, {});
async function handleAddTodo(event: SubmitEvent) {
event.preventDefault();
const text = newTodoText.trim();
if (!text || isAdding) return;
isAdding = true;
addError = null;
try {
await client.mutation(api.todos.create, { text });
newTodoText = '';
} catch (err) {
console.error('Failed to add todo:', err);
addError = err instanceof Error ? err : new Error(String(err));
} finally {
isAdding = false;
}
}
async function handleToggleTodo(id: Id<'todos'>, completed: boolean) {
if (togglingId === id || deletingId === id) return;
togglingId = id;
toggleError = null;
try {
await client.mutation(api.todos.toggle, { id, completed: !completed });
} catch (err) {
console.error('Failed to toggle todo:', err);
toggleError = err instanceof Error ? err : new Error(String(err));
} finally {
if (togglingId === id) {
togglingId = null;
}
}
}
async function handleDeleteTodo(id: Id<'todos'>) {
if (togglingId === id || deletingId === id) return;
deletingId = id;
deleteError = null;
try {
await client.mutation(api.todos.deleteTodo, { id });
} catch (err) {
console.error('Failed to delete todo:', err);
deleteError = err instanceof Error ? err : new Error(String(err));
} finally {
if (deletingId === id) {
deletingId = null;
}
}
}
const canAdd = $derived(!isAdding && newTodoText.trim().length > 0);
const isLoadingTodos = $derived(todosQuery.isLoading);
const todos = $derived(todosQuery.data ?? []);
const hasTodos = $derived(todos.length > 0);
</script>
<div class="p-4">
<h1 class="text-xl mb-4">Todos (Convex)</h1>
<form onsubmit={handleAddTodo} class="flex gap-2 mb-4">
<input
type="text"
bind:value={newTodoText}
placeholder="New task..."
disabled={isAdding}
class="p-1 flex-grow"
/>
<button
type="submit"
disabled={!canAdd}
class="bg-blue-500 text-white px-3 py-1 rounded disabled:opacity-50"
>
{#if isAdding}Adding...{:else}Add{/if}
</button>
</form>
{#if isLoadingTodos}
<p>Loading...</p>
{:else if !hasTodos}
<p>No todos yet.</p>
{:else}
<ul class="space-y-1">
{#each todos as todo (todo._id)}
{@const isTogglingThis = togglingId === todo._id}
{@const isDeletingThis = deletingId === todo._id}
{@const isDisabled = isTogglingThis || isDeletingThis}
<li
class="flex items-center justify-between p-2"
class:opacity-50={isDisabled}
>
<div class="flex items-center gap-2">
<input
type="checkbox"
id={`todo-${todo._id}`}
checked={todo.completed}
onchange={() => handleToggleTodo(todo._id, todo.completed)}
disabled={isDisabled}
/>
<label
for={`todo-${todo._id}`}
class:line-through={todo.completed}
>
{todo.text}
</label>
</div>
<button
type="button"
onclick={() => handleDeleteTodo(todo._id)}
disabled={isDisabled}
aria-label="Delete todo"
class="text-red-500 px-1 disabled:opacity-50"
>
{#if isDeletingThis}Deleting...{:else}X{/if}
</button>
</li>
{/each}
</ul>
{/if}
{#if todosQuery.error}
<p class="mt-4 text-red-500">
Error loading: {todosQuery.error?.message ?? 'Unknown error'}
</p>
{/if}
{#if addError}
<p class="mt-4 text-red-500">
Error adding: {addError.message ?? 'Unknown error'}
</p>
{/if}
{#if toggleError}
<p class="mt-4 text-red-500">
Error updating: {toggleError.message ?? 'Unknown error'}
</p>
{/if}
{#if deleteError}
<p class="mt-4 text-red-500">
Error deleting: {deleteError.message ?? 'Unknown error'}
</p>
{/if}
</div>
{{else}}
<script lang="ts">
{{#if (eq api "orpc")}}
import { orpc } from '$lib/orpc';
{{/if}}
{{#if (eq api "trpc")}}
import { trpc } from '$lib/trpc';
{{/if}}
import { createQuery, createMutation } from '@tanstack/svelte-query';
let newTodoText = $state('');
{{#if (eq api "orpc")}}
const todosQuery = createQuery(orpc.todo.getAll.queryOptions());
const addMutation = createMutation(
orpc.todo.create.mutationOptions({
onSuccess: () => {
$todosQuery.refetch();
newTodoText = '';
},
onError: (error) => {
console.error('Failed to create todo:', error?.message ?? error);
},
})
);
const toggleMutation = createMutation(
orpc.todo.toggle.mutationOptions({
onSuccess: () => {
$todosQuery.refetch();
},
onError: (error) => {
console.error('Failed to toggle todo:', error?.message ?? error);
},
})
);
const deleteMutation = createMutation(
orpc.todo.delete.mutationOptions({
onSuccess: () => {
$todosQuery.refetch();
},
onError: (error) => {
console.error('Failed to delete todo:', error?.message ?? error);
},
})
);
{{/if}}
{{#if (eq api "trpc")}}
const todosQuery = createQuery(trpc.todo.getAll.queryOptions());
const addMutation = createMutation(
trpc.todo.create.mutationOptions({
onSuccess: () => {
$todosQuery.refetch();
newTodoText = '';
},
onError: (error) => {
console.error('Failed to create todo:', error?.message ?? error);
},
})
);
const toggleMutation = createMutation(
trpc.todo.toggle.mutationOptions({
onSuccess: () => {
$todosQuery.refetch();
},
onError: (error) => {
console.error('Failed to toggle todo:', error?.message ?? error);
},
})
);
const deleteMutation = createMutation(
trpc.todo.delete.mutationOptions({
onSuccess: () => {
$todosQuery.refetch();
},
onError: (error) => {
console.error('Failed to delete todo:', error?.message ?? error);
},
})
);
{{/if}}
function handleAddTodo(event: SubmitEvent) {
event.preventDefault();
const text = newTodoText.trim();
if (text) {
$addMutation.mutate({ text });
}
}
function handleToggleTodo(id: number, completed: boolean) {
$toggleMutation.mutate({ id, completed: !completed });
}
function handleDeleteTodo(id: number) {
$deleteMutation.mutate({ id });
}
const isAdding = $derived($addMutation.isPending);
const canAdd = $derived(!isAdding && newTodoText.trim().length > 0);
const isLoadingTodos = $derived($todosQuery.isLoading);
const todos = $derived($todosQuery.data ?? []);
const hasTodos = $derived(todos.length > 0);
</script>
<div class="p-4">
<h1 class="text-xl mb-4">Todos{{#if (eq api "trpc")}} (tRPC){{/if}}{{#if (eq api "orpc")}} (oRPC){{/if}}</h1>
<form onsubmit={handleAddTodo} class="flex gap-2 mb-4">
<input
type="text"
bind:value={newTodoText}
placeholder="New task..."
disabled={isAdding}
class=" p-1 flex-grow"
/>
<button
type="submit"
disabled={!canAdd}
class="bg-blue-500 text-white px-3 py-1 rounded disabled:opacity-50"
>
{#if isAdding}Adding...{:else}Add{/if}
</button>
</form>
{#if isLoadingTodos}
<p>Loading...</p>
{:else if !hasTodos}
<p>No todos yet.</p>
{:else}
<ul class="space-y-1">
{#each todos as todo (todo.id)}
{@const isToggling = $toggleMutation.isPending && $toggleMutation.variables?.id === todo.id}
{@const isDeleting = $deleteMutation.isPending && $deleteMutation.variables?.id === todo.id}
{@const isDisabled = isToggling || isDeleting}
<li
class="flex items-center justify-between p-2 "
class:opacity-50={isDisabled}
>
<div class="flex items-center gap-2">
<input
type="checkbox"
id={`todo-${todo.id}`}
checked={todo.completed}
onchange={() => handleToggleTodo(todo.id, todo.completed)}
disabled={isDisabled}
/>
<label
for={`todo-${todo.id}`}
class:line-through={todo.completed}
>
{todo.text}
</label>
</div>
<button
type="button"
onclick={() => handleDeleteTodo(todo.id)}
disabled={isDisabled}
aria-label="Delete todo"
class="text-red-500 px-1 disabled:opacity-50"
>
{#if isDeleting}Deleting...{:else}X{/if}
</button>
</li>
{/each}
</ul>
{/if}
{#if $todosQuery.isError}
<p class="mt-4 text-red-500">
Error loading: {$todosQuery.error?.message ?? 'Unknown error'}
</p>
{/if}
{#if $addMutation.isError}
<p class="mt-4 text-red-500">
Error adding: {$addMutation.error?.message ?? 'Unknown error'}
</p>
{/if}
{#if $toggleMutation.isError}
<p class="mt-4 text-red-500">
Error updating: {$toggleMutation.error?.message ?? 'Unknown error'}
</p>
{/if}
{#if $deleteMutation.isError}
<p class="mt-4 text-red-500">
Error deleting: {$deleteMutation.error?.message ?? 'Unknown error'}
</p>
{/if}
</div>
{{/if}}

View File

@@ -1,8 +1,31 @@
{{#if (eq backend "convex")}}
<script lang="ts">
import '../app.css';
import Header from '../components/Header.svelte';
import { PUBLIC_CONVEX_URL } from '$env/static/public';
import { setupConvex } from 'convex-svelte';
const { children } = $props();
setupConvex(PUBLIC_CONVEX_URL);
</script>
<div class="grid h-svh grid-rows-[auto_1fr]">
<Header />
<main class="overflow-y-auto">
{@render children()}
</main>
</div>
{{else}}
<script lang="ts">
import { QueryClientProvider } from '@tanstack/svelte-query';
import { SvelteQueryDevtools } from '@tanstack/svelte-query-devtools'
import '../app.css';
{{#if (eq api "orpc")}}
import { queryClient } from '$lib/orpc';
{{/if}}
{{#if (eq api "trpc")}}
import { queryClient } from '$lib/trpc';
{{/if}}
import Header from '../components/Header.svelte';
let { children } = $props();
@@ -17,3 +40,4 @@
</div>
<SvelteQueryDevtools />
</QueryClientProvider>
{{/if}}

View File

@@ -1,8 +1,10 @@
{{#if (eq backend "convex")}}
<script lang="ts">
import { orpc } from "$lib/orpc";
import { createQuery } from "@tanstack/svelte-query";
import { useQuery } from 'convex-svelte';
import { api } from "@{{projectName}}/backend/convex/_generated/api.js";
const healthCheck = createQuery(orpc.healthCheck.queryOptions());
const healthCheck = useQuery(api.healthCheck.get, {});
const TITLE_TEXT = `
@@ -26,7 +28,62 @@ const TITLE_TEXT = `
<pre class="overflow-x-auto font-mono text-sm">{TITLE_TEXT}</pre>
<div class="grid gap-6">
<section class="rounded-lg border p-4">
<h2 class="mb-2 font-medium">API Status</h2>
<h2 class="mb-2 font-medium">API Status (Convex)</h2>
<div class="flex items-center gap-2">
<div
class={`h-2 w-2 rounded-full ${healthCheck.data ? "bg-green-500" : "bg-red-500"}`}
></div>
<span class="text-muted-foreground text-sm">
{healthCheck.isLoading
? "Checking..."
: healthCheck.data
? "Connected"
: "Disconnected"}
</span>
</div>
</section>
</div>
</div>
{{else}}
<script lang="ts">
{{#if (eq api "orpc")}}
import { orpc } from "$lib/orpc";
{{/if}}
{{#if (eq api "trpc")}}
import { trpc } from "$lib/trpc";
{{/if}}
import { createQuery } from "@tanstack/svelte-query";
{{#if (eq api "orpc")}}
const healthCheck = createQuery(orpc.healthCheck.queryOptions());
{{/if}}
{{#if (eq api "trpc")}}
const healthCheck = createQuery(trpc.healthCheck.queryOptions());
{{/if}}
const TITLE_TEXT = `
██████╗ ███████╗████████╗████████╗███████╗██████╗
██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗
██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝
██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗
██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║
╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝
████████╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗
╚══██╔══╝ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝
██║ ███████╗ ██║ ███████║██║ █████╔╝
██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗
██║ ███████║ ██║ ██║ ██║╚██████╗██║ ██╗
╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝
`;
</script>
<div class="container mx-auto max-w-3xl px-4 py-2">
<pre class="overflow-x-auto font-mono text-sm">{TITLE_TEXT}</pre>
<div class="grid gap-6">
<section class="rounded-lg border p-4">
<h2 class="mb-2 font-medium">API Status{{#if (eq api "trpc")}} (tRPC){{/if}}{{#if (eq api "orpc")}} (oRPC){{/if}}</h2>
<div class="flex items-center gap-2">
<div
class={`h-2 w-2 rounded-full ${$healthCheck.data ? "bg-green-500" : "bg-red-500"}`}
@@ -42,3 +99,4 @@ const TITLE_TEXT = `
</section>
</div>
</div>
{{/if}}