Skip to content

Commit e17f27e

Browse files
committed
Merge branch 'vue-router-implementation'
2 parents 7d8b28b + eefd943 commit e17f27e

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)