Skip to content

Commit 61d2e33

Browse files
gitbutler-clientleoisadev8
authored andcommitted
feat(cli): add Vue Router support and resolve merge conflicts
1 parent defa0e9 commit 61d2e33

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
@@ -27,7 +27,7 @@ pnpm create better-t-stack@latest
2727

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

apps/cli/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Follow the prompts to configure your project or use the `--yes` flag for default
3232
| Category | Options |
3333
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
3434
| **TypeScript** | End-to-end type safety across all parts of your application |
35-
| **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 |
35+
| **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 |
3636
| **Backend** | • Hono<br>• Express<br>• Elysia<br>• Next.js API routes<br>• Convex<br>• Fastify<br>• None |
3737
| **API Layer** | • tRPC (type-safe APIs)<br>• oRPC (OpenAPI-compatible type-safe APIs)<br>• None |
3838
| **Runtime** | • Bun<br>• Node.js<br>• Cloudflare Workers<br>• None |
@@ -57,7 +57,7 @@ Options:
5757
--orm <type> ORM type (none, drizzle, prisma, mongoose)
5858
--auth Include authentication
5959
--no-auth Exclude authentication
60-
--frontend <types...> Frontend types (tanstack-router, react-router, tanstack-start, next, nuxt, svelte, solid, native-nativewind, native-unistyles, none)
60+
--frontend <types...> Frontend types (tanstack-router, react-router, tanstack-start, next, nuxt, vue-router, svelte, solid, native-nativewind, native-unistyles, none)
6161
--addons <types...> Additional addons (pwa, tauri, starlight, biome, husky, turborepo, none)
6262
--examples <types...> Examples to include (todo, ai, none)
6363
--git Initialize git repository

apps/cli/src/constants.ts

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

130130
export const ADDON_COMPATIBILITY: Record<Addons, readonly Frontend[]> = {
131-
pwa: ["tanstack-router", "react-router", "solid", "next"],
132-
tauri: ["tanstack-router", "react-router", "nuxt", "svelte", "solid"],
131+
pwa: ["tanstack-router", "react-router", "solid", "next", "vue-router"],
132+
tauri: ["tanstack-router", "react-router", "nuxt", "vue-router", "svelte", "solid"],
133133
biome: [],
134134
husky: [],
135135
turborepo: [],
@@ -147,6 +147,7 @@ export const WEB_FRAMEWORKS: readonly Frontend[] = [
147147
"tanstack-start",
148148
"next",
149149
"nuxt",
150+
"vue-router",
150151
"svelte",
151152
"solid",
152153
];

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

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

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

@@ -140,6 +141,23 @@ export async function setupFrontendTemplates(
140141
} else {
141142
}
142143
}
144+
} else if (hasVueRouterWeb) {
145+
const vueRouterBaseDir = path.join(PKG_ROOT, "templates/frontend/vue/vue-router");
146+
if (await fs.pathExists(vueRouterBaseDir)) {
147+
await processAndCopyFiles("**/*", vueRouterBaseDir, webAppDir, context);
148+
} else {
149+
}
150+
151+
if (!isConvex && context.api !== "none") {
152+
const apiWebVueDir = path.join(
153+
PKG_ROOT,
154+
`templates/api/${context.api}/web/vue`,
155+
);
156+
if (await fs.pathExists(apiWebVueDir)) {
157+
await processAndCopyFiles("**/*", apiWebVueDir, webAppDir, context);
158+
} else {
159+
}
160+
}
143161
} else if (hasSvelteWeb) {
144162
const svelteBaseDir = path.join(PKG_ROOT, "templates/frontend/svelte");
145163
if (await fs.pathExists(svelteBaseDir)) {
@@ -372,6 +390,7 @@ export async function setupAuthTemplate(
372390
["tanstack-router", "react-router", "tanstack-start", "next"].includes(f),
373391
);
374392
const hasNuxtWeb = context.frontend.includes("nuxt");
393+
const hasVueRouterWeb = context.frontend.includes("vue-router");
375394
const hasSvelteWeb = context.frontend.includes("svelte");
376395
const hasSolidWeb = context.frontend.includes("solid");
377396
const hasNativeWind = context.frontend.includes("native-nativewind");
@@ -434,7 +453,7 @@ export async function setupAuthTemplate(
434453
}
435454

436455
if (
437-
(hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb) &&
456+
(hasReactWeb || hasNuxtWeb || hasVueRouterWeb || hasSvelteWeb || hasSolidWeb) &&
438457
webAppDirExists
439458
) {
440459
if (hasReactWeb) {
@@ -473,6 +492,14 @@ export async function setupAuthTemplate(
473492
await processAndCopyFiles("**/*", authWebNuxtSrc, webAppDir, context);
474493
} else {
475494
}
495+
} else if (hasVueRouterWeb) {
496+
if (context.api !== "none") {
497+
const authWebVueSrc = path.join(PKG_ROOT, "templates/auth/web/vue");
498+
if (await fs.pathExists(authWebVueSrc)) {
499+
await processAndCopyFiles("**/*", authWebVueSrc, webAppDir, context);
500+
} else {
501+
}
502+
}
476503
} else if (hasSvelteWeb) {
477504
const authWebSvelteSrc = path.join(PKG_ROOT, "templates/auth/web/svelte");
478505
if (await fs.pathExists(authWebSvelteSrc)) {
@@ -588,6 +615,7 @@ export async function setupExamplesTemplate(
588615
["tanstack-router", "react-router", "tanstack-start", "next"].includes(f),
589616
);
590617
const hasNuxtWeb = context.frontend.includes("nuxt");
618+
const hasVueRouterWeb = context.frontend.includes("vue-router");
591619
const hasSvelteWeb = context.frontend.includes("svelte");
592620
const hasSolidWeb = context.frontend.includes("solid");
593621

@@ -716,6 +744,18 @@ export async function setupExamplesTemplate(
716744
);
717745
} else {
718746
}
747+
} else if (hasVueRouterWeb) {
748+
const exampleWebVueSrc = path.join(exampleBaseDir, "web/vue");
749+
if (await fs.pathExists(exampleWebVueSrc)) {
750+
await processAndCopyFiles(
751+
"**/*",
752+
exampleWebVueSrc,
753+
webAppDir,
754+
context,
755+
false,
756+
);
757+
} else {
758+
}
719759
} else if (hasSvelteWeb) {
720760
const exampleWebSvelteSrc = path.join(exampleBaseDir, "web/svelte");
721761
if (await fs.pathExists(exampleWebSvelteSrc)) {
@@ -863,6 +903,7 @@ export async function setupDeploymentTemplates(
863903
solid: "solid",
864904
next: "react/next",
865905
nuxt: "nuxt",
906+
"vue-router": "vue",
866907
svelte: "svelte",
867908
};
868909

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 !== "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 === "solid",
208+
(f) => f === "nuxt" || f === "solid" || f === "vue-router",
209209
);
210210
if (incompatibleFrontends.length > 0) {
211211
consola.fatal(
@@ -632,9 +632,10 @@ export function validateConfigCompatibility(config: Partial<ProjectConfig>) {
632632
process.exit(1);
633633
}
634634

635-
if (config.examples.includes("ai") && includesSolid) {
635+
const includesVueRouter = effectiveFrontend?.includes("vue-router");
636+
if (config.examples.includes("ai") && (includesSolid || includesVueRouter)) {
636637
consola.fatal(
637-
"The 'ai' example is not compatible with the Solid frontend.",
638+
`The 'ai' example is not compatible with the ${includesSolid ? "Solid" : "Vue Router"} frontend.`,
638639
);
639640
process.exit(1);
640641
}
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)