Skip to content

Commit 81ab504

Browse files
gitbutler-clientleoisadev8
authored andcommitted
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 defa0e9 commit 81ab504

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)