Implemented template structure; Implemented TanStack Query Template a331cc64
Max L · 2025-08-03 12:43 18 file(s) · +711 −28
src/installers/tanstack-query.ts (added) +61 −0
1 +
import path from "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 +
10 +
export const tanstackQueryInstaller = async (
11 +
	options: Required<ProjectOptions>,
12 +
): Promise<boolean> => {
13 +
	const spinner = yoctoSpinner({
14 +
		text: "Setting up TanStack Query...",
15 +
	}).start();
16 +
17 +
	try {
18 +
		const { projectName, rpc, shadcn, tailwind, tanstackQuery } = options;
19 +
20 +
		const projectPath = path.resolve(process.cwd(), projectName);
21 +
		spinner.text = "Installing TanStack Query...";
22 +
		await addPackageDependency({
23 +
			dependencies: ["@tanstack/react-query"],
24 +
			target: "client",
25 +
			projectName,
26 +
		});
27 +
28 +
		const selectedTemplate = `App-with${tailwind ? "-tailwind" : ""}${shadcn ? "-shadcn" : ""}${rpc ? "-rpc" : ""}${tanstackQuery ? "-tanstackquery" : ""}.tsx`;
29 +
30 +
		const appTsxSrc = path.join(
31 +
			EXTRAS_DIR,
32 +
			"client",
33 +
			"src",
34 +
			"App.tsx",
35 +
			selectedTemplate,
36 +
		);
37 +
		const appTsxTarget = path.join(projectPath, "client", "src", "App.tsx");
38 +
		fs.copySync(appTsxSrc, appTsxTarget);
39 +
40 +
		const mainTsxSrc = path.join(
41 +
			EXTRAS_DIR,
42 +
			"client",
43 +
			"src",
44 +
			"main.tsx",
45 +
			"main-with-tanstackquery.tsx",
46 +
		);
47 +
		const mainTsxTarget = path.join(projectPath, "client", "src", "main.tsx");
48 +
		fs.copySync(mainTsxSrc, mainTsxTarget);
49 +
50 +
		spinner.success("TanStack Query setup completed");
51 +
		return true;
52 +
	} catch (err: unknown) {
53 +
		spinner.error("Failed to set up TanStack Query");
54 +
		if (err instanceof Error) {
55 +
			consola.error(pc.red("Error:"), err.message);
56 +
		} else {
57 +
			consola.error(pc.red("Error: Unknown error"));
58 +
		}
59 +
		return false;
60 +
	}
61 +
};
src/lib/create-project.ts +3 −0
3 3
import { installDependencies } from "./install-dependencies";
4 4
import { promptForOptions } from "./prompt-for-options";
5 5
import { scaffoldTemplate } from "./scaffold-template";
6 +
import { installPackages } from "./install-packages";
6 7
7 8
export async function createProject(
8 9
	projectDirectory: string,
22 23
	if (!scaffolded) {
23 24
		return null;
24 25
	}
26 +
27 +
	const packagesInstalled = await installPackages(projectOptions);
25 28
26 29
	const gitInitialized = await initializeGit(
27 30
		projectOptions.projectName ?? projectDirectory,
src/lib/install-packages.ts (added) +28 −0
1 +
import type { ProjectOptions } from "@/types";
2 +
import { setupBiome } from "./setup-biome";
3 +
import path from "node:path";
4 +
import { tanstackQueryInstaller } from "@/installers/tanstack-query";
5 +
import { rpcInstaller } from "@/installers/rpc";
6 +
7 +
export async function installPackages(
8 +
	options: Required<ProjectOptions>,
9 +
): Promise<boolean> {
10 +
	const { projectName, template, repo, branch, rpc, linter, tanstackQuery } =
11 +
		options;
12 +
13 +
	const projectPath = path.resolve(process.cwd(), projectName);
14 +
15 +
	if (rpc) {
16 +
		await rpcInstaller(options);
17 +
	}
18 +
19 +
	if (linter === "biome") {
20 +
		await setupBiome(projectPath);
21 +
	}
22 +
23 +
	if (tanstackQuery) {
24 +
		await tanstackQueryInstaller(options);
25 +
	}
26 +
27 +
	return false;
28 +
}
src/lib/patch-files-rpc.ts → src/installers/rpc.ts +17 −20
11 11
	shadcnTemplate,
12 12
	tailwindTemplate,
13 13
} from "@/utils/templates";
14 +
import { ProjectOptions } from "@/types";
15 +
import { EXTRAS_DIR } from "@/utils";
14 16
15 -
export async function patchFilesForRPC(
16 -
	projectPath: string,
17 -
	templateChoice: string,
17 +
export async function rpcInstaller(
18 +
	options: Required<ProjectOptions>,
18 19
): Promise<boolean> {
19 20
	const spinner = yoctoSpinner({ text: "Setting up RPC client..." }).start();
20 21
21 22
	try {
23 +
		const { projectName, rpc, shadcn, tailwind } = options;
24 +
		const projectPath = path.resolve(process.cwd(), projectName);
25 +
22 26
		// 1. Update client package.json to ensure hono client is installed
23 27
		const clientPkgPath = path.join(projectPath, "client", "package.json");
24 28
		const clientPkg = await fs.readJson(clientPkgPath);
52 56
		await fs.writeFile(clientHelperPath, honoClientTemplate, "utf8");
53 57
54 58
		// 5. Update App.tsx based on template selection using switch statement
55 -
		const appTsxPath = path.join(projectPath, "client", "src", "App.tsx");
59 +
		const selectedTemplate = `App-with${tailwind ? "-tailwind" : ""}${shadcn ? "-shadcn" : ""}${rpc ? "-rpc" : ""}.tsx`;
56 60
57 -
		// Determine template content based on the template type
58 -
		let updatedAppContent: string;
61 +
		const appTsxSrc = path.join(
62 +
			EXTRAS_DIR,
63 +
			"client",
64 +
			"src",
65 +
			"App.tsx",
66 +
			selectedTemplate,
67 +
		);
68 +
		const appTsxTarget = path.join(projectPath, "client", "src", "App.tsx");
59 69
60 -
		// Select template based on choice
61 -
		switch (templateChoice) {
62 -
			case "shadcn":
63 -
				updatedAppContent = shadcnTemplate;
64 -
				break;
65 -
			case "tailwind":
66 -
				updatedAppContent = tailwindTemplate;
67 -
				break;
68 -
			default:
69 -
				updatedAppContent = defaultTemplate;
70 -
				break;
71 -
		}
72 -
73 -
		await fs.writeFile(appTsxPath, updatedAppContent, "utf8");
70 +
		fs.copySync(appTsxSrc, appTsxTarget);
74 71
		spinner.success("RPC client setup completed");
75 72
		return true;
76 73
	} catch (err: unknown) {
src/lib/prompt-for-options.ts +24 −0
93 93
		linter = linterResponse as "eslint" | "biome";
94 94
	}
95 95
96 +
	let useTanstackQuery = options.tanstackQuery;
97 +
98 +
	if (!options.yes && !options.tanstackQuery) {
99 +
		const { data: tanstackQueryResponse, error } = await tryCatch(
100 +
			consola.prompt(
101 +
				"Would you like to enable TanStack Query for data fetching and state management?",
102 +
				{
103 +
					type: "confirm",
104 +
					initial: false,
105 +
				},
106 +
			),
107 +
		);
108 +
109 +
		if (error) {
110 +
			consola.error("Project creation cancelled.");
111 +
			process.exit(1);
112 +
		}
113 +
114 +
		useTanstackQuery = tanstackQueryResponse;
115 +
	}
116 +
96 117
	return {
97 118
		...options,
98 119
		projectName,
99 120
		template: templateChoice,
121 +
		tailwind: templateChoice === "tailwind" || templateChoice === "shadcn",
122 +
		shadcn: templateChoice === "shadcn",
100 123
		rpc: useRpc,
101 124
		linter,
125 +
		tanstackQuery: useTanstackQuery,
102 126
	};
103 127
}
src/lib/scaffold-template.ts +0 −8
55 55
			console.log(pc.blue("Removed .git directory"));
56 56
		}
57 57
58 -
		if (rpc) {
59 -
			await patchFilesForRPC(projectPath, template);
60 -
		}
61 -
62 -
		if (linter === "biome") {
63 -
			await setupBiome(projectPath);
64 -
		}
65 -
66 58
		return true;
67 59
	} catch (err) {
68 60
		spinner.error("Failed to download template");
src/templates/extras/client/src/App.tsx/App-with-rpc-tanstackquery.tsx (added) +64 −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 +
7 +
const SERVER_URL = import.meta.env.VITE_SERVER_URL || "http://localhost:3000";
8 +
9 +
const client = hcWithType(SERVER_URL);
10 +
11 +
type ResponseType = Awaited<ReturnType<typeof client.hello.$get>>;
12 +
13 +
function App() {
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 +
		<>
36 +
			<div>
37 +
				<a href="https://github.com/stevedylandev/bhvr" target="_blank">
38 +
					<img src={beaver} className="logo" alt="beaver logo" />
39 +
				</a>
40 +
			</div>
41 +
			<h1>bhvr</h1>
42 +
			<h2>Bun + Hono + Vite + React</h2>
43 +
			<p>A typesafe fullstack monorepo</p>
44 +
			<div className="card">
45 +
				<div className="button-container">
46 +
					<button onClick={sendRequest}>Call API</button>
47 +
					<a className="docs-link" target="_blank" href="https://bhvr.dev">
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 +
		</>
61 +
	);
62 +
}
63 +
64 +
export default App;
src/templates/extras/client/src/App.tsx/App-with-rpc.tsx (added) +61 −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 +
6 +
const SERVER_URL = import.meta.env.VITE_SERVER_URL || "http://localhost:3000";
7 +
8 +
const client = hcWithType(SERVER_URL);
9 +
10 +
type ResponseType = Awaited<ReturnType<typeof client.hello.$get>>;
11 +
12 +
function App() {
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 +
		<>
33 +
			<div>
34 +
				<a href="https://github.com/stevedylandev/bhvr" target="_blank">
35 +
					<img src={beaver} className="logo" alt="beaver logo" />
36 +
				</a>
37 +
			</div>
38 +
			<h1>bhvr</h1>
39 +
			<h2>Bun + Hono + Vite + React</h2>
40 +
			<p>A typesafe fullstack monorepo</p>
41 +
			<div className="card">
42 +
				<div className="button-container">
43 +
					<button onClick={sendRequest}>Call API</button>
44 +
					<a className="docs-link" target="_blank" href="https://bhvr.dev">
45 +
						Docs
46 +
					</a>
47 +
				</div>
48 +
				{data && (
49 +
					<pre className="response">
50 +
						<code>
51 +
							Message: {data.message} <br />
52 +
							Success: {data.success.toString()}
53 +
						</code>
54 +
					</pre>
55 +
				)}
56 +
			</div>
57 +
		</>
58 +
	);
59 +
}
60 +
61 +
export default App;
src/templates/extras/client/src/App.tsx/App-with-tailwind-rpc-tanstackquery.tsx (added) +72 −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 +
6 +
const SERVER_URL = import.meta.env.VITE_SERVER_URL || "http://localhost:3000";
7 +
8 +
type ResponseType = Awaited<ReturnType<typeof client.hello.$get>>;
9 +
10 +
const client = hcWithType(SERVER_URL);
11 +
12 +
function App() {
13 +
	const [data, setData] = useState<
14 +
		Awaited<ReturnType<ResponseType["json"]>> | undefined
15 +
	>();
16 +
17 +
	const { mutate: sendRequest } = useMutation({
18 +
		mutationFn: async () => {
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 +
33 +
	return (
34 +
		<div className="max-w-xl mx-auto flex flex-col gap-6 items-center justify-center min-h-screen">
35 +
			<a href="https://github.com/stevedylandev/bhvr" target="_blank">
36 +
				<img
37 +
					src={beaver}
38 +
					className="w-16 h-16 cursor-pointer"
39 +
					alt="beaver logo"
40 +
				/>
41 +
			</a>
42 +
			<h1 className="text-5xl font-black">bhvr</h1>
43 +
			<h2 className="text-2xl font-bold">Bun + Hono + Vite + React</h2>
44 +
			<p>A typesafe fullstack monorepo</p>
45 +
			<div className="flex items-center gap-4">
46 +
				<button
47 +
					onClick={sendRequest}
48 +
					className="bg-black text-white px-2.5 py-1.5 rounded-md"
49 +
				>
50 +
					Call API
51 +
				</button>
52 +
				<a
53 +
					target="_blank"
54 +
					href="https://bhvr.dev"
55 +
					className="border-1 border-black text-black px-2.5 py-1.5 rounded-md"
56 +
				>
57 +
					Docs
58 +
				</a>
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 +
		</div>
69 +
	);
70 +
}
71 +
72 +
export default App;
src/templates/extras/client/src/App.tsx/App-with-tailwind-rpc.tsx (added) +69 −0
1 +
import { useState } from "react";
2 +
import beaver from "./assets/beaver.svg";
3 +
import { hcWithType } from "server/dist/client";
4 +
5 +
const SERVER_URL = import.meta.env.VITE_SERVER_URL || "http://localhost:3000";
6 +
7 +
type ResponseType = Awaited<ReturnType<typeof client.hello.$get>>;
8 +
9 +
const client = hcWithType(SERVER_URL);
10 +
11 +
function App() {
12 +
	const [data, setData] = useState<
13 +
		Awaited<ReturnType<ResponseType["json"]>> | undefined
14 +
	>();
15 +
16 +
	async function sendRequest() {
17 +
		try {
18 +
			const res = await client.hello.$get();
19 +
			if (!res.ok) {
20 +
				console.log("Error fetching data");
21 +
				return;
22 +
			}
23 +
			const data = await res.json();
24 +
			setData(data);
25 +
		} catch (error) {
26 +
			console.log(error);
27 +
		}
28 +
	}
29 +
30 +
	return (
31 +
		<div className="max-w-xl mx-auto flex flex-col gap-6 items-center justify-center min-h-screen">
32 +
			<a href="https://github.com/stevedylandev/bhvr" target="_blank">
33 +
				<img
34 +
					src={beaver}
35 +
					className="w-16 h-16 cursor-pointer"
36 +
					alt="beaver logo"
37 +
				/>
38 +
			</a>
39 +
			<h1 className="text-5xl font-black">bhvr</h1>
40 +
			<h2 className="text-2xl font-bold">Bun + Hono + Vite + React</h2>
41 +
			<p>A typesafe fullstack monorepo</p>
42 +
			<div className="flex items-center gap-4">
43 +
				<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 +
				>
54 +
					Docs
55 +
				</a>
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 +
		</div>
66 +
	);
67 +
}
68 +
69 +
export default App;
src/templates/extras/client/src/App.tsx/App-with-tailwind-shadcn-rpc-tanstackquery.tsx (added) +66 −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 +
7 +
const SERVER_URL = import.meta.env.VITE_SERVER_URL || "http://localhost:3000";
8 +
9 +
const client = hcWithType(SERVER_URL);
10 +
11 +
type ResponseType = Awaited<ReturnType<typeof client.hello.$get>>;
12 +
13 +
function App() {
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 href="https://github.com/stevedylandev/bhvr" target="_blank">
37 +
				<img
38 +
					src={beaver}
39 +
					className="w-16 h-16 cursor-pointer"
40 +
					alt="beaver logo"
41 +
				/>
42 +
			</a>
43 +
			<h1 className="text-5xl font-black">bhvr</h1>
44 +
			<h2 className="text-2xl font-bold">Bun + Hono + Vite + React</h2>
45 +
			<p>A typesafe fullstack monorepo</p>
46 +
			<div className="flex items-center gap-4">
47 +
				<Button onClick={sendRequest}>Call API</Button>
48 +
				<Button variant="secondary" asChild>
49 +
					<a target="_blank" href="https://bhvr.dev">
50 +
						Docs
51 +
					</a>
52 +
				</Button>
53 +
			</div>
54 +
			{data && (
55 +
				<pre className="bg-gray-100 p-4 rounded-md">
56 +
					<code>
57 +
						Message: {data.message} <br />
58 +
						Success: {data.success.toString()}
59 +
					</code>
60 +
				</pre>
61 +
			)}
62 +
		</div>
63 +
	);
64 +
}
65 +
66 +
export default App;
src/templates/extras/client/src/App.tsx/App-with-tailwind-shadcn-rpc.tsx (added) +63 −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 +
6 +
const SERVER_URL = import.meta.env.VITE_SERVER_URL || "http://localhost:3000";
7 +
8 +
const client = hcWithType(SERVER_URL);
9 +
10 +
type ResponseType = Awaited<ReturnType<typeof client.hello.$get>>;
11 +
12 +
function App() {
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 href="https://github.com/stevedylandev/bhvr" target="_blank">
34 +
				<img
35 +
					src={beaver}
36 +
					className="w-16 h-16 cursor-pointer"
37 +
					alt="beaver logo"
38 +
				/>
39 +
			</a>
40 +
			<h1 className="text-5xl font-black">bhvr</h1>
41 +
			<h2 className="text-2xl font-bold">Bun + Hono + Vite + React</h2>
42 +
			<p>A typesafe fullstack monorepo</p>
43 +
			<div className="flex items-center gap-4">
44 +
				<Button onClick={sendRequest}>Call API</Button>
45 +
				<Button variant="secondary" asChild>
46 +
					<a target="_blank" href="https://bhvr.dev">
47 +
						Docs
48 +
					</a>
49 +
				</Button>
50 +
			</div>
51 +
			{data && (
52 +
				<pre className="bg-gray-100 p-4 rounded-md">
53 +
					<code>
54 +
						Message: {data.message} <br />
55 +
						Success: {data.success.toString()}
56 +
					</code>
57 +
				</pre>
58 +
			)}
59 +
		</div>
60 +
	);
61 +
}
62 +
63 +
export default App;
src/templates/extras/client/src/App.tsx/App-with-tailwind-shadcn-tanstackquery.tsx (added) +56 −0
1 +
import { useState } from "react";
2 +
import beaver from "./assets/beaver.svg";
3 +
import { ApiResponse } from "shared";
4 +
import { Button } from "./components/ui/button";
5 +
import { useMutation } from "@tanstack/react-query";
6 +
7 +
const SERVER_URL = import.meta.env.VITE_SERVER_URL || "http://localhost:3000";
8 +
9 +
function App() {
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 href="https://github.com/stevedylandev/bhvr" target="_blank">
27 +
				<img
28 +
					src={beaver}
29 +
					className="w-16 h-16 cursor-pointer"
30 +
					alt="beaver logo"
31 +
				/>
32 +
			</a>
33 +
			<h1 className="text-5xl font-black">bhvr</h1>
34 +
			<h2 className="text-2xl font-bold">Bun + Hono + Vite + React</h2>
35 +
			<p>A typesafe fullstack monorepo</p>
36 +
			<div className="flex items-center gap-4">
37 +
				<Button onClick={sendRequest}>Call API</Button>
38 +
				<Button variant="secondary" asChild>
39 +
					<a target="_blank" href="https://bhvr.dev">
40 +
						Docs
41 +
					</a>
42 +
				</Button>
43 +
			</div>
44 +
			{data && (
45 +
				<pre className="bg-gray-100 p-4 rounded-md">
46 +
					<code>
47 +
						Message: {data.message} <br />
48 +
						Success: {data.success.toString()}
49 +
					</code>
50 +
				</pre>
51 +
			)}
52 +
		</div>
53 +
	);
54 +
}
55 +
56 +
export default App;
src/templates/extras/client/src/App.tsx/App-with-tailwind-tanstackquery.tsx (added) +62 −0
1 +
import { useState } from "react";
2 +
import beaver from "./assets/beaver.svg";
3 +
import { ApiResponse } from "shared";
4 +
import { useMutation } from "@tanstack/react-query";
5 +
6 +
const SERVER_URL = import.meta.env.VITE_SERVER_URL || "http://localhost:3000";
7 +
8 +
function App() {
9 +
	const [data, setData] = useState<ApiResponse | undefined>();
10 +
11 +
	const { mutate: sendRequest } = useMutation({
12 +
		mutationFn: async () => {
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 +
23 +
	return (
24 +
		<div className="max-w-xl mx-auto flex flex-col gap-6 items-center justify-center min-h-screen">
25 +
			<a href="https://github.com/stevedylandev/bhvr" target="_blank">
26 +
				<img
27 +
					src={beaver}
28 +
					className="w-16 h-16 cursor-pointer"
29 +
					alt="beaver logo"
30 +
				/>
31 +
			</a>
32 +
			<h1 className="text-5xl font-black">bhvr</h1>
33 +
			<h2 className="text-2xl font-bold">Bun + Hono + Vite + React</h2>
34 +
			<p>A typesafe fullstack monorepo</p>
35 +
			<div className="flex items-center gap-4">
36 +
				<button
37 +
					onClick={sendRequest}
38 +
					className="bg-black text-white px-2.5 py-1.5 rounded-md"
39 +
				>
40 +
					Call API
41 +
				</button>
42 +
				<a
43 +
					target="_blank"
44 +
					href="https://bhvr.dev"
45 +
					className="border-1 border-black text-black px-2.5 py-1.5 rounded-md"
46 +
				>
47 +
					Docs
48 +
				</a>
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 +
		</div>
59 +
	);
60 +
}
61 +
62 +
export default App;
src/templates/extras/client/src/main.tsx/main-with-tanstackquery.tsx (added) +15 −0
1 +
import { StrictMode } from "react";
2 +
import { createRoot } from "react-dom/client";
3 +
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
4 +
import "./index.css";
5 +
import App from "./App.tsx";
6 +
7 +
const queryClient = new QueryClient();
8 +
9 +
createRoot(document.getElementById("root")!).render(
10 +
	<StrictMode>
11 +
		<QueryClientProvider client={queryClient}>
12 +
			<App />
13 +
		</QueryClientProvider>
14 +
	</StrictMode>,
15 +
);
src/types.ts +3 −0
10 10
	repo?: string;
11 11
	template?: string;
12 12
	branch?: string;
13 +
	tailwind?: boolean;
14 +
	shadcn?: boolean;
13 15
	rpc?: boolean;
14 16
	linter?: "eslint" | "biome";
17 +
	tanstackQuery?: boolean;
15 18
};
16 19
17 20
export interface ProjectResult {
src/utils/add-package-dependency.ts (added) +46 −0
1 +
import path from "path";
2 +
import fs from "fs-extra";
3 +
import sortPackageJson from "sort-package-json";
4 +
import { type PackageJson } from "type-fest";
5 +
import { execa } from "execa";
6 +
7 +
export const addPackageDependency = async (opts: {
8 +
	dependencies: string[];
9 +
	devMode?: boolean;
10 +
	projectName: string;
11 +
	target?: "client" | "server";
12 +
}) => {
13 +
	const { dependencies, devMode, projectName, target } = opts;
14 +
15 +
	const projectPath = path.resolve(process.cwd(), projectName);
16 +
17 +
	if (target !== undefined) {
18 +
		if (target === "client") {
19 +
			const clientPath = path.join(projectPath, "client");
20 +
			await execa("bun", [`install${devMode ? " -D" : ""}`, ...dependencies], {
21 +
				cwd: clientPath,
22 +
			});
23 +
			return;
24 +
		}
25 +
26 +
		if (target === "server") {
27 +
			const serverPath = path.join(projectPath, "server");
28 +
			await execa(
29 +
				"bun",
30 +
				[
31 +
					`install${devMode ? " -D" : ""}`,
32 +
					devMode ? "-D" : "",
33 +
					...dependencies,
34 +
				],
35 +
				{
36 +
					cwd: serverPath,
37 +
				},
38 +
			);
39 +
			return;
40 +
		}
41 +
	}
42 +
43 +
	await execa("bun", ["install", ...dependencies], {
44 +
		cwd: projectPath,
45 +
	});
46 +
};
src/utils/constants.ts +1 −0
1 1
export const DEFAULT_REPO = "stevedylandev/bhvr";
2 +
export const EXTRAS_DIR = "./src/templates/extras";