Skip to content

Commit eefd943

Browse files
GitButler WIP Commit
This is a WIP commit for the virtual branch 'main' This commit is used to store the state of the virtual branch while you are working on it. It is not meant to be used for anything else.
1 parent cd93d5e commit eefd943

File tree

36 files changed

+1107
-12
lines changed

36 files changed

+1107
-12
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ pnpm create better-t-stack@latest
2121

2222
- **Zero-config setup** with interactive CLI wizard
2323
- **End-to-end type safety** from database to frontend via tRPC
24-
- **Modern stack** with React, Hono/Elysia, and TanStack libraries
24+
- **Modern stack** with React, Vue, Nuxt, Svelte, Solid, and more
2525
- **Multi-platform** supporting web, mobile (Expo), and desktop applications
2626
- **Database flexibility** with SQLite (Turso) or PostgreSQL options
2727
- **ORM choice** between Drizzle or Prisma

apps/cli/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Follow the prompts to configure your project or use the `--yes` flag for default
2626
| Category | Options |
2727
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2828
| **TypeScript** | End-to-end type safety across all parts of your application |
29-
| **Frontend** | • React with TanStack Router<br>• React with React Router<br>• React with TanStack Start (SSR)<br>• Next.js<br>• SvelteKit<br>• Nuxt (Vue)<br>• SolidJS<br>• React Native with NativeWind (via Expo)<br>• React Native with Unistyles (via Expo)<br>• None |
29+
| **Frontend** | • React with TanStack Router<br>• React with React Router<br>• React with TanStack Start (SSR)<br>• Next.js<br>• Vue with Vue Router<br>• Nuxt (Vue)<br>• SvelteKit<br>• SolidJS<br>• React Native with NativeWind (via Expo)<br>• React Native with Unistyles (via Expo)<br>• None |
3030
| **Backend** | • Hono<br>• Express<br>• Elysia<br>• Next.js API routes<br>• Convex<br>• Fastify<br>• None |
3131
| **API Layer** | • tRPC (type-safe APIs)<br>• oRPC (OpenAPI-compatible type-safe APIs)<br>• None |
3232
| **Runtime** | • Bun<br>• Node.js<br>• Cloudflare Workers<br>• None |
@@ -51,7 +51,7 @@ Options:
5151
--orm <type> ORM type (none, drizzle, prisma, mongoose)
5252
--auth Include authentication
5353
--no-auth Exclude authentication
54-
--frontend <types...> Frontend types (tanstack-router, react-router, tanstack-start, next, nuxt, svelte, solid, native-nativewind, native-unistyles, none)
54+
--frontend <types...> Frontend types (tanstack-router, react-router, tanstack-start, next, nuxt, vue-router, svelte, solid, native-nativewind, native-unistyles, none)
5555
--addons <types...> Additional addons (pwa, tauri, starlight, biome, husky, turborepo, none)
5656
--examples <types...> Examples to include (todo, ai, none)
5757
--git Initialize git repository

apps/cli/src/constants.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,8 @@ export const dependencyVersionMap = {
118118
export type AvailableDependencies = keyof typeof dependencyVersionMap;
119119

120120
export const ADDON_COMPATIBILITY = {
121-
pwa: ["tanstack-router", "react-router", "solid", "next"],
122-
tauri: ["tanstack-router", "react-router", "nuxt", "svelte", "solid"],
121+
pwa: ["tanstack-router", "react-router", "solid", "next", "vue-router"],
122+
tauri: ["tanstack-router", "react-router", "nuxt", "vue-router", "svelte", "solid"],
123123
biome: [],
124124
husky: [],
125125
turborepo: [],
@@ -134,6 +134,7 @@ export const WEB_FRAMEWORKS: readonly Frontend[] = [
134134
"tanstack-start",
135135
"next",
136136
"nuxt",
137+
"vue-router",
137138
"svelte",
138139
"solid",
139140
];

apps/cli/src/helpers/project-generation/template-manager.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,15 @@ export async function setupFrontendTemplates(
6868
["tanstack-router", "react-router", "tanstack-start", "next"].includes(f),
6969
);
7070
const hasNuxtWeb = context.frontend.includes("nuxt");
71+
const hasVueRouterWeb = context.frontend.includes("vue-router");
7172
const hasSvelteWeb = context.frontend.includes("svelte");
7273
const hasSolidWeb = context.frontend.includes("solid");
7374
const hasNativeWind = context.frontend.includes("native-nativewind");
7475
const hasUnistyles = context.frontend.includes("native-unistyles");
7576
const _hasNative = hasNativeWind || hasUnistyles;
7677
const isConvex = context.backend === "convex";
7778

78-
if (hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb) {
79+
if (hasReactWeb || hasNuxtWeb || hasVueRouterWeb || hasSvelteWeb || hasSolidWeb) {
7980
const webAppDir = path.join(projectDir, "apps/web");
8081
await fs.ensureDir(webAppDir);
8182

@@ -141,6 +142,23 @@ export async function setupFrontendTemplates(
141142
} else {
142143
}
143144
}
145+
} else if (hasVueRouterWeb) {
146+
const vueRouterBaseDir = path.join(PKG_ROOT, "templates/frontend/vue/vue-router");
147+
if (await fs.pathExists(vueRouterBaseDir)) {
148+
await processAndCopyFiles("**/*", vueRouterBaseDir, webAppDir, context);
149+
} else {
150+
}
151+
152+
if (!isConvex && context.api !== "none") {
153+
const apiWebVueDir = path.join(
154+
PKG_ROOT,
155+
`templates/api/${context.api}/web/vue`,
156+
);
157+
if (await fs.pathExists(apiWebVueDir)) {
158+
await processAndCopyFiles("**/*", apiWebVueDir, webAppDir, context);
159+
} else {
160+
}
161+
}
144162
} else if (hasSvelteWeb) {
145163
const svelteBaseDir = path.join(PKG_ROOT, "templates/frontend/svelte");
146164
if (await fs.pathExists(svelteBaseDir)) {
@@ -373,6 +391,7 @@ export async function setupAuthTemplate(
373391
["tanstack-router", "react-router", "tanstack-start", "next"].includes(f),
374392
);
375393
const hasNuxtWeb = context.frontend.includes("nuxt");
394+
const hasVueRouterWeb = context.frontend.includes("vue-router");
376395
const hasSvelteWeb = context.frontend.includes("svelte");
377396
const hasSolidWeb = context.frontend.includes("solid");
378397
const hasNativeWind = context.frontend.includes("native-nativewind");
@@ -435,7 +454,7 @@ export async function setupAuthTemplate(
435454
}
436455

437456
if (
438-
(hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb) &&
457+
(hasReactWeb || hasNuxtWeb || hasVueRouterWeb || hasSvelteWeb || hasSolidWeb) &&
439458
webAppDirExists
440459
) {
441460
if (hasReactWeb) {
@@ -474,6 +493,14 @@ export async function setupAuthTemplate(
474493
await processAndCopyFiles("**/*", authWebNuxtSrc, webAppDir, context);
475494
} else {
476495
}
496+
} else if (hasVueRouterWeb) {
497+
if (context.api !== "none") {
498+
const authWebVueSrc = path.join(PKG_ROOT, "templates/auth/web/vue");
499+
if (await fs.pathExists(authWebVueSrc)) {
500+
await processAndCopyFiles("**/*", authWebVueSrc, webAppDir, context);
501+
} else {
502+
}
503+
}
477504
} else if (hasSvelteWeb) {
478505
if (context.api === "orpc") {
479506
const authWebSvelteSrc = path.join(
@@ -606,6 +633,7 @@ export async function setupExamplesTemplate(
606633
["tanstack-router", "react-router", "tanstack-start", "next"].includes(f),
607634
);
608635
const hasNuxtWeb = context.frontend.includes("nuxt");
636+
const hasVueRouterWeb = context.frontend.includes("vue-router");
609637
const hasSvelteWeb = context.frontend.includes("svelte");
610638
const hasSolidWeb = context.frontend.includes("solid");
611639

@@ -734,6 +762,18 @@ export async function setupExamplesTemplate(
734762
);
735763
} else {
736764
}
765+
} else if (hasVueRouterWeb) {
766+
const exampleWebVueSrc = path.join(exampleBaseDir, "web/vue");
767+
if (await fs.pathExists(exampleWebVueSrc)) {
768+
await processAndCopyFiles(
769+
"**/*",
770+
exampleWebVueSrc,
771+
webAppDir,
772+
context,
773+
false,
774+
);
775+
} else {
776+
}
737777
} else if (hasSvelteWeb) {
738778
const exampleWebSvelteSrc = path.join(exampleBaseDir, "web/svelte");
739779
if (await fs.pathExists(exampleWebSvelteSrc)) {
@@ -876,6 +916,7 @@ export async function setupDeploymentTemplates(
876916
solid: "solid",
877917
next: "react/next",
878918
nuxt: "nuxt",
919+
"vue-router": "vue",
879920
svelte: "svelte",
880921
};
881922

apps/cli/src/prompts/frontend.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ export async function getFrontendChoice(
5656
label: "Nuxt",
5757
hint: "The Progressive Web Framework for Vue.js",
5858
},
59+
{
60+
value: "vue-router" as const,
61+
label: "Vue Router",
62+
hint: "The official router for Vue.js single-page applications",
63+
},
5964
{
6065
value: "svelte" as const,
6166
label: "Svelte",
@@ -75,7 +80,7 @@ export async function getFrontendChoice(
7580

7681
const webOptions = allWebOptions.filter((option) => {
7782
if (backend === "convex") {
78-
return option.value !== "nuxt" && option.value !== "solid";
83+
return option.value !== "nuxt" && option.value !== "solid" && option.value !== "vue-router";
7984
}
8085
return true;
8186
});

apps/cli/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export const FrontendSchema = z
2929
"tanstack-start",
3030
"next",
3131
"nuxt",
32+
"vue-router",
3233
"native-nativewind",
3334
"native-unistyles",
3435
"svelte",

apps/cli/src/validation.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ export function processAndValidateFlags(
135135

136136
if (webFrontends.length > 1) {
137137
consola.fatal(
138-
"Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, svelte, solid",
138+
"Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, vue-router, svelte, solid",
139139
);
140140
process.exit(1);
141141
}
@@ -205,7 +205,7 @@ export function processAndValidateFlags(
205205

206206
if (providedFlags.has("frontend") && options.frontend) {
207207
const incompatibleFrontends = options.frontend.filter(
208-
(f) => f === "nuxt" || f === "solid",
208+
(f) => f === "nuxt" || f === "solid" || f === "vue-router",
209209
);
210210
if (incompatibleFrontends.length > 0) {
211211
consola.fatal(
@@ -639,9 +639,10 @@ export function validateConfigCompatibility(
639639
process.exit(1);
640640
}
641641

642-
if (config.examples.includes("ai") && includesSolid) {
642+
const includesVueRouter = effectiveFrontend?.includes("vue-router");
643+
if (config.examples.includes("ai") && (includesSolid || includesVueRouter)) {
643644
consola.fatal(
644-
"The 'ai' example is not compatible with the Solid frontend.",
645+
`The 'ai' example is not compatible with the ${includesSolid ? "Solid" : "Vue Router"} frontend.`,
645646
);
646647
process.exit(1);
647648
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { createORPCClient, createORPCFetchHandler } from "@orpc/client";
2+
import { ref } from "vue";
3+
import type { ORPCContext, orpc } from "../../../../server/src/lib/orpc";
4+
5+
export const getAuthToken = () => {
6+
if (typeof window === "undefined") return undefined;
7+
return localStorage.getItem("auth-token") ?? undefined;
8+
};
9+
10+
export const authToken = ref(getAuthToken());
11+
12+
const fetchHandler = createORPCFetchHandler({
13+
baseURL: import.meta.env.VITE_SERVER_URL ?? "http://localhost:5000/api",
14+
headers: () => {
15+
const token = authToken.value;
16+
return token ? { Authorization: `Bearer ${token}` } : undefined;
17+
},
18+
});
19+
20+
export const { api } = createORPCClient<
21+
typeof orpc,
22+
ORPCContext
23+
>({
24+
async: fetchHandler,
25+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { createTRPCProxyClient, httpBatchLink } from "@trpc/client";
2+
import superjson from "superjson";
3+
import type { AppRouter } from "../../../../server/src/router/root";
4+
5+
export const api = createTRPCProxyClient<AppRouter>({
6+
links: [
7+
httpBatchLink({
8+
transformer: superjson,
9+
url: import.meta.env.VITE_SERVER_URL ?? "http://localhost:5000/api/trpc",
10+
async headers() {
11+
const headers: Record<string, string> = {};
12+
const token = localStorage.getItem("auth-token");
13+
if (token) {
14+
headers.authorization = `Bearer ${token}`;
15+
}
16+
return headers;
17+
},
18+
}),
19+
],
20+
});
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { ref, computed } from 'vue'
2+
import { useRouter } from 'vue-router'
3+
import { api{{#if (eq api "trpc")}}, authToken{{/if}} } from './api'
4+
5+
export interface User {
6+
id: string
7+
email: string
8+
name: string | null
9+
}
10+
11+
export interface Session {
12+
user: User
13+
expiresAt: Date
14+
}
15+
16+
const currentUser = ref<User | null>(null)
17+
const isLoading = ref(true)
18+
19+
export function useAuth() {
20+
const router = useRouter()
21+
22+
const isAuthenticated = computed(() => !!currentUser.value)
23+
24+
const initialize = async () => {
25+
try {
26+
const token = localStorage.getItem('auth-token')
27+
if (!token) {
28+
isLoading.value = false
29+
return
30+
}
31+
32+
const { data } = await api.auth.getSession{{#if (eq api "orpc")}}(){{/if}}
33+
if (data) {
34+
currentUser.value = data.user
35+
{{#if (eq api "orpc")}} authToken.value = token{{/if}}
36+
}
37+
} catch (error) {
38+
localStorage.removeItem('auth-token')
39+
{{#if (eq api "orpc")}} authToken.value = null{{/if}}
40+
} finally {
41+
isLoading.value = false
42+
}
43+
}
44+
45+
const login = async (email: string, password: string) => {
46+
const { data } = await api.auth.login{{#if (eq api "orpc")}}({email, password}){{else}}({ email, password }){{/if}}
47+
48+
if (data?.token) {
49+
localStorage.setItem('auth-token', data.token)
50+
{{#if (eq api "orpc")}} authToken.value = data.token{{/if}}
51+
currentUser.value = data.user
52+
router.push('/')
53+
}
54+
}
55+
56+
const signup = async (email: string, password: string, name: string) => {
57+
const { data } = await api.auth.signup{{#if (eq api "orpc")}}({email, password, name}){{else}}({ email, password, name }){{/if}}
58+
59+
if (data?.token) {
60+
localStorage.setItem('auth-token', data.token)
61+
{{#if (eq api "orpc")}} authToken.value = data.token{{/if}}
62+
currentUser.value = data.user
63+
router.push('/')
64+
}
65+
}
66+
67+
const logout = async () => {
68+
try {
69+
await api.auth.logout{{#if (eq api "orpc")}}(){{/if}}
70+
} catch (error) {
71+
console.error('Logout error:', error)
72+
} finally {
73+
localStorage.removeItem('auth-token')
74+
{{#if (eq api "orpc")}} authToken.value = null{{/if}}
75+
currentUser.value = null
76+
router.push('/login')
77+
}
78+
}
79+
80+
return {
81+
user: currentUser,
82+
isAuthenticated,
83+
isLoading,
84+
initialize,
85+
login,
86+
signup,
87+
logout,
88+
}
89+
}

0 commit comments

Comments
 (0)