src/utils/helpers.ts 8.4 K raw
1
import figlet from "figlet";
2
import chalk from "chalk";
3
import { execa } from "execa";
4
import ora from "ora";
5
import path from "node:path";
6
import fs from "fs-extra";
7
import {
8
	honoRpcTemplate,
9
	shadcnTemplate,
10
	tailwindTemplate,
11
	defaultTemplate,
12
	TEMPLATES,
13
} from "./templates";
14
import type { ProjectOptions, ProjectResult } from "../types";
15
import degit from "degit";
16
import prompts from "prompts";
17
18
export const DEFAULT_REPO = "stevedylandev/bhvr";
19
20
export function displayBanner() {
21
	try {
22
		const text = figlet.textSync("bhvr", {
23
			font: "Big",
24
			horizontalLayout: "default",
25
			verticalLayout: "default",
26
			width: 80,
27
			whitespaceBreak: true,
28
		});
29
30
		console.log("\n");
31
		console.log(chalk.yellowBright(text));
32
	} catch (error) {
33
		console.log("\n");
34
		console.log(chalk.yellowBright("B H V R"));
35
		console.log(chalk.yellow("=========="));
36
	}
37
38
	console.log(`\n${chalk.cyan("🦫 Lets build 🦫")}\n`);
39
	console.log(`${chalk.blue("https://github.com/stevedylandev/bhvr")}\n`);
40
}
41
42
export async function patchFilesForRPC(
43
	projectPath: string,
44
	templateChoice: string,
45
): Promise<boolean> {
46
	const spinner = ora("Setting up RPC client...").start();
47
48
	try {
49
		// 1. Update client package.json to ensure hono client is installed
50
		const clientPkgPath = path.join(projectPath, "client", "package.json");
51
		const clientPkg = await fs.readJson(clientPkgPath);
52
53
		if (!clientPkg.dependencies.hono) {
54
			await execa("bun", ["install", "hono"], { cwd: projectPath });
55
		}
56
57
		await fs.writeJson(clientPkgPath, clientPkg, { spaces: 2 });
58
59
		// 2. Server modification for RPC export type
60
		const serverIndexPath = path.join(projectPath, "server", "src", "index.ts");
61
		await fs.writeFile(serverIndexPath, honoRpcTemplate, "utf8");
62
63
		// 3. Update App.tsx based on template selection using switch statement
64
		const appTsxPath = path.join(projectPath, "client", "src", "App.tsx");
65
66
		// Determine template content based on the template type
67
		let updatedAppContent: string;
68
69
		// Select template based on choice
70
		switch (templateChoice) {
71
			case "shadcn":
72
				updatedAppContent = shadcnTemplate;
73
				break;
74
			case "tailwind":
75
				updatedAppContent = tailwindTemplate;
76
				break;
77
			default:
78
				updatedAppContent = defaultTemplate;
79
				break;
80
		}
81
82
		await fs.writeFile(appTsxPath, updatedAppContent, "utf8");
83
		spinner.succeed("RPC client setup completed");
84
		return true;
85
	} catch (err: unknown) {
86
		spinner.fail("Failed to set up RPC client");
87
		if (err instanceof Error) {
88
			console.error(chalk.red("Error:"), err.message);
89
		} else {
90
			console.error(chalk.red("Error: Unknown error"));
91
		}
92
		return false;
93
	}
94
}
95
96
export async function createProject(
97
	projectDirectory: string,
98
	options: ProjectOptions,
99
): Promise<ProjectResult | null> {
100
	let projectName = projectDirectory;
101
102
	if (!projectName && !options.yes) {
103
		const response = await prompts({
104
			type: "text",
105
			name: "projectName",
106
			message: "What is the name of your project?",
107
			initial: "my-bhvr-app",
108
		});
109
110
		if (!response.projectName) {
111
			console.log(chalk.yellow("Project creation cancelled."));
112
			return null;
113
		}
114
115
		projectName = response.projectName;
116
	} else if (!projectName) {
117
		projectName = "my-bhvr-app";
118
	}
119
120
	let templateChoice = options.template || "default";
121
122
	if (!options.yes && !options.branch) {
123
		const templateChoices = Object.keys(TEMPLATES).map((key) => ({
124
			title: `${key} (${TEMPLATES[key]?.description})`,
125
			value: key,
126
		}));
127
128
		const templateResponse = await prompts({
129
			type: "select",
130
			name: "template",
131
			message: "Select a template:",
132
			choices: templateChoices,
133
			initial: 0,
134
		});
135
136
		if (templateResponse.template === undefined) {
137
			console.log(chalk.yellow("Project creation cancelled."));
138
			return null;
139
		}
140
141
		templateChoice = templateResponse.template;
142
	}
143
144
	const projectPath = path.resolve(process.cwd(), projectName);
145
146
	if (fs.existsSync(projectPath)) {
147
		const files = fs.readdirSync(projectPath);
148
149
		if (files.length > 0 && !options.yes) {
150
			const { overwrite } = await prompts({
151
				type: "confirm",
152
				name: "overwrite",
153
				message: `The directory ${projectName} already exists and is not empty. Do you want to overwrite it?`,
154
				initial: false,
155
			});
156
157
			if (!overwrite) {
158
				console.log(chalk.yellow("Project creation cancelled."));
159
				return null;
160
			}
161
162
			await fs.emptyDir(projectPath);
163
		}
164
	}
165
166
	fs.ensureDirSync(projectPath);
167
168
	const repoPath = options.repo || DEFAULT_REPO;
169
	const templateConfig =
170
		TEMPLATES[templateChoice as keyof typeof TEMPLATES] || TEMPLATES.default;
171
	const branch = options.branch || (templateConfig?.branch ?? "main");
172
	const repoUrl = `${repoPath}#${branch}`;
173
	const spinner = ora("Downloading template...").start();
174
175
	try {
176
		const emitter = degit(repoUrl, {
177
			cache: false,
178
			force: true,
179
			verbose: false,
180
		});
181
182
		await emitter.clone(projectPath);
183
		spinner.succeed(
184
			`Template downloaded successfully (${templateChoice} template)`,
185
		);
186
187
		const pkgJsonPath = path.join(projectPath, "package.json");
188
		if (fs.existsSync(pkgJsonPath)) {
189
			const pkgJson = await fs.readJson(pkgJsonPath);
190
			pkgJson.name = projectName;
191
			await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
192
		}
193
194
		const gitDir = path.join(projectPath, ".git");
195
		if (fs.existsSync(gitDir)) {
196
			await fs.remove(gitDir);
197
			console.log(chalk.blue("Removed .git directory"));
198
		}
199
200
		let useRpc = options.rpc;
201
202
		if (!options.yes && !options.rpc) {
203
			const rpcResponse = await prompts({
204
				type: "confirm",
205
				name: "useRpc",
206
				message: "Use Hono RPC client for type-safe API communication?",
207
				initial: false,
208
			});
209
210
			if (rpcResponse.useRpc === undefined) {
211
				console.log(chalk.yellow("Project creation cancelled."));
212
				return null;
213
			}
214
215
			useRpc = rpcResponse.useRpc;
216
		}
217
218
		if (useRpc) {
219
			await patchFilesForRPC(projectPath, templateChoice);
220
		}
221
222
		let gitInitialized = false;
223
224
		if (!options.yes) {
225
			const gitResponse = await prompts({
226
				type: "confirm",
227
				name: "initGit",
228
				message: "Initialize a git repository?",
229
				initial: true,
230
			});
231
232
			if (gitResponse.initGit) {
233
				try {
234
					spinner.start("Initializing git repository...");
235
					await execa("git", ["init"], { cwd: projectPath });
236
					spinner.succeed("Git repository initialized");
237
					gitInitialized = true;
238
				} catch (err: unknown) {
239
					spinner.fail(
240
						"Failed to initialize git repository. Is git installed?",
241
					);
242
					if (err instanceof Error) {
243
						console.error(chalk.red("Git error:"), err.message);
244
					} else {
245
						console.error(chalk.red("Git error: Unknown error"));
246
					}
247
				}
248
			}
249
		} else {
250
			try {
251
				spinner.start("Initializing git repository...");
252
				await execa("git", ["init"], { cwd: projectPath });
253
				spinner.succeed("Git repository initialized");
254
				gitInitialized = true;
255
			} catch (err) {
256
				spinner.fail("Failed to initialize git repository. Is git installed?");
257
			}
258
		}
259
260
		let dependenciesInstalled = false;
261
262
		if (!options.yes) {
263
			const depsResponse = await prompts({
264
				type: "confirm",
265
				name: "installDeps",
266
				message: "Install dependencies?",
267
				initial: true,
268
			});
269
270
			if (depsResponse.installDeps) {
271
				spinner.start("Installing dependencies...");
272
				try {
273
					await execa("bun", ["install"], { cwd: projectPath });
274
					spinner.succeed("Dependencies installed with bun");
275
					dependenciesInstalled = true;
276
				} catch (bunErr) {
277
					try {
278
						spinner.text = "Installing dependencies with npm...";
279
						await execa("npm", ["install"], { cwd: projectPath });
280
						spinner.succeed("Dependencies installed with npm");
281
						dependenciesInstalled = true;
282
					} catch (npmErr) {
283
						spinner.fail("Failed to install dependencies.");
284
						console.log(
285
							chalk.yellow(
286
								"You can install them manually after navigating to the project directory.",
287
							),
288
						);
289
					}
290
				}
291
			}
292
		} else {
293
			spinner.start("Installing dependencies...");
294
			try {
295
				await execa("bun", ["install"], { cwd: projectPath });
296
				spinner.succeed("Dependencies installed with bun");
297
				dependenciesInstalled = true;
298
			} catch (bunErr) {
299
				try {
300
					spinner.text = "Installing dependencies with npm...";
301
					await execa("npm", ["install"], { cwd: projectPath });
302
					spinner.succeed("Dependencies installed with npm");
303
					dependenciesInstalled = true;
304
				} catch (npmErr) {
305
					spinner.fail(
306
						"Failed to install dependencies. You can install them manually later.",
307
					);
308
				}
309
			}
310
		}
311
312
		return {
313
			projectName,
314
			gitInitialized,
315
			dependenciesInstalled,
316
			template: templateChoice,
317
		};
318
	} catch (err) {
319
		spinner.fail("Failed to download template");
320
		throw err;
321
	}
322
}