create-better-t-stack 2.28.5 → 2.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -100,10 +100,14 @@ const dependencyVersionMap = {
100
100
  "@trpc/tanstack-react-query": "^11.4.2",
101
101
  "@trpc/server": "^11.4.2",
102
102
  "@trpc/client": "^11.4.2",
103
- convex: "^1.25.0",
103
+ convex: "^1.25.4",
104
104
  "@convex-dev/react-query": "^0.0.0-alpha.8",
105
105
  "convex-svelte": "^0.0.11",
106
+ "convex-nuxt": "0.1.5",
107
+ "convex-vue": "^0.1.5",
106
108
  "@tanstack/svelte-query": "^5.74.4",
109
+ "@tanstack/vue-query-devtools": "^5.83.0",
110
+ "@tanstack/vue-query": "^5.83.0",
107
111
  "@tanstack/react-query-devtools": "^5.80.5",
108
112
  "@tanstack/react-query": "^5.80.5",
109
113
  "@tanstack/solid-query": "^5.75.0",
@@ -471,7 +475,7 @@ async function getAuthChoice(auth, hasDatabase, backend) {
471
475
  //#region src/prompts/backend.ts
472
476
  async function getBackendFrameworkChoice(backendFramework, frontends) {
473
477
  if (backendFramework !== void 0) return backendFramework;
474
- const hasIncompatibleFrontend = frontends?.some((f) => f === "nuxt" || f === "solid");
478
+ const hasIncompatibleFrontend = frontends?.some((f) => f === "solid");
475
479
  const backendOptions = [
476
480
  {
477
481
  value: "hono",
@@ -509,12 +513,10 @@ async function getBackendFrameworkChoice(backendFramework, frontends) {
509
513
  label: "None",
510
514
  hint: "No backend server"
511
515
  });
512
- let initialValue = DEFAULT_CONFIG.backend;
513
- if (hasIncompatibleFrontend && initialValue === "convex") initialValue = "hono";
514
516
  const response = await select({
515
517
  message: "Select backend",
516
518
  options: backendOptions,
517
- initialValue
519
+ initialValue: DEFAULT_CONFIG.backend
518
520
  });
519
521
  if (isCancel(response)) {
520
522
  cancel(pc.red("Operation cancelled"));
@@ -754,7 +756,7 @@ async function getFrontendChoice(frontendOptions, backend) {
754
756
  }
755
757
  ];
756
758
  const webOptions = allWebOptions.filter((option) => {
757
- if (backend === "convex") return option.value !== "nuxt" && option.value !== "solid";
759
+ if (backend === "convex") return option.value !== "solid";
758
760
  return true;
759
761
  });
760
762
  const webFramework = await select({
@@ -1466,7 +1468,7 @@ function processAndValidateFlags(options, providedFlags, projectName) {
1466
1468
  process.exit(1);
1467
1469
  }
1468
1470
  if (providedFlags.has("frontend") && options.frontend) {
1469
- const incompatibleFrontends = options.frontend.filter((f) => f === "nuxt" || f === "solid");
1471
+ const incompatibleFrontends = options.frontend.filter((f) => f === "solid");
1470
1472
  if (incompatibleFrontends.length > 0) {
1471
1473
  consola$1.fatal(`The following frontends are not compatible with '--backend convex': ${incompatibleFrontends.join(", ")}. Please choose a different frontend or backend.`);
1472
1474
  process.exit(1);
@@ -3010,6 +3012,8 @@ async function setupApi(config) {
3010
3012
  } else if (hasNuxtWeb) {
3011
3013
  if (api === "orpc") await addPackageDependency({
3012
3014
  dependencies: [
3015
+ "@tanstack/vue-query",
3016
+ "@tanstack/vue-query-devtools",
3013
3017
  "@orpc/tanstack-query",
3014
3018
  "@orpc/client",
3015
3019
  "@orpc/server"
@@ -3113,6 +3117,10 @@ async function setupApi(config) {
3113
3117
  const webDepsToAdd = ["convex"];
3114
3118
  if (frontend.includes("tanstack-start")) webDepsToAdd.push("@convex-dev/react-query");
3115
3119
  if (hasSvelteWeb) webDepsToAdd.push("convex-svelte");
3120
+ if (hasNuxtWeb) {
3121
+ webDepsToAdd.push("convex-nuxt");
3122
+ webDepsToAdd.push("convex-vue");
3123
+ }
3116
3124
  await addPackageDependency({
3117
3125
  dependencies: webDepsToAdd,
3118
3126
  projectDir: webDir
@@ -4903,7 +4911,7 @@ function getBunWebNativeWarning() {
4903
4911
  return `\n${pc.yellow("WARNING:")} 'bun' might cause issues with web + native apps in a monorepo. Use 'pnpm' if problems arise.`;
4904
4912
  }
4905
4913
  function getWorkersDeployInstructions(runCmd) {
4906
- return `\n${pc.bold("Deploy frontend to Cloudflare Workers:")}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd || "bun run"} deploy`}`;
4914
+ return `\n${pc.bold("Deploy frontend to Cloudflare Workers:")}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} run deploy`}`;
4907
4915
  }
4908
4916
 
4909
4917
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-better-t-stack",
3
- "version": "2.28.5",
3
+ "version": "2.29.0",
4
4
  "description": "A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with best practices and customizable configurations",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -9,9 +9,9 @@
9
9
  "license": "ISC",
10
10
  "description": "",
11
11
  "devDependencies": {
12
- "typescript": "^5.8.3"
12
+ "typescript": "^5.9.2"
13
13
  },
14
14
  "dependencies": {
15
- "convex": "^1.25.0"
15
+ "convex": "^1.25.4"
16
16
  }
17
17
  }
@@ -0,0 +1,195 @@
1
+ <script setup lang="ts">
2
+ import { ref } from 'vue'
3
+ {{#if (eq backend "convex")}}
4
+ import { api } from "@{{ projectName }}/backend/convex/_generated/api";
5
+ import type { Id } from "@{{ projectName }}/backend/convex/_generated/dataModel";
6
+ import { useConvexMutation, useConvexQuery } from "convex-vue";
7
+
8
+ const { data, error, isPending } = useConvexQuery(api.todos.getAll, {});
9
+
10
+ const newTodoText = ref("");
11
+ const { mutate: createTodo, isPending: isCreatePending } = useConvexMutation(api.todos.create);
12
+
13
+ const { mutate: toggleTodo } = useConvexMutation(api.todos.toggle);
14
+ const { mutate: deleteTodo, error: deleteError } = useConvexMutation(
15
+ api.todos.deleteTodo,
16
+ );
17
+
18
+ function handleAddTodo() {
19
+ const text = newTodoText.value.trim();
20
+ if (!text) return;
21
+
22
+ createTodo({ text });
23
+ newTodoText.value = "";
24
+ }
25
+
26
+ function handleToggleTodo(id: Id<"todos">, completed: boolean) {
27
+ toggleTodo({ id, completed: !completed });
28
+ }
29
+
30
+ function handleDeleteTodo(id: Id<"todos">) {
31
+ deleteTodo({ id });
32
+ }
33
+ {{else}}
34
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query'
35
+
36
+ const { $orpc } = useNuxtApp()
37
+
38
+ const newTodoText = ref('')
39
+ const queryClient = useQueryClient()
40
+
41
+ const todos = useQuery($orpc.todo.getAll.queryOptions())
42
+
43
+ const createMutation = useMutation($orpc.todo.create.mutationOptions({
44
+ onSuccess: () => {
45
+ queryClient.invalidateQueries()
46
+ newTodoText.value = ''
47
+ }
48
+ }))
49
+
50
+ const toggleMutation = useMutation($orpc.todo.toggle.mutationOptions({
51
+ onSuccess: () => queryClient.invalidateQueries()
52
+ }))
53
+
54
+ const deleteMutation = useMutation($orpc.todo.delete.mutationOptions({
55
+ onSuccess: () => queryClient.invalidateQueries()
56
+ }))
57
+
58
+ function handleAddTodo() {
59
+ if (newTodoText.value.trim()) {
60
+ createMutation.mutate({ text: newTodoText.value })
61
+ }
62
+ }
63
+
64
+ function handleToggleTodo(id: number, completed: boolean) {
65
+ toggleMutation.mutate({ id, completed: !completed })
66
+ }
67
+
68
+ function handleDeleteTodo(id: number) {
69
+ deleteMutation.mutate({ id })
70
+ }
71
+ {{/if}}
72
+ </script>
73
+
74
+ <template>
75
+ <div class="mx-auto w-full max-w-md py-10">
76
+ <UCard>
77
+ <template #header>
78
+ <div>
79
+ <div class="text-xl font-bold">Todo List</div>
80
+ <div class="text-muted text-sm">Manage your tasks efficiently</div>
81
+ </div>
82
+ </template>
83
+ <form @submit.prevent="handleAddTodo" class="mb-6 flex items-center gap-2">
84
+ <UInput
85
+ v-model="newTodoText"
86
+ placeholder="Add a new task..."
87
+ autocomplete="off"
88
+ class="w-full"
89
+ {{#if (eq backend "convex")}}
90
+ :disabled="isCreatePending"
91
+ {{/if}}
92
+ />
93
+ <UButton
94
+ type="submit"
95
+ {{#if (eq backend "convex")}}
96
+ :disabled="isCreatePending || !newTodoText.trim()"
97
+ {{/if}}
98
+ >
99
+ {{#if (eq backend "convex")}}
100
+ <span v-if="isCreatePending">
101
+ <UIcon name="i-lucide-loader-2" class="animate-spin" />
102
+ </span>
103
+ <span v-else>Add</span>
104
+ {{else}}
105
+ Add
106
+ {{/if}}
107
+ </UButton>
108
+ </form>
109
+
110
+ {{#if (eq backend "convex")}}
111
+ <p v-if="error || deleteError" class="mb-4 text-red-500">
112
+ Error: \{{ error?.message || deleteError?.message }}
113
+ </p>
114
+ <div v-if="isPending" class="flex justify-center py-4">
115
+ <UIcon name="i-lucide-loader-2" class="animate-spin w-6 h-6" />
116
+ </div>
117
+ <p v-else-if="data?.length === 0" class="py-4 text-center">
118
+ No todos yet. Add one above!
119
+ </p>
120
+ <ul v-else-if="data" class="space-y-2">
121
+ <li
122
+ v-for="todo in data"
123
+ :key="todo._id"
124
+ class="flex items-center justify-between rounded-md border p-2"
125
+ >
126
+ <div class="flex items-center gap-2">
127
+ <UCheckbox
128
+ :model-value="todo.completed"
129
+ @update:model-value="() => handleToggleTodo(todo._id, todo.completed)"
130
+ :id="`todo-${todo._id}`"
131
+ />
132
+ <label
133
+ :for="`todo-${todo._id}`"
134
+ :class="{ 'line-through text-muted': todo.completed }"
135
+ class="cursor-pointer"
136
+ >
137
+ \{{ todo.text }}
138
+ </label>
139
+ </div>
140
+ <UButton
141
+ color="neutral"
142
+ variant="ghost"
143
+ size="sm"
144
+ square
145
+ @click="handleDeleteTodo(todo._id)"
146
+ aria-label="Delete todo"
147
+ icon="i-lucide-trash-2"
148
+ />
149
+ </li>
150
+ </ul>
151
+ {{else}}
152
+ <div v-if="todos.status.value === 'pending'" class="flex justify-center py-4">
153
+ <UIcon name="i-lucide-loader-2" class="animate-spin w-6 h-6" />
154
+ </div>
155
+ <p v-else-if="todos.status.value === 'error'" class="py-4 text-center text-red-500">
156
+ Error: \{{ todos.error.value?.message || 'Failed to load todos' }}
157
+ </p>
158
+ <p v-else-if="todos.data.value?.length === 0" class="py-4 text-center">
159
+ No todos yet. Add one above!
160
+ </p>
161
+ <ul v-else class="space-y-2">
162
+ <li
163
+ v-for="todo in todos.data.value"
164
+ :key="todo.id"
165
+ class="flex items-center justify-between rounded-md border p-2"
166
+ >
167
+ <div class="flex items-center gap-2">
168
+ <UCheckbox
169
+ :model-value="todo.completed"
170
+ @update:model-value="() => handleToggleTodo(todo.id, todo.completed)"
171
+ :id="`todo-${todo.id}`"
172
+ />
173
+ <label
174
+ :for="`todo-${todo.id}`"
175
+ :class="{ 'line-through text-muted': todo.completed }"
176
+ class="cursor-pointer"
177
+ >
178
+ \{{ todo.text }}
179
+ </label>
180
+ </div>
181
+ <UButton
182
+ color="neutral"
183
+ variant="ghost"
184
+ size="sm"
185
+ square
186
+ @click="handleDeleteTodo(todo.id)"
187
+ aria-label="Delete todo"
188
+ icon="i-lucide-trash-2"
189
+ />
190
+ </li>
191
+ </ul>
192
+ {{/if}}
193
+ </UCard>
194
+ </div>
195
+ </template>
@@ -1,5 +1,7 @@
1
1
  <script setup lang="ts">
2
+ {{#if (eq api "orpc")}}
2
3
  import { VueQueryDevtools } from '@tanstack/vue-query-devtools'
4
+ {{/if}}
3
5
  </script>
4
6
 
5
7
  <template>
@@ -9,5 +11,7 @@ import { VueQueryDevtools } from '@tanstack/vue-query-devtools'
9
11
  <NuxtPage />
10
12
  </NuxtLayout>
11
13
  </UApp>
14
+ {{#if (eq api "orpc")}}
12
15
  <VueQueryDevtools />
16
+ {{/if}}
13
17
  </template>
@@ -1,5 +1,4 @@
1
1
  <script setup lang="ts">
2
- import { USeparator } from '#components';
3
2
  import ModeToggle from './ModeToggle.vue'
4
3
  {{#if auth}}
5
4
  import UserMenu from './UserMenu.vue'
@@ -1,4 +1,6 @@
1
1
  <script setup lang="ts">
2
+ import { computed } from "vue";
3
+
2
4
  const colorMode = useColorMode()
3
5
 
4
6
  const isDark = computed({
@@ -1,8 +1,13 @@
1
1
  <script setup lang="ts">
2
- {{#unless (eq api "none")}}
3
- const { $orpc } = useNuxtApp()
4
- import { useQuery } from '@tanstack/vue-query'
5
- {{/unless}}
2
+ {{#if (eq backend "convex")}}
3
+ import { api } from "@{{ projectName }}/backend/convex/_generated/api";
4
+ import { useConvexQuery } from "convex-vue";
5
+ {{else}}
6
+ {{#unless (eq api "none")}}
7
+ const { $orpc } = useNuxtApp()
8
+ import { useQuery } from '@tanstack/vue-query'
9
+ {{/unless}}
10
+ {{/if}}
6
11
 
7
12
  const TITLE_TEXT = `
8
13
  ██████╗ ███████╗████████╗████████╗███████╗██████╗
@@ -20,19 +25,34 @@ const TITLE_TEXT = `
20
25
  ╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝
21
26
  `;
22
27
 
23
- {{#unless (eq api "none")}}
24
- const healthCheck = useQuery($orpc.healthCheck.queryOptions())
25
- {{/unless}}
28
+ {{#if (eq backend "convex")}}
29
+ const healthCheck = useConvexQuery(api.healthCheck.get, {});
30
+ {{else}}
31
+ {{#unless (eq api "none")}}
32
+ const healthCheck = useQuery($orpc.healthCheck.queryOptions())
33
+ {{/unless}}
34
+ {{/if}}
26
35
  </script>
27
36
 
28
37
  <template>
29
38
  <div class="container mx-auto max-w-3xl px-4 py-2">
30
39
  <pre class="overflow-x-auto font-mono text-sm whitespace-pre-wrap">\{{ TITLE_TEXT }}</pre>
31
40
  <div class="grid gap-6 mt-4">
32
- {{#unless (eq api "none")}}
33
41
  <section class="rounded-lg border p-4">
34
42
  <h2 class="mb-2 font-medium">API Status</h2>
35
43
  <div class="flex items-center gap-2">
44
+ {{#if (eq backend "convex")}}
45
+ <span class="text-sm text-muted-foreground">
46
+ \{{
47
+ healthCheck === undefined
48
+ ? "Checking..."
49
+ : healthCheck.data.value === "OK"
50
+ ? "Connected"
51
+ : "Error"
52
+ }}
53
+ </span>
54
+ {{else}}
55
+ {{#unless (eq api "none")}}
36
56
  <div class="flex items-center gap-2">
37
57
  <div
38
58
  class="w-2 h-2 rounded-full"
@@ -60,9 +80,10 @@ const healthCheck = useQuery($orpc.healthCheck.queryOptions())
60
80
  </template>
61
81
  </span>
62
82
  </div>
63
- </div>
83
+ {{/unless}}
84
+ {{/if}}
85
+ </div>
64
86
  </section>
65
- {{/unless}}
66
87
  </div>
67
88
  </div>
68
89
  </template>
@@ -2,12 +2,22 @@
2
2
  export default defineNuxtConfig({
3
3
  compatibilityDate: 'latest',
4
4
  devtools: { enabled: true },
5
- modules: ['@nuxt/ui'],
5
+ modules: [
6
+ '@nuxt/ui'
7
+ {{#if (eq backend "convex")}},
8
+ 'convex-nuxt'
9
+ {{/if}}
10
+ ],
6
11
  css: ['~/assets/css/main.css'],
7
12
  devServer: {
8
13
  port: 3001
9
14
  },
10
15
  ssr: false,
16
+ {{#if (eq backend "convex")}}
17
+ convex: {
18
+ url: process.env.NUXT_PUBLIC_CONVEX_URL,
19
+ },
20
+ {{/if}}
11
21
  runtimeConfig: {
12
22
  public: {
13
23
  serverURL: process.env.NUXT_PUBLIC_SERVER_URL,
@@ -11,7 +11,6 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "@nuxt/ui": "3.3.0",
14
- "@tanstack/vue-query": "^5.83.0",
15
14
  "nuxt": "^4.0.2",
16
15
  "typescript": "^5.8.3",
17
16
  "vue": "^3.5.18",
@@ -20,7 +19,6 @@
20
19
  },
21
20
  "devDependencies": {
22
21
  "tailwindcss": "^4.1.11",
23
- "@tanstack/vue-query-devtools": "^5.83.0",
24
22
  "@iconify-json/lucide": "^1.2.57"
25
23
  }
26
24
  }
@@ -1,108 +0,0 @@
1
- <script setup lang="ts">
2
- import { ref } from 'vue'
3
- import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query'
4
-
5
- const { $orpc } = useNuxtApp()
6
-
7
- const newTodoText = ref('')
8
- const queryClient = useQueryClient()
9
-
10
- const todos = useQuery($orpc.todo.getAll.queryOptions())
11
-
12
- const createMutation = useMutation($orpc.todo.create.mutationOptions({
13
- onSuccess: () => {
14
- queryClient.invalidateQueries()
15
- newTodoText.value = ''
16
- }
17
- }))
18
-
19
- const toggleMutation = useMutation($orpc.todo.toggle.mutationOptions({
20
- onSuccess: () => queryClient.invalidateQueries()
21
- }))
22
-
23
- const deleteMutation = useMutation($orpc.todo.delete.mutationOptions({
24
- onSuccess: () => queryClient.invalidateQueries()
25
- }))
26
-
27
- function handleAddTodo() {
28
- if (newTodoText.value.trim()) {
29
- createMutation.mutate({ text: newTodoText.value })
30
- }
31
- }
32
-
33
- function handleToggleTodo(id: number, completed: boolean) {
34
- toggleMutation.mutate({ id, completed: !completed })
35
- }
36
-
37
- function handleDeleteTodo(id: number) {
38
- deleteMutation.mutate({ id })
39
- }
40
- </script>
41
-
42
- <template>
43
- <div class="mx-auto w-full max-w-md py-10">
44
- <UCard>
45
- <template #header>
46
- <div>
47
- <div class="text-xl font-bold">Todo List</div>
48
- <div class="text-muted text-sm">Manage your tasks efficiently</div>
49
- </div>
50
- </template>
51
- <form @submit.prevent="handleAddTodo" class="mb-6 flex items-center gap-2">
52
- <UInput
53
- v-model="newTodoText"
54
- placeholder="Add a new task..."
55
- autocomplete="off"
56
- class="w-full"
57
- />
58
- <UButton
59
- type="submit"
60
- icon="i-lucide-plus"
61
- >
62
- Add
63
- </UButton>
64
- </form>
65
-
66
- <div v-if="todos.status.value === 'pending'" class="flex justify-center py-4">
67
- <UIcon name="i-lucide-loader-2" class="animate-spin w-6 h-6" />
68
- </div>
69
- <p v-else-if="todos.status.value === 'error'" class="py-4 text-center text-red-500">
70
- Error: {{ todos.error.value?.message || 'Failed to load todos' }}
71
- </p>
72
- <p v-else-if="todos.data.value?.length === 0" class="py-4 text-center">
73
- No todos yet. Add one above!
74
- </p>
75
- <ul v-else class="space-y-2">
76
- <li
77
- v-for="todo in todos.data.value"
78
- :key="todo.id"
79
- class="flex items-center justify-between rounded-md border p-2"
80
- >
81
- <div class="flex items-center gap-2">
82
- <UCheckbox
83
- :model-value="todo.completed"
84
- @update:model-value="() => handleToggleTodo(todo.id, todo.completed)"
85
- :id="`todo-${todo.id}`"
86
- />
87
- <label
88
- :for="`todo-${todo.id}`"
89
- :class="{ 'line-through text-muted': todo.completed }"
90
- class="cursor-pointer"
91
- >
92
- {{ todo.text }}
93
- </label>
94
- </div>
95
- <UButton
96
- color="neutral"
97
- variant="ghost"
98
- size="sm"
99
- square
100
- @click="handleDeleteTodo(todo.id)"
101
- aria-label="Delete todo"
102
- icon="i-lucide-trash-2"
103
- />
104
- </li>
105
- </ul>
106
- </UCard>
107
- </div>
108
- </template>