feat: added mvp of mpa template 060b2c84
Steve · 2025-10-04 15:40 26 file(s) · +1215 −3
src/index.ts +1 −1
29 29
	)
30 30
	.option(
31 31
		"--router <router>",
32 -
		"specify a client router (none, reactrouter, tanstackrouter)",
32 +
		"specify a client router (none, reactrouter, reactroutermpa, tanstackrouter)",
33 33
	)
34 34
	.option("--linter <linter>", "specify the linter to use (eslint or biome)")
35 35
	.action(create);
src/installers/react-router-mpa.ts (added) +186 −0
1 +
import path from "node:path";
2 +
import fs from "fs-extra";
3 +
import type { ProjectOptions } from "@/types";
4 +
import yoctoSpinner from "yocto-spinner";
5 +
import pc from "picocolors";
6 +
import { consola } from "consola";
7 +
import { addPackageDependency } from "@/utils/add-package-dependency";
8 +
import { EXTRAS_DIR } from "@/utils";
9 +
import { nameGenerator } from "@/utils/name-generator";
10 +
11 +
export const reactRouterMpaInstaller = async (
12 +
	options: Required<ProjectOptions>,
13 +
): Promise<boolean> => {
14 +
	const spinner = yoctoSpinner({
15 +
		text: "Setting up React Router MPA...",
16 +
	}).start();
17 +
18 +
	try {
19 +
		const { projectName, rpc, shadcn, tailwind, tanstackQuery } = options;
20 +
21 +
		const projectPath = path.resolve(process.cwd(), projectName);
22 +
23 +
		spinner.text = "Installing React Router MPA dependencies...";
24 +
		await addPackageDependency({
25 +
			dependencies: [
26 +
				"react-router",
27 +
				"@react-router/dev",
28 +
				"@react-router/node",
29 +
				"@react-router/serve",
30 +
				"vite-tsconfig-paths",
31 +
				"isbot",
32 +
			],
33 +
			target: "client",
34 +
			projectName,
35 +
		});
36 +
37 +
		// Copy root.tsx
38 +
		const rootTsxSrc = path.join(
39 +
			EXTRAS_DIR,
40 +
			"client",
41 +
			"src",
42 +
			"root.tsx",
43 +
			"root.tsx",
44 +
		);
45 +
		const rootTsxTarget = path.join(projectPath, "client", "src", "root.tsx");
46 +
		fs.copySync(rootTsxSrc, rootTsxTarget);
47 +
48 +
		// Copy routes.ts
49 +
		const routesTsSrc = path.join(
50 +
			EXTRAS_DIR,
51 +
			"client",
52 +
			"src",
53 +
			"routes.ts",
54 +
			"routes.ts",
55 +
		);
56 +
		const routesTsTarget = path.join(projectPath, "client", "src", "routes.ts");
57 +
		fs.copySync(routesTsSrc, routesTsTarget);
58 +
59 +
		// Create routes directory and copy home.tsx
60 +
		const routesDir = path.join(projectPath, "client", "src", "routes");
61 +
		fs.ensureDirSync(routesDir);
62 +
63 +
		const homeTsxSrc = path.join(
64 +
			EXTRAS_DIR,
65 +
			"client",
66 +
			"src",
67 +
			"routes",
68 +
			"home.tsx",
69 +
			"home.tsx",
70 +
		);
71 +
		const homeTsxTarget = path.join(routesDir, "home.tsx");
72 +
		fs.copySync(homeTsxSrc, homeTsxTarget);
73 +
74 +
		// Copy ClientOnly component
75 +
		const clientOnlySrc = path.join(
76 +
			EXTRAS_DIR,
77 +
			"client",
78 +
			"src",
79 +
			"components",
80 +
			"ClientOnly.tsx",
81 +
			"ClientOnly.tsx",
82 +
		);
83 +
		const clientOnlyTarget = path.join(
84 +
			projectPath,
85 +
			"client",
86 +
			"src",
87 +
			"components",
88 +
			"ClientOnly.tsx",
89 +
		);
90 +
		fs.copySync(clientOnlySrc, clientOnlyTarget);
91 +
92 +
		// Update Home component with ClientOnly wrapper
93 +
		const homeTsxTemplate = nameGenerator("Home.tsx", {
94 +
			reactroutermpa: true,
95 +
			rpc,
96 +
			shadcn,
97 +
			tailwind,
98 +
			tanstackQuery,
99 +
		});
100 +
101 +
		const homeComponentSrc = path.join(
102 +
			EXTRAS_DIR,
103 +
			"client",
104 +
			"src",
105 +
			"components",
106 +
			"Home.tsx",
107 +
			homeTsxTemplate,
108 +
		);
109 +
		const homeComponentTarget = path.join(
110 +
			projectPath,
111 +
			"client",
112 +
			"src",
113 +
			"components",
114 +
			"Home.tsx",
115 +
		);
116 +
		fs.copySync(homeComponentSrc, homeComponentTarget);
117 +
118 +
		// Copy react-router.config.ts
119 +
		const reactRouterConfigSrc = path.join(
120 +
			EXTRAS_DIR,
121 +
			"client",
122 +
			"react-router.config.ts",
123 +
			"react-router.config.ts",
124 +
		);
125 +
		const reactRouterConfigTarget = path.join(
126 +
			projectPath,
127 +
			"client",
128 +
			"react-router.config.ts",
129 +
		);
130 +
		fs.copySync(reactRouterConfigSrc, reactRouterConfigTarget);
131 +
132 +
		// Copy tsconfig.app.json
133 +
		const tsconfigAppSrc = path.join(
134 +
			EXTRAS_DIR,
135 +
			"client",
136 +
			"tsconfig.app.json",
137 +
			"tsconfig.app.json",
138 +
		);
139 +
		const tsconfigAppTarget = path.join(
140 +
			projectPath,
141 +
			"client",
142 +
			"tsconfig.app.json",
143 +
		);
144 +
		fs.copySync(tsconfigAppSrc, tsconfigAppTarget);
145 +
146 +
		// Update vite.config.ts
147 +
		const viteConfigTemplate = nameGenerator("vite.config.ts", {
148 +
			reactroutermpa: true,
149 +
			shadcn,
150 +
			tailwind,
151 +
		});
152 +
153 +
		const viteConfigSrc = path.join(
154 +
			EXTRAS_DIR,
155 +
			"client",
156 +
			"vite.config.ts",
157 +
			viteConfigTemplate,
158 +
		);
159 +
		const viteConfigTarget = path.join(projectPath, "client", "vite.config.ts");
160 +
		fs.copySync(viteConfigSrc, viteConfigTarget);
161 +
162 +
		// Update package.json scripts
163 +
		const packageJsonPath = path.join(projectPath, "client", "package.json");
164 +
		const packageJson = fs.readJsonSync(packageJsonPath);
165 +
166 +
		packageJson.scripts = {
167 +
			...packageJson.scripts,
168 +
			dev: "react-router dev",
169 +
			build: "react-router typegen && tsc -b && react-router build",
170 +
			typecheck: "react-router typegen && tsc",
171 +
		};
172 +
173 +
		fs.writeJsonSync(packageJsonPath, packageJson, { spaces: 2 });
174 +
175 +
		spinner.success("React Router MPA setup completed");
176 +
		return true;
177 +
	} catch (err: unknown) {
178 +
		spinner.error("Failed to set up React Router MPA");
179 +
		if (err instanceof Error) {
180 +
			consola.error(pc.red("Error:"), err.message);
181 +
		} else {
182 +
			consola.error(pc.red("Error: Unknown error"));
183 +
		}
184 +
		return false;
185 +
	}
186 +
};
src/lib/install-packages.ts +5 −0
4 4
import { tanstackQueryInstaller } from "@/installers/tanstack-query";
5 5
import { rpcInstaller } from "@/installers/rpc";
6 6
import { reactRouterInstaller } from "@/installers/react-router";
7 +
import { reactRouterMpaInstaller } from "@/installers/react-router-mpa";
7 8
import { tanstackRouterInstaller } from "@/installers/tanstack-router";
8 9
9 10
export async function installPackages(
29 30
		switch (router) {
30 31
			case "reactrouter": {
31 32
				await reactRouterInstaller(options);
33 +
				break;
34 +
			}
35 +
			case "reactroutermpa": {
36 +
				await reactRouterMpaInstaller(options);
32 37
				break;
33 38
			}
34 39
			case "tanstackrouter": {
src/lib/prompt-for-options.ts +6 −1
102 102
				options: [
103 103
					{ label: "None (default)", value: "none" },
104 104
					{ label: "React Router", value: "reactrouter" },
105 +
					{ label: "React Router MPA", value: "reactroutermpa" },
105 106
					{ label: "TanStack Router", value: "tanstackrouter" },
106 107
				],
107 108
				initial: "none",
114 115
			process.exit(1);
115 116
		}
116 117
117 -
		router = routerResponse as "none" | "reactrouter" | "tanstackrouter";
118 +
		router = routerResponse as
119 +
			| "none"
120 +
			| "reactrouter"
121 +
			| "reactroutermpa"
122 +
			| "tanstackrouter";
118 123
	}
119 124
120 125
	let useTanstackQuery = options.tanstackQuery;
src/templates/extras/client/react-router.config.ts/react-router.config.ts (added) +10 −0
1 +
import type { Config } from "@react-router/dev/config";
2 +
3 +
export default {
4 +
	buildDirectory: "dist",
5 +
	appDirectory: "src",
6 +
	ssr: false,
7 +
	async prerender() {
8 +
		return ["/"];
9 +
	},
10 +
} satisfies Config;
src/templates/extras/client/src/components/ClientOnly.tsx/ClientOnly.tsx (added) +22 −0
1 +
import {
2 +
	useEffect,
3 +
	useState,
4 +
	type PropsWithChildren,
5 +
	type ReactNode,
6 +
} from "react";
7 +
8 +
type ClientOnlyProps = PropsWithChildren & {
9 +
	fallback?: ReactNode;
10 +
};
11 +
12 +
const ClientOnly = ({ children, fallback }: ClientOnlyProps) => {
13 +
	const [isClient, setIsClient] = useState(false);
14 +
15 +
	useEffect(() => {
16 +
		setIsClient(true);
17 +
	}, []);
18 +
19 +
	return isClient ? children : fallback;
20 +
};
21 +
22 +
export default ClientOnly;
src/templates/extras/client/src/components/Home.tsx/Home-with-reactroutermpa-rpc-shadcn-tailwind-tanstackquery.tsx (added) +73 −0
1 +
import { useState } from "react";
2 +
import beaver from "@/assets/beaver.svg";
3 +
import { Button } from "@/components/ui/button";
4 +
import { hcWithType } from "server/dist/client";
5 +
import { useMutation } from "@tanstack/react-query";
6 +
import ClientOnly from "./ClientOnly";
7 +
8 +
const SERVER_URL = import.meta.env.DEV ? "http://localhost:3000" : "/api";
9 +
10 +
const client = hcWithType(SERVER_URL);
11 +
12 +
type ResponseType = Awaited<ReturnType<typeof client.hello.$get>>;
13 +
14 +
function Home() {
15 +
	const [data, setData] = useState<
16 +
		Awaited<ReturnType<ResponseType["json"]>> | undefined
17 +
	>();
18 +
19 +
	const { mutate: sendRequest } = useMutation({
20 +
		mutationFn: async () => {
21 +
			try {
22 +
				const res = await client.hello.$get();
23 +
				if (!res.ok) {
24 +
					console.log("Error fetching data");
25 +
					return;
26 +
				}
27 +
				const data = await res.json();
28 +
				setData(data);
29 +
			} catch (error) {
30 +
				console.log(error);
31 +
			}
32 +
		},
33 +
	});
34 +
35 +
	return (
36 +
		<div className="max-w-xl mx-auto flex flex-col gap-6 items-center justify-center min-h-screen">
37 +
			<a
38 +
				href="https://github.com/stevedylandev/bhvr"
39 +
				target="_blank"
40 +
				rel="noopener"
41 +
			>
42 +
				<img
43 +
					src={beaver}
44 +
					className="w-16 h-16 cursor-pointer"
45 +
					alt="beaver logo"
46 +
				/>
47 +
			</a>
48 +
			<h1 className="text-5xl font-black">bhvr</h1>
49 +
			<h2 className="text-2xl font-bold">Bun + Hono + Vite + React</h2>
50 +
			<p>A typesafe fullstack monorepo</p>
51 +
			<ClientOnly>
52 +
				<div className="flex items-center gap-4">
53 +
					<Button onClick={() => sendRequest()}>Call API</Button>
54 +
					<Button variant="secondary" asChild>
55 +
						<a target="_blank" href="https://bhvr.dev" rel="noopener">
56 +
							Docs
57 +
						</a>
58 +
					</Button>
59 +
				</div>
60 +
				{data && (
61 +
					<pre className="bg-gray-100 p-4 rounded-md">
62 +
						<code>
63 +
							Message: {data.message} <br />
64 +
							Success: {data.success.toString()}
65 +
						</code>
66 +
					</pre>
67 +
				)}
68 +
			</ClientOnly>
69 +
		</div>
70 +
	);
71 +
}
72 +
73 +
export default Home;
src/templates/extras/client/src/components/Home.tsx/Home-with-reactroutermpa-rpc-shadcn-tailwind.tsx (added) +70 −0
1 +
import { useState } from "react";
2 +
import beaver from "@/assets/beaver.svg";
3 +
import { Button } from "@/components/ui/button";
4 +
import { hcWithType } from "server/dist/client";
5 +
import ClientOnly from "./ClientOnly";
6 +
7 +
const SERVER_URL = import.meta.env.DEV ? "http://localhost:3000" : "/api";
8 +
9 +
const client = hcWithType(SERVER_URL);
10 +
11 +
type ResponseType = Awaited<ReturnType<typeof client.hello.$get>>;
12 +
13 +
function Home() {
14 +
	const [data, setData] = useState<
15 +
		Awaited<ReturnType<ResponseType["json"]>> | undefined
16 +
	>();
17 +
18 +
	async function sendRequest() {
19 +
		try {
20 +
			const res = await client.hello.$get();
21 +
			if (!res.ok) {
22 +
				console.log("Error fetching data");
23 +
				return;
24 +
			}
25 +
			const data = await res.json();
26 +
			setData(data);
27 +
		} catch (error) {
28 +
			console.log(error);
29 +
		}
30 +
	}
31 +
32 +
	return (
33 +
		<div className="max-w-xl mx-auto flex flex-col gap-6 items-center justify-center min-h-screen">
34 +
			<a
35 +
				href="https://github.com/stevedylandev/bhvr"
36 +
				target="_blank"
37 +
				rel="noopener"
38 +
			>
39 +
				<img
40 +
					src={beaver}
41 +
					className="w-16 h-16 cursor-pointer"
42 +
					alt="beaver logo"
43 +
				/>
44 +
			</a>
45 +
			<h1 className="text-5xl font-black">bhvr</h1>
46 +
			<h2 className="text-2xl font-bold">Bun + Hono + Vite + React</h2>
47 +
			<p>A typesafe fullstack monorepo</p>
48 +
			<ClientOnly>
49 +
				<div className="flex items-center gap-4">
50 +
					<Button onClick={sendRequest}>Call API</Button>
51 +
					<Button variant="secondary" asChild>
52 +
						<a target="_blank" href="https://bhvr.dev" rel="noopener">
53 +
							Docs
54 +
						</a>
55 +
					</Button>
56 +
				</div>
57 +
				{data && (
58 +
					<pre className="bg-gray-100 p-4 rounded-md">
59 +
						<code>
60 +
							Message: {data.message} <br />
61 +
							Success: {data.success.toString()}
62 +
						</code>
63 +
					</pre>
64 +
				)}
65 +
			</ClientOnly>
66 +
		</div>
67 +
	);
68 +
}
69 +
70 +
export default Home;
src/templates/extras/client/src/components/Home.tsx/Home-with-reactroutermpa-rpc-tailwind-tanstackquery.tsx (added) +81 −0
1 +
import { useState } from "react";
2 +
import beaver from "../assets/beaver.svg";
3 +
import { hcWithType } from "server/dist/client";
4 +
import { useMutation } from "@tanstack/react-query";
5 +
import ClientOnly from "./ClientOnly";
6 +
7 +
const SERVER_URL = import.meta.env.DEV ? "http://localhost:3000" : "/api";
8 +
9 +
const client = hcWithType(SERVER_URL);
10 +
11 +
type ResponseType = Awaited<ReturnType<typeof client.hello.$get>>;
12 +
13 +
function Home() {
14 +
	const [data, setData] = useState<
15 +
		Awaited<ReturnType<ResponseType["json"]>> | undefined
16 +
	>();
17 +
18 +
	const { mutate: sendRequest } = useMutation({
19 +
		mutationFn: async () => {
20 +
			try {
21 +
				const res = await client.hello.$get();
22 +
				if (!res.ok) {
23 +
					console.log("Error fetching data");
24 +
					return;
25 +
				}
26 +
				const data = await res.json();
27 +
				setData(data);
28 +
			} catch (error) {
29 +
				console.log(error);
30 +
			}
31 +
		},
32 +
	});
33 +
34 +
	return (
35 +
		<div className="max-w-xl mx-auto flex flex-col gap-6 items-center justify-center min-h-screen">
36 +
			<a
37 +
				href="https://github.com/stevedylandev/bhvr"
38 +
				target="_blank"
39 +
				rel="noopener"
40 +
			>
41 +
				<img
42 +
					src={beaver}
43 +
					className="w-16 h-16 cursor-pointer"
44 +
					alt="beaver logo"
45 +
				/>
46 +
			</a>
47 +
			<h1 className="text-5xl font-black">bhvr</h1>
48 +
			<h2 className="text-2xl font-bold">Bun + Hono + Vite + React</h2>
49 +
			<p>A typesafe fullstack monorepo</p>
50 +
			<ClientOnly>
51 +
				<div className="flex items-center gap-4">
52 +
					<button
53 +
						type="button"
54 +
						onClick={() => sendRequest()}
55 +
						className="bg-black text-white px-2.5 py-1.5 rounded-md"
56 +
					>
57 +
						Call API
58 +
					</button>
59 +
					<a
60 +
						target="_blank"
61 +
						href="https://bhvr.dev"
62 +
						className="border-1 border-black text-black px-2.5 py-1.5 rounded-md"
63 +
						rel="noopener"
64 +
					>
65 +
						Docs
66 +
					</a>
67 +
				</div>
68 +
				{data && (
69 +
					<pre className="bg-gray-100 p-4 rounded-md">
70 +
						<code>
71 +
							Message: {data.message} <br />
72 +
							Success: {data.success.toString()}
73 +
						</code>
74 +
					</pre>
75 +
				)}
76 +
			</ClientOnly>
77 +
		</div>
78 +
	);
79 +
}
80 +
81 +
export default Home;
src/templates/extras/client/src/components/Home.tsx/Home-with-reactroutermpa-rpc-tailwind.tsx (added) +78 −0
1 +
import { useState } from "react";
2 +
import beaver from "../assets/beaver.svg";
3 +
import { hcWithType } from "server/dist/client";
4 +
import ClientOnly from "./ClientOnly";
5 +
6 +
const SERVER_URL = import.meta.env.DEV ? "http://localhost:3000" : "/api";
7 +
8 +
const client = hcWithType(SERVER_URL);
9 +
10 +
type ResponseType = Awaited<ReturnType<typeof client.hello.$get>>;
11 +
12 +
function Home() {
13 +
	const [data, setData] = useState<
14 +
		Awaited<ReturnType<ResponseType["json"]>> | undefined
15 +
	>();
16 +
17 +
	async function sendRequest() {
18 +
		try {
19 +
			const res = await client.hello.$get();
20 +
			if (!res.ok) {
21 +
				console.log("Error fetching data");
22 +
				return;
23 +
			}
24 +
			const data = await res.json();
25 +
			setData(data);
26 +
		} catch (error) {
27 +
			console.log(error);
28 +
		}
29 +
	}
30 +
31 +
	return (
32 +
		<div className="max-w-xl mx-auto flex flex-col gap-6 items-center justify-center min-h-screen">
33 +
			<a
34 +
				href="https://github.com/stevedylandev/bhvr"
35 +
				target="_blank"
36 +
				rel="noopener"
37 +
			>
38 +
				<img
39 +
					src={beaver}
40 +
					className="w-16 h-16 cursor-pointer"
41 +
					alt="beaver logo"
42 +
				/>
43 +
			</a>
44 +
			<h1 className="text-5xl font-black">bhvr</h1>
45 +
			<h2 className="text-2xl font-bold">Bun + Hono + Vite + React</h2>
46 +
			<p>A typesafe fullstack monorepo</p>
47 +
			<ClientOnly>
48 +
				<div className="flex items-center gap-4">
49 +
					<button
50 +
						type="button"
51 +
						onClick={sendRequest}
52 +
						className="bg-black text-white px-2.5 py-1.5 rounded-md"
53 +
					>
54 +
						Call API
55 +
					</button>
56 +
					<a
57 +
						target="_blank"
58 +
						href="https://bhvr.dev"
59 +
						className="border-1 border-black text-black px-2.5 py-1.5 rounded-md"
60 +
						rel="noopener"
61 +
					>
62 +
						Docs
63 +
					</a>
64 +
				</div>
65 +
				{data && (
66 +
					<pre className="bg-gray-100 p-4 rounded-md">
67 +
						<code>
68 +
							Message: {data.message} <br />
69 +
							Success: {data.success.toString()}
70 +
						</code>
71 +
					</pre>
72 +
				)}
73 +
			</ClientOnly>
74 +
		</div>
75 +
	);
76 +
}
77 +
78 +
export default Home;
src/templates/extras/client/src/components/Home.tsx/Home-with-reactroutermpa-rpc-tanstackquery.tsx (added) +78 −0
1 +
import { useState } from "react";
2 +
import beaver from "../assets/beaver.svg";
3 +
import { hcWithType } from "server/dist/client";
4 +
import { useMutation } from "@tanstack/react-query";
5 +
import "../App.css";
6 +
import ClientOnly from "./ClientOnly";
7 +
8 +
const SERVER_URL = import.meta.env.DEV ? "http://localhost:3000" : "/api";
9 +
10 +
const client = hcWithType(SERVER_URL);
11 +
12 +
type ResponseType = Awaited<ReturnType<typeof client.hello.$get>>;
13 +
14 +
function Home() {
15 +
	const [data, setData] = useState<
16 +
		Awaited<ReturnType<ResponseType["json"]>> | undefined
17 +
	>();
18 +
19 +
	const { mutate: sendRequest } = useMutation({
20 +
		mutationFn: async () => {
21 +
			try {
22 +
				const res = await client.hello.$get();
23 +
				if (!res.ok) {
24 +
					console.log("Error fetching data");
25 +
					return;
26 +
				}
27 +
				const data = await res.json();
28 +
				setData(data);
29 +
			} catch (error) {
30 +
				console.log(error);
31 +
			}
32 +
		},
33 +
	});
34 +
35 +
	return (
36 +
		<>
37 +
			<div>
38 +
				<a
39 +
					href="https://github.com/stevedylandev/bhvr"
40 +
					target="_blank"
41 +
					rel="noopener"
42 +
				>
43 +
					<img src={beaver} className="logo" alt="beaver logo" />
44 +
				</a>
45 +
			</div>
46 +
			<h1>bhvr</h1>
47 +
			<h2>Bun + Hono + Vite + React</h2>
48 +
			<p>A typesafe fullstack monorepo</p>
49 +
			<ClientOnly>
50 +
				<div className="card">
51 +
					<div className="button-container">
52 +
						<button type="button" onClick={() => sendRequest()}>
53 +
							Call API
54 +
						</button>
55 +
						<a
56 +
							className="docs-link"
57 +
							target="_blank"
58 +
							href="https://bhvr.dev"
59 +
							rel="noopener"
60 +
						>
61 +
							Docs
62 +
						</a>
63 +
					</div>
64 +
					{data && (
65 +
						<pre className="response">
66 +
							<code>
67 +
								Message: {data.message} <br />
68 +
								Success: {data.success.toString()}
69 +
							</code>
70 +
						</pre>
71 +
					)}
72 +
				</div>
73 +
			</ClientOnly>
74 +
		</>
75 +
	);
76 +
}
77 +
78 +
export default Home;
src/templates/extras/client/src/components/Home.tsx/Home-with-reactroutermpa-rpc.tsx (added) +75 −0
1 +
import { useState } from "react";
2 +
import beaver from "../assets/beaver.svg";
3 +
import { hcWithType } from "server/dist/client";
4 +
import "../App.css";
5 +
import ClientOnly from "./ClientOnly";
6 +
7 +
const SERVER_URL = import.meta.env.DEV ? "http://localhost:3000" : "/api";
8 +
9 +
const client = hcWithType(SERVER_URL);
10 +
11 +
type ResponseType = Awaited<ReturnType<typeof client.hello.$get>>;
12 +
13 +
function Home() {
14 +
	const [data, setData] = useState<
15 +
		Awaited<ReturnType<ResponseType["json"]>> | undefined
16 +
	>();
17 +
18 +
	async function sendRequest() {
19 +
		try {
20 +
			const res = await client.hello.$get();
21 +
			if (!res.ok) {
22 +
				console.log("Error fetching data");
23 +
				return;
24 +
			}
25 +
			const data = await res.json();
26 +
			setData(data);
27 +
		} catch (error) {
28 +
			console.log(error);
29 +
		}
30 +
	}
31 +
32 +
	return (
33 +
		<>
34 +
			<div>
35 +
				<a
36 +
					href="https://github.com/stevedylandev/bhvr"
37 +
					target="_blank"
38 +
					rel="noopener"
39 +
				>
40 +
					<img src={beaver} className="logo" alt="beaver logo" />
41 +
				</a>
42 +
			</div>
43 +
			<h1>bhvr</h1>
44 +
			<h2>Bun + Hono + Vite + React</h2>
45 +
			<p>A typesafe fullstack monorepo</p>
46 +
			<ClientOnly>
47 +
				<div className="card">
48 +
					<div className="button-container">
49 +
						<button type="button" onClick={sendRequest}>
50 +
							Call API
51 +
						</button>
52 +
						<a
53 +
							className="docs-link"
54 +
							target="_blank"
55 +
							href="https://bhvr.dev"
56 +
							rel="noopener"
57 +
						>
58 +
							Docs
59 +
						</a>
60 +
					</div>
61 +
					{data && (
62 +
						<pre className="response">
63 +
							<code>
64 +
								Message: {data.message} <br />
65 +
								Success: {data.success.toString()}
66 +
							</code>
67 +
						</pre>
68 +
					)}
69 +
				</div>
70 +
			</ClientOnly>
71 +
		</>
72 +
	);
73 +
}
74 +
75 +
export default Home;
src/templates/extras/client/src/components/Home.tsx/Home-with-reactroutermpa-shadcn-tailwind-tanstackquery.tsx (added) +63 −0
1 +
import { useState } from "react";
2 +
import beaver from "@/assets/beaver.svg";
3 +
import type { ApiResponse } from "shared";
4 +
import { Button } from "@/components/ui/button";
5 +
import { useMutation } from "@tanstack/react-query";
6 +
import ClientOnly from "./ClientOnly";
7 +
8 +
const SERVER_URL = import.meta.env.DEV ? "http://localhost:3000" : "/api";
9 +
10 +
function Home() {
11 +
	const [data, setData] = useState<ApiResponse | undefined>();
12 +
13 +
	const { mutate: sendRequest } = useMutation({
14 +
		mutationFn: async () => {
15 +
			try {
16 +
				const req = await fetch(`${SERVER_URL}/hello`);
17 +
				const res: ApiResponse = await req.json();
18 +
				setData(res);
19 +
			} catch (error) {
20 +
				console.log(error);
21 +
			}
22 +
		},
23 +
	});
24 +
25 +
	return (
26 +
		<div className="max-w-xl mx-auto flex flex-col gap-6 items-center justify-center min-h-screen">
27 +
			<a
28 +
				href="https://github.com/stevedylandev/bhvr"
29 +
				target="_blank"
30 +
				rel="noopener"
31 +
			>
32 +
				<img
33 +
					src={beaver}
34 +
					className="w-16 h-16 cursor-pointer"
35 +
					alt="beaver logo"
36 +
				/>
37 +
			</a>
38 +
			<h1 className="text-5xl font-black">bhvr</h1>
39 +
			<h2 className="text-2xl font-bold">Bun + Hono + Vite + React</h2>
40 +
			<p>A typesafe fullstack monorepo</p>
41 +
			<ClientOnly>
42 +
				<div className="flex items-center gap-4">
43 +
					<Button onClick={() => sendRequest()}>Call API</Button>
44 +
					<Button variant="secondary" asChild>
45 +
						<a target="_blank" href="https://bhvr.dev" rel="noopener">
46 +
							Docs
47 +
						</a>
48 +
					</Button>
49 +
				</div>
50 +
				{data && (
51 +
					<pre className="bg-gray-100 p-4 rounded-md">
52 +
						<code>
53 +
							Message: {data.message} <br />
54 +
							Success: {data.success.toString()}
55 +
						</code>
56 +
					</pre>
57 +
				)}
58 +
			</ClientOnly>
59 +
		</div>
60 +
	);
61 +
}
62 +
63 +
export default Home;
src/templates/extras/client/src/components/Home.tsx/Home-with-reactroutermpa-shadcn-tailwind.tsx (added) +60 −0
1 +
import { useState } from "react";
2 +
import beaver from "@/assets/beaver.svg";
3 +
import type { ApiResponse } from "shared";
4 +
import { Button } from "@/components/ui/button";
5 +
import ClientOnly from "./ClientOnly";
6 +
7 +
const SERVER_URL = import.meta.env.DEV ? "http://localhost:3000" : "/api";
8 +
9 +
function Home() {
10 +
	const [data, setData] = useState<ApiResponse | undefined>();
11 +
12 +
	async function sendRequest() {
13 +
		try {
14 +
			const req = await fetch(`${SERVER_URL}/hello`);
15 +
			const res: ApiResponse = await req.json();
16 +
			setData(res);
17 +
		} catch (error) {
18 +
			console.log(error);
19 +
		}
20 +
	}
21 +
22 +
	return (
23 +
		<div className="max-w-xl mx-auto flex flex-col gap-6 items-center justify-center min-h-screen">
24 +
			<a
25 +
				href="https://github.com/stevedylandev/bhvr"
26 +
				target="_blank"
27 +
				rel="noopener"
28 +
			>
29 +
				<img
30 +
					src={beaver}
31 +
					className="w-16 h-16 cursor-pointer"
32 +
					alt="beaver logo"
33 +
				/>
34 +
			</a>
35 +
			<h1 className="text-5xl font-black">bhvr</h1>
36 +
			<h2 className="text-2xl font-bold">Bun + Hono + Vite + React</h2>
37 +
			<p>A typesafe fullstack monorepo</p>
38 +
			<ClientOnly>
39 +
				<div className="flex items-center gap-4">
40 +
					<Button onClick={sendRequest}>Call API</Button>
41 +
					<Button variant="secondary" asChild>
42 +
						<a target="_blank" href="https://bhvr.dev" rel="noopener">
43 +
							Docs
44 +
						</a>
45 +
					</Button>
46 +
				</div>
47 +
				{data && (
48 +
					<pre className="bg-gray-100 p-4 rounded-md">
49 +
						<code>
50 +
							Message: {data.message} <br />
51 +
							Success: {data.success.toString()}
52 +
						</code>
53 +
					</pre>
54 +
				)}
55 +
			</ClientOnly>
56 +
		</div>
57 +
	);
58 +
}
59 +
60 +
export default Home;
src/templates/extras/client/src/components/Home.tsx/Home-with-reactroutermpa-tailwind-tanstackquery.tsx (added) +71 −0
1 +
import { useState } from "react";
2 +
import beaver from "../assets/beaver.svg";
3 +
import type { ApiResponse } from "shared";
4 +
import { useMutation } from "@tanstack/react-query";
5 +
import ClientOnly from "./ClientOnly";
6 +
7 +
const SERVER_URL = import.meta.env.DEV ? "http://localhost:3000" : "/api";
8 +
9 +
function Home() {
10 +
	const [data, setData] = useState<ApiResponse | undefined>();
11 +
12 +
	const { mutate: sendRequest } = useMutation({
13 +
		mutationFn: async () => {
14 +
			try {
15 +
				const req = await fetch(`${SERVER_URL}/hello`);
16 +
				const res: ApiResponse = await req.json();
17 +
				setData(res);
18 +
			} catch (error) {
19 +
				console.log(error);
20 +
			}
21 +
		},
22 +
	});
23 +
24 +
	return (
25 +
		<div className="max-w-xl mx-auto flex flex-col gap-6 items-center justify-center min-h-screen">
26 +
			<a
27 +
				href="https://github.com/stevedylandev/bhvr"
28 +
				target="_blank"
29 +
				rel="noopener"
30 +
			>
31 +
				<img
32 +
					src={beaver}
33 +
					className="w-16 h-16 cursor-pointer"
34 +
					alt="beaver logo"
35 +
				/>
36 +
			</a>
37 +
			<h1 className="text-5xl font-black">bhvr</h1>
38 +
			<h2 className="text-2xl font-bold">Bun + Hono + Vite + React</h2>
39 +
			<p>A typesafe fullstack monorepo</p>
40 +
			<ClientOnly>
41 +
				<div className="flex items-center gap-4">
42 +
					<button
43 +
						type="button"
44 +
						onClick={() => sendRequest()}
45 +
						className="bg-black text-white px-2.5 py-1.5 rounded-md"
46 +
					>
47 +
						Call API
48 +
					</button>
49 +
					<a
50 +
						target="_blank"
51 +
						href="https://bhvr.dev"
52 +
						className="border-1 border-black text-black px-2.5 py-1.5 rounded-md"
53 +
						rel="noopener"
54 +
					>
55 +
						Docs
56 +
					</a>
57 +
				</div>
58 +
				{data && (
59 +
					<pre className="bg-gray-100 p-4 rounded-md">
60 +
						<code>
61 +
							Message: {data.message} <br />
62 +
							Success: {data.success.toString()}
63 +
						</code>
64 +
					</pre>
65 +
				)}
66 +
			</ClientOnly>
67 +
		</div>
68 +
	);
69 +
}
70 +
71 +
export default Home;
src/templates/extras/client/src/components/Home.tsx/Home-with-reactroutermpa-tailwind.tsx (added) +68 −0
1 +
import { useState } from "react";
2 +
import beaver from "../assets/beaver.svg";
3 +
import type { ApiResponse } from "shared";
4 +
import ClientOnly from "./ClientOnly";
5 +
6 +
const SERVER_URL = import.meta.env.DEV ? "http://localhost:3000" : "/api";
7 +
8 +
function Home() {
9 +
	const [data, setData] = useState<ApiResponse | undefined>();
10 +
11 +
	async function sendRequest() {
12 +
		try {
13 +
			const req = await fetch(`${SERVER_URL}/hello`);
14 +
			const res: ApiResponse = await req.json();
15 +
			setData(res);
16 +
		} catch (error) {
17 +
			console.log(error);
18 +
		}
19 +
	}
20 +
21 +
	return (
22 +
		<div className="max-w-xl mx-auto flex flex-col gap-6 items-center justify-center min-h-screen">
23 +
			<a
24 +
				href="https://github.com/stevedylandev/bhvr"
25 +
				target="_blank"
26 +
				rel="noopener"
27 +
			>
28 +
				<img
29 +
					src={beaver}
30 +
					className="w-16 h-16 cursor-pointer"
31 +
					alt="beaver logo"
32 +
				/>
33 +
			</a>
34 +
			<h1 className="text-5xl font-black">bhvr</h1>
35 +
			<h2 className="text-2xl font-bold">Bun + Hono + Vite + React</h2>
36 +
			<p>A typesafe fullstack monorepo</p>
37 +
			<ClientOnly>
38 +
				<div className="flex items-center gap-4">
39 +
					<button
40 +
						type="button"
41 +
						onClick={sendRequest}
42 +
						className="bg-black text-white px-2.5 py-1.5 rounded-md"
43 +
					>
44 +
						Call API
45 +
					</button>
46 +
					<a
47 +
						target="_blank"
48 +
						href="https://bhvr.dev"
49 +
						className="border-1 border-black text-black px-2.5 py-1.5 rounded-md"
50 +
						rel="noopener"
51 +
					>
52 +
						Docs
53 +
					</a>
54 +
				</div>
55 +
				{data && (
56 +
					<pre className="bg-gray-100 p-4 rounded-md">
57 +
						<code>
58 +
							Message: {data.message} <br />
59 +
							Success: {data.success.toString()}
60 +
						</code>
61 +
					</pre>
62 +
				)}
63 +
			</ClientOnly>
64 +
		</div>
65 +
	);
66 +
}
67 +
68 +
export default Home;
src/templates/extras/client/src/components/Home.tsx/Home-with-reactroutermpa-tanstackquery.tsx (added) +65 −0
1 +
import { useState } from "react";
2 +
import beaver from "../assets/beaver.svg";
3 +
import { useMutation } from "@tanstack/react-query";
4 +
import type { ApiResponse } from "shared";
5 +
import "../App.css";
6 +
import ClientOnly from "./ClientOnly";
7 +
8 +
const SERVER_URL = import.meta.env.DEV ? "http://localhost:3000" : "/api";
9 +
10 +
function Home() {
11 +
	const [data, setData] = useState<ApiResponse | undefined>();
12 +
13 +
	const { mutate: sendRequest } = useMutation({
14 +
		mutationFn: async () => {
15 +
			const req = await fetch(`${SERVER_URL}/hello`);
16 +
			const res: ApiResponse = await req.json();
17 +
			setData(res);
18 +
		},
19 +
		onError: (err) => console.log(err),
20 +
	});
21 +
22 +
	return (
23 +
		<>
24 +
			<div>
25 +
				<a
26 +
					href="https://github.com/stevedylandev/bhvr"
27 +
					target="_blank"
28 +
					rel="noopener"
29 +
				>
30 +
					<img src={beaver} className="logo" alt="beaver logo" />
31 +
				</a>
32 +
			</div>
33 +
			<h1>bhvr</h1>
34 +
			<h2>Bun + Hono + Vite + React</h2>
35 +
			<p>A typesafe fullstack monorepo</p>
36 +
			<ClientOnly>
37 +
				<div className="card">
38 +
					<div className="button-container">
39 +
						<button type="button" onClick={() => sendRequest()}>
40 +
							Call API
41 +
						</button>
42 +
						<a
43 +
							className="docs-link"
44 +
							target="_blank"
45 +
							href="https://bhvr.dev"
46 +
							rel="noopener"
47 +
						>
48 +
							Docs
49 +
						</a>
50 +
					</div>
51 +
					{data && (
52 +
						<pre className="response">
53 +
							<code>
54 +
								Message: {data.message} <br />
55 +
								Success: {data.success.toString()}
56 +
							</code>
57 +
						</pre>
58 +
					)}
59 +
				</div>
60 +
			</ClientOnly>
61 +
		</>
62 +
	);
63 +
}
64 +
65 +
export default Home;
src/templates/extras/client/src/components/Home.tsx/Home-with-reactroutermpa.tsx (added) +65 −0
1 +
import { useState } from "react";
2 +
import beaver from "../assets/beaver.svg";
3 +
import type { ApiResponse } from "shared";
4 +
import "../App.css";
5 +
import ClientOnly from "./ClientOnly";
6 +
7 +
const SERVER_URL = import.meta.env.DEV ? "http://localhost:3000" : "/api";
8 +
9 +
function Home() {
10 +
	const [data, setData] = useState<ApiResponse | undefined>();
11 +
12 +
	async function sendRequest() {
13 +
		try {
14 +
			const req = await fetch(`${SERVER_URL}/hello`);
15 +
			const res: ApiResponse = await req.json();
16 +
			setData(res);
17 +
		} catch (error) {
18 +
			console.log(error);
19 +
		}
20 +
	}
21 +
22 +
	return (
23 +
		<>
24 +
			<div>
25 +
				<a
26 +
					href="https://github.com/stevedylandev/bhvr"
27 +
					target="_blank"
28 +
					rel="noopener"
29 +
				>
30 +
					<img src={beaver} className="logo" alt="beaver logo" />
31 +
				</a>
32 +
			</div>
33 +
			<h1>bhvr</h1>
34 +
			<h2>Bun + Hono + Vite + React</h2>
35 +
			<p>A typesafe fullstack monorepo</p>
36 +
			<ClientOnly>
37 +
				<div className="card">
38 +
					<div className="button-container">
39 +
						<button type="button" onClick={sendRequest}>
40 +
							Call API
41 +
						</button>
42 +
						<a
43 +
							className="docs-link"
44 +
							target="_blank"
45 +
							href="https://bhvr.dev"
46 +
							rel="noopener"
47 +
						>
48 +
							Docs
49 +
						</a>
50 +
					</div>
51 +
					{data && (
52 +
						<pre className="response">
53 +
							<code>
54 +
								Message: {data.message} <br />
55 +
								Success: {data.success.toString()}
56 +
							</code>
57 +
						</pre>
58 +
					)}
59 +
				</div>
60 +
			</ClientOnly>
61 +
		</>
62 +
	);
63 +
}
64 +
65 +
export default Home;
src/templates/extras/client/src/root.tsx/root.tsx (added) +61 −0
1 +
import {
2 +
	isRouteErrorResponse,
3 +
	Links,
4 +
	Meta,
5 +
	Outlet,
6 +
	Scripts,
7 +
	ScrollRestoration,
8 +
} from "react-router";
9 +
10 +
import "./index.css";
11 +
12 +
export function Layout({ children }: { children: React.ReactNode }) {
13 +
	return (
14 +
		<html lang="en">
15 +
			<head>
16 +
				<meta charSet="utf-8" />
17 +
				<meta name="viewport" content="width=device-width, initial-scale=1" />
18 +
				<Meta />
19 +
				<Links />
20 +
			</head>
21 +
			<body>
22 +
				{children}
23 +
				<ScrollRestoration />
24 +
				<Scripts />
25 +
			</body>
26 +
		</html>
27 +
	);
28 +
}
29 +
30 +
export default function App() {
31 +
	return <Outlet />;
32 +
}
33 +
34 +
export function ErrorBoundary({ error }: any) {
35 +
	let message = "Oops!";
36 +
	let details = "An unexpected error occurred.";
37 +
	let stack: string | undefined;
38 +
39 +
	if (isRouteErrorResponse(error)) {
40 +
		message = error.status === 404 ? "404" : "Error";
41 +
		details =
42 +
			error.status === 404
43 +
				? "The requested page could not be found."
44 +
				: error.statusText || details;
45 +
	} else if (import.meta.env.DEV && error && error instanceof Error) {
46 +
		details = error.message;
47 +
		stack = error.stack;
48 +
	}
49 +
50 +
	return (
51 +
		<main className="pt-16 p-4 container mx-auto">
52 +
			<h1>{message}</h1>
53 +
			<p>{details}</p>
54 +
			{stack && (
55 +
				<pre className="w-full p-4 overflow-x-auto">
56 +
					<code>{stack}</code>
57 +
				</pre>
58 +
			)}
59 +
		</main>
60 +
	);
61 +
}
src/templates/extras/client/src/routes.ts/routes.ts (added) +3 −0
1 +
import { type RouteConfig, index } from "@react-router/dev/routes";
2 +
3 +
export default [index("routes/home.tsx")] satisfies RouteConfig;
src/templates/extras/client/src/routes/home.tsx/home.tsx (added) +13 −0
1 +
import Home from "../components/Home";
2 +
import type { Route } from "./+types/home";
3 +
4 +
export function meta({}: Route.MetaArgs) {
5 +
	return [
6 +
		{ title: "bhvr" },
7 +
		{ name: "description", content: "A typesafe fullstack monorepo" },
8 +
	];
9 +
}
10 +
11 +
export default function HomePage() {
12 +
	return <Home />;
13 +
}
src/templates/extras/client/tsconfig.app.json/tsconfig.app.json (added) +31 −0
1 +
{
2 +
	"compilerOptions": {
3 +
		"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4 +
		"target": "ES2020",
5 +
		"useDefineForClassFields": true,
6 +
		"lib": ["ES2020", "DOM", "DOM.Iterable"],
7 +
		"module": "ESNext",
8 +
		"skipLibCheck": true,
9 +
		"moduleResolution": "bundler",
10 +
		"allowImportingTsExtensions": true,
11 +
		"isolatedModules": true,
12 +
		"moduleDetection": "force",
13 +
		"noEmit": true,
14 +
		"jsx": "react-jsx",
15 +
		"rootDirs": [".", "./.react-router/types"],
16 +
		"paths": {
17 +
			"~/*": ["./src/*"]
18 +
		},
19 +
		"strict": true,
20 +
		"noUnusedLocals": true,
21 +
		"noUnusedParameters": true,
22 +
		"noFallthroughCasesInSwitch": true,
23 +
		"noUncheckedSideEffectImports": true
24 +
	},
25 +
	"include": [
26 +
		"**/*",
27 +
		"**/.server/**/*",
28 +
		"**/.client/**/*",
29 +
		".react-router/types/**/*"
30 +
	]
31 +
}
src/templates/extras/client/vite.config.ts/vite.config-with-reactroutermpa-shadcn-tailwind.ts (added) +14 −0
1 +
import { reactRouter } from "@react-router/dev/vite";
2 +
import tailwindcss from "@tailwindcss/vite";
3 +
import { defineConfig } from "vite";
4 +
import path from "node:path";
5 +
import tsconfigPaths from "vite-tsconfig-paths";
6 +
7 +
export default defineConfig({
8 +
	plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],
9 +
	resolve: {
10 +
		alias: {
11 +
			"@": path.resolve(__dirname, "./src"),
12 +
		},
13 +
	},
14 +
});
src/templates/extras/client/vite.config.ts/vite.config-with-reactroutermpa-tailwind.ts (added) +8 −0
1 +
import { reactRouter } from "@react-router/dev/vite";
2 +
import tailwindcss from "@tailwindcss/vite";
3 +
import { defineConfig } from "vite";
4 +
import tsconfigPaths from "vite-tsconfig-paths";
5 +
6 +
export default defineConfig({
7 +
	plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],
8 +
});
src/templates/extras/client/vite.config.ts/vite.config-with-reactroutermpa.ts (added) +7 −0
1 +
import { reactRouter } from "@react-router/dev/vite";
2 +
import { defineConfig } from "vite";
3 +
import tsconfigPaths from "vite-tsconfig-paths";
4 +
5 +
export default defineConfig({
6 +
	plugins: [reactRouter(), tsconfigPaths()],
7 +
});
src/types.ts +1 −1
14 14
	shadcn?: boolean;
15 15
	rpc?: boolean;
16 16
	linter?: "eslint" | "biome";
17 -
	router?: "none" | "reactrouter" | "tanstackrouter";
17 +
	router?: "none" | "reactrouter" | "reactroutermpa" | "tanstackrouter";
18 18
	tanstackQuery?: boolean;
19 19
};
20 20