feat: Added biome linting c2903345
Steve Simkins · 2025-08-02 15:49 18 file(s) · +596 −558
biome.json (added) +35 −0
1 +
{
2 +
	"$schema": "https://biomejs.dev/schemas/2.1.3/schema.json",
3 +
	"vcs": {
4 +
		"enabled": false,
5 +
		"clientKind": "git",
6 +
		"useIgnoreFile": false
7 +
	},
8 +
	"files": {
9 +
		"ignoreUnknown": false
10 +
	},
11 +
	"formatter": {
12 +
		"enabled": true,
13 +
		"indentStyle": "tab",
14 +
		"indentWidth": 2
15 +
	},
16 +
	"linter": {
17 +
		"enabled": true,
18 +
		"rules": {
19 +
			"recommended": true
20 +
		}
21 +
	},
22 +
	"javascript": {
23 +
		"formatter": {
24 +
			"quoteStyle": "double"
25 +
		}
26 +
	},
27 +
	"assist": {
28 +
		"enabled": true,
29 +
		"actions": {
30 +
			"source": {
31 +
				"organizeImports": "on"
32 +
			}
33 +
		}
34 +
	}
35 +
}
package.json +56 −54
1 1
{
2 -
  "name": "create-bhvr",
3 -
  "version": "0.3.12",
4 -
  "description": "Create a new bhvr project",
5 -
  "type": "module",
6 -
  "main": "./dist/index.js",
7 -
  "types": "./dist/index.d.ts",
8 -
  "bin": {
9 -
    "create-bhvr": "./dist/index.js"
10 -
  },
11 -
  "files": [
12 -
    "dist"
13 -
  ],
14 -
  "scripts": {
15 -
    "build": "bun build src/index.ts --outdir dist --target node",
16 -
    "start": "bun ./dist/index.js",
17 -
    "test": "bun test",
18 -
    "test:watch": "bun test --watch",
19 -
    "test:coverage": "bun test --coverage"
20 -
  },
21 -
  "keywords": [
22 -
    "bun",
23 -
    "vite",
24 -
    "react",
25 -
    "hono",
26 -
    "monorepo",
27 -
    "starter",
28 -
    "template",
29 -
    "create"
30 -
  ],
31 -
  "author": "Steve Simkins",
32 -
  "license": "MIT",
33 -
  "devDependencies": {
34 -
    "@types/degit": "^2.8.6",
35 -
    "@types/figlet": "^1.7.0",
36 -
    "@types/fs-extra": "^11.0.4",
37 -
    "@types/inquirer": "^9.0.8",
38 -
    "@types/node": "^20.19.0",
39 -
    "@types/prompts": "^2.4.9",
40 -
    "ts-node": "^10.9.2",
41 -
    "typescript": "^5.8.3"
42 -
  },
43 -
  "dependencies": {
44 -
    "commander": "^11.1.0",
45 -
    "consola": "^3.4.2",
46 -
    "degit": "^2.8.4",
47 -
    "execa": "^7.2.0",
48 -
    "figlet": "^1.8.1",
49 -
    "fs-extra": "^11.3.0",
50 -
    "picocolors": "^1.1.1",
51 -
    "yocto-spinner": "^1.0.0"
52 -
  },
53 -
  "engines": {
54 -
    "node": ">=18"
55 -
  }
2 +
	"name": "create-bhvr",
3 +
	"version": "0.3.12",
4 +
	"description": "Create a new bhvr project",
5 +
	"type": "module",
6 +
	"main": "./dist/index.js",
7 +
	"types": "./dist/index.d.ts",
8 +
	"bin": {
9 +
		"create-bhvr": "./dist/index.js"
10 +
	},
11 +
	"files": [
12 +
		"dist"
13 +
	],
14 +
	"scripts": {
15 +
		"build": "bun build src/index.ts --outdir dist --target node",
16 +
		"start": "bun ./dist/index.js",
17 +
		"test": "bun test",
18 +
		"test:watch": "bun test --watch",
19 +
		"test:coverage": "bun test --coverage",
20 +
		"lint": "biome lint --write src package.json",
21 +
		"format": "biome format --write src package.json"
22 +
	},
23 +
	"keywords": [
24 +
		"bun",
25 +
		"vite",
26 +
		"react",
27 +
		"hono",
28 +
		"monorepo",
29 +
		"starter",
30 +
		"template",
31 +
		"create"
32 +
	],
33 +
	"author": "Steve Simkins",
34 +
	"license": "MIT",
35 +
	"devDependencies": {
36 +
		"@types/degit": "^2.8.6",
37 +
		"@types/figlet": "^1.7.0",
38 +
		"@types/fs-extra": "^11.0.4",
39 +
		"@types/inquirer": "^9.0.8",
40 +
		"@types/node": "^20.19.0",
41 +
		"@types/prompts": "^2.4.9",
42 +
		"ts-node": "^10.9.2",
43 +
		"typescript": "^5.8.3"
44 +
	},
45 +
	"dependencies": {
46 +
		"commander": "^11.1.0",
47 +
		"consola": "^3.4.2",
48 +
		"degit": "^2.8.4",
49 +
		"execa": "^7.2.0",
50 +
		"figlet": "^1.8.1",
51 +
		"fs-extra": "^11.3.0",
52 +
		"picocolors": "^1.1.1",
53 +
		"yocto-spinner": "^1.0.0"
54 +
	},
55 +
	"engines": {
56 +
		"node": ">=18"
57 +
	}
56 58
}
src/commands/create.ts +27 −27
4 4
import { createProject } from "../lib/create-project";
5 5
6 6
export const create = async (
7 -
  projectDirectory: string,
8 -
  options: ProjectOptions,
7 +
	projectDirectory: string,
8 +
	options: ProjectOptions,
9 9
) => {
10 -
  try {
11 -
    displayBanner();
12 -
    const result = await createProject(projectDirectory, options);
13 -
    if (result) {
14 -
      console.log(pc.green(pc.bold("🎉 Project created successfully!")));
15 -
      console.log("\nNext steps:");
16 -
      if (!result.dependenciesInstalled) {
17 -
        console.log(pc.cyan(`  cd ${result.projectName}`));
18 -
        console.log(pc.cyan("  bun install"));
19 -
      } else {
20 -
        console.log(pc.cyan(`  cd ${result.projectName}`));
21 -
      }
22 -
      console.log(pc.cyan("  bun run dev:client   # Start the client"));
23 -
      console.log(
24 -
        pc.cyan(
25 -
          "  bun run dev:server   # Start the server in another terminal",
26 -
        ),
27 -
      );
28 -
      console.log(pc.cyan("  bun run dev          # Start all"));
29 -
      process.exit(0);
30 -
    }
31 -
  } catch (err) {
32 -
    console.error(pc.red("Error creating project:"), err);
33 -
    process.exit(1);
34 -
  }
10 +
	try {
11 +
		displayBanner();
12 +
		const result = await createProject(projectDirectory, options);
13 +
		if (result) {
14 +
			console.log(pc.green(pc.bold("🎉 Project created successfully!")));
15 +
			console.log("\nNext steps:");
16 +
			if (!result.dependenciesInstalled) {
17 +
				console.log(pc.cyan(`  cd ${result.projectName}`));
18 +
				console.log(pc.cyan("  bun install"));
19 +
			} else {
20 +
				console.log(pc.cyan(`  cd ${result.projectName}`));
21 +
			}
22 +
			console.log(pc.cyan("  bun run dev:client   # Start the client"));
23 +
			console.log(
24 +
				pc.cyan(
25 +
					"  bun run dev:server   # Start the server in another terminal",
26 +
				),
27 +
			);
28 +
			console.log(pc.cyan("  bun run dev          # Start all"));
29 +
			process.exit(0);
30 +
		}
31 +
	} catch (err) {
32 +
		console.error(pc.red("Error creating project:"), err);
33 +
		process.exit(1);
34 +
	}
35 35
};
src/index.ts +19 −19
7 7
import { DEFAULT_REPO } from "./utils";
8 8
9 9
program
10 -
  .name("create-bhvr")
11 -
  .description("Create a bhvr monorepo starter project")
12 -
  .argument("[project-directory]", "directory to create the project in")
13 -
  .option("-y, --yes", "skip confirmation prompts")
14 -
  .option("--ts, --typescript", "use TypeScript (default)")
15 -
  .option(
16 -
    "--repo <repo>",
17 -
    "specify a custom GitHub repository as source",
18 -
    DEFAULT_REPO,
19 -
  )
20 -
  .option(
21 -
    "--template <template>",
22 -
    "specify a template (default, tailwind, shadcn)",
23 -
    "default",
24 -
  )
25 -
  .option("--branch <branch>", "specify a branch to use from the repository")
26 -
  .option("--rpc", "use Hono RPC client for type-safe API communication")
27 -
  .option("--linter <linter>", "specify the linter to use (eslint or biome)")
28 -
  .action(create);
10 +
	.name("create-bhvr")
11 +
	.description("Create a bhvr monorepo starter project")
12 +
	.argument("[project-directory]", "directory to create the project in")
13 +
	.option("-y, --yes", "skip confirmation prompts")
14 +
	.option("--ts, --typescript", "use TypeScript (default)")
15 +
	.option(
16 +
		"--repo <repo>",
17 +
		"specify a custom GitHub repository as source",
18 +
		DEFAULT_REPO,
19 +
	)
20 +
	.option(
21 +
		"--template <template>",
22 +
		"specify a template (default, tailwind, shadcn)",
23 +
		"default",
24 +
	)
25 +
	.option("--branch <branch>", "specify a branch to use from the repository")
26 +
	.option("--rpc", "use Hono RPC client for type-safe API communication")
27 +
	.option("--linter <linter>", "specify the linter to use (eslint or biome)")
28 +
	.action(create);
29 29
30 30
program.parse();
src/lib/create-project.ts +27 −27
5 5
import { scaffoldTemplate } from "./scaffold-template";
6 6
7 7
export async function createProject(
8 -
  projectDirectory: string,
9 -
  options: ProjectOptions,
8 +
	projectDirectory: string,
9 +
	options: ProjectOptions,
10 10
): Promise<ProjectResult | null> {
11 -
  const projectOptions = await promptForOptions({
12 -
    ...options,
13 -
    projectName: projectDirectory,
14 -
  });
11 +
	const projectOptions = await promptForOptions({
12 +
		...options,
13 +
		projectName: projectDirectory,
14 +
	});
15 15
16 -
  if (!projectOptions) {
17 -
    return null;
18 -
  }
16 +
	if (!projectOptions) {
17 +
		return null;
18 +
	}
19 19
20 -
  const scaffolded = await scaffoldTemplate(projectOptions);
20 +
	const scaffolded = await scaffoldTemplate(projectOptions);
21 21
22 -
  if (!scaffolded) {
23 -
    return null;
24 -
  }
22 +
	if (!scaffolded) {
23 +
		return null;
24 +
	}
25 25
26 -
  const gitInitialized = await initializeGit(
27 -
    projectOptions.projectName ?? projectDirectory,
28 -
    projectOptions.yes,
29 -
  );
26 +
	const gitInitialized = await initializeGit(
27 +
		projectOptions.projectName ?? projectDirectory,
28 +
		projectOptions.yes,
29 +
	);
30 30
31 -
  const dependenciesInstalled = await installDependencies(
32 -
    projectOptions.projectName ?? projectDirectory,
33 -
    projectOptions.yes,
34 -
  );
31 +
	const dependenciesInstalled = await installDependencies(
32 +
		projectOptions.projectName ?? projectDirectory,
33 +
		projectOptions.yes,
34 +
	);
35 35
36 -
  return {
37 -
    projectName: projectOptions.projectName ?? projectDirectory,
38 -
    gitInitialized,
39 -
    dependenciesInstalled,
40 -
    template: projectOptions.template ?? "default",
41 -
  };
36 +
	return {
37 +
		projectName: projectOptions.projectName ?? projectDirectory,
38 +
		gitInitialized,
39 +
		dependenciesInstalled,
40 +
		template: projectOptions.template ?? "default",
41 +
	};
42 42
}
src/lib/display-banner.ts +11 −11
3 3
import pc from "picocolors";
4 4
5 5
export function displayBanner() {
6 -
  const text = figlet.textSync("bhvr", {
7 -
    font: "Big",
8 -
    horizontalLayout: "default",
9 -
    verticalLayout: "default",
10 -
    width: 80,
11 -
    whitespaceBreak: true,
12 -
  });
6 +
	const text = figlet.textSync("bhvr", {
7 +
		font: "Big",
8 +
		horizontalLayout: "default",
9 +
		verticalLayout: "default",
10 +
		width: 80,
11 +
		whitespaceBreak: true,
12 +
	});
13 13
14 -
  console.log("\n");
15 -
  console.log(pc.yellowBright(text));
14 +
	console.log("\n");
15 +
	console.log(pc.yellowBright(text));
16 16
17 -
  consola.info(`${pc.cyan("🦫 Lets build 🦫")}`);
18 -
  consola.info(`${pc.blue("https://github.com/stevedylandev/bhvr")}\n`);
17 +
	consola.info(`${pc.cyan("🦫 Lets build 🦫")}`);
18 +
	consola.info(`${pc.blue("https://github.com/stevedylandev/bhvr")}\n`);
19 19
}
src/lib/initialize-git.ts +34 −34
5 5
import { tryCatch } from "@/utils/try-catch";
6 6
7 7
export async function initializeGit(
8 -
  projectPath: string,
9 -
  skipConfirmation?: boolean,
8 +
	projectPath: string,
9 +
	skipConfirmation?: boolean,
10 10
): Promise<boolean> {
11 -
  if (!skipConfirmation) {
12 -
    const { data: gitResponse, error } = await tryCatch(
13 -
      consola.prompt("Initialize a git repository?", {
14 -
        type: "confirm",
15 -
        initial: true,
16 -
        cancel: "reject",
17 -
      }),
18 -
    );
11 +
	if (!skipConfirmation) {
12 +
		const { data: gitResponse, error } = await tryCatch(
13 +
			consola.prompt("Initialize a git repository?", {
14 +
				type: "confirm",
15 +
				initial: true,
16 +
				cancel: "reject",
17 +
			}),
18 +
		);
19 19
20 -
    if (error) {
21 -
      console.log(pc.yellow("Project creation cancelled."));
22 -
      return false;
23 -
    }
20 +
		if (error) {
21 +
			console.log(pc.yellow("Project creation cancelled."));
22 +
			return false;
23 +
		}
24 24
25 -
    if (!gitResponse) {
26 -
      return false;
27 -
    }
28 -
  }
25 +
		if (!gitResponse) {
26 +
			return false;
27 +
		}
28 +
	}
29 29
30 -
  const spinner = yoctoSpinner({
31 -
    text: "Initializing git repository...",
32 -
  }).start();
30 +
	const spinner = yoctoSpinner({
31 +
		text: "Initializing git repository...",
32 +
	}).start();
33 33
34 -
  try {
35 -
    await execa("git", ["init"], { cwd: projectPath });
36 -
    spinner.success("Git repository initialized");
37 -
    return true;
38 -
  } catch (err: unknown) {
39 -
    spinner.error("Failed to initialize git repository. Is git installed?");
40 -
    if (err instanceof Error) {
41 -
      consola.error(pc.red("Git error:"), err.message);
42 -
    } else {
43 -
      consola.error(pc.red("Git error: Unknown error"));
44 -
    }
45 -
    return false;
46 -
  }
34 +
	try {
35 +
		await execa("git", ["init"], { cwd: projectPath });
36 +
		spinner.success("Git repository initialized");
37 +
		return true;
38 +
	} catch (err: unknown) {
39 +
		spinner.error("Failed to initialize git repository. Is git installed?");
40 +
		if (err instanceof Error) {
41 +
			consola.error(pc.red("Git error:"), err.message);
42 +
		} else {
43 +
			consola.error(pc.red("Git error: Unknown error"));
44 +
		}
45 +
		return false;
46 +
	}
47 47
}
src/lib/install-dependencies.ts +42 −42
5 5
import { tryCatch } from "@/utils/try-catch";
6 6
7 7
async function getPackageManager(): Promise<"bun"> {
8 -
  const { error } = await tryCatch(execa("bun", ["--version"]));
8 +
	const { error } = await tryCatch(execa("bun", ["--version"]));
9 9
10 -
  if (error) {
11 -
    consola.error(new Error("Bun is not installed."));
12 -
    consola.warn("Please install bun from https://bun.sh/");
13 -
    process.exit(1);
14 -
  }
10 +
	if (error) {
11 +
		consola.error(new Error("Bun is not installed."));
12 +
		consola.warn("Please install bun from https://bun.sh/");
13 +
		process.exit(1);
14 +
	}
15 15
16 -
  return "bun";
16 +
	return "bun";
17 17
}
18 18
19 19
export async function installDependencies(
20 -
  projectPath: string,
21 -
  skipConfirmation?: boolean,
20 +
	projectPath: string,
21 +
	skipConfirmation?: boolean,
22 22
): Promise<boolean> {
23 -
  if (!skipConfirmation) {
24 -
    const { data: depsResponse, error } = await tryCatch(
25 -
      consola.prompt("Install dependencies?", {
26 -
        type: "confirm",
27 -
        initial: true,
28 -
        cancel: "reject",
29 -
      }),
30 -
    );
23 +
	if (!skipConfirmation) {
24 +
		const { data: depsResponse, error } = await tryCatch(
25 +
			consola.prompt("Install dependencies?", {
26 +
				type: "confirm",
27 +
				initial: true,
28 +
				cancel: "reject",
29 +
			}),
30 +
		);
31 31
32 -
    if (error) {
33 -
      console.log(pc.yellow("Project creation cancelled."));
34 -
      return false;
35 -
    }
32 +
		if (error) {
33 +
			console.log(pc.yellow("Project creation cancelled."));
34 +
			return false;
35 +
		}
36 36
37 -
    if (!depsResponse) {
38 -
      return false;
39 -
    }
40 -
  }
37 +
		if (!depsResponse) {
38 +
			return false;
39 +
		}
40 +
	}
41 41
42 -
  const packageManager = await getPackageManager();
42 +
	const packageManager = await getPackageManager();
43 43
44 -
  const spinner = yoctoSpinner({
45 -
    text: `Installing dependencies with ${packageManager}...`,
46 -
  }).start();
44 +
	const spinner = yoctoSpinner({
45 +
		text: `Installing dependencies with ${packageManager}...`,
46 +
	}).start();
47 47
48 -
  try {
49 -
    await execa(packageManager, ["install"], { cwd: projectPath });
50 -
    spinner.success(`Dependencies installed with ${packageManager}`);
51 -
    return true;
52 -
  } catch (_err) {
53 -
    spinner.error("Failed to install dependencies.");
54 -
    console.log(
55 -
      pc.yellow(
56 -
        "You can install them manually after navigating to the project directory.",
57 -
      ),
58 -
    );
59 -
    return false;
60 -
  }
48 +
	try {
49 +
		await execa(packageManager, ["install"], { cwd: projectPath });
50 +
		spinner.success(`Dependencies installed with ${packageManager}`);
51 +
		return true;
52 +
	} catch (_err) {
53 +
		spinner.error("Failed to install dependencies.");
54 +
		console.log(
55 +
			pc.yellow(
56 +
				"You can install them manually after navigating to the project directory.",
57 +
			),
58 +
		);
59 +
		return false;
60 +
	}
61 61
}
src/lib/patch-files-rpc.ts +61 −61
5 5
import pc from "picocolors";
6 6
import yoctoSpinner from "yocto-spinner";
7 7
import {
8 -
  defaultTemplate,
9 -
  honoClientTemplate,
10 -
  honoRpcTemplate,
11 -
  shadcnTemplate,
12 -
  tailwindTemplate,
8 +
	defaultTemplate,
9 +
	honoClientTemplate,
10 +
	honoRpcTemplate,
11 +
	shadcnTemplate,
12 +
	tailwindTemplate,
13 13
} from "@/utils/templates";
14 14
15 15
export async function patchFilesForRPC(
16 -
  projectPath: string,
17 -
  templateChoice: string,
16 +
	projectPath: string,
17 +
	templateChoice: string,
18 18
): Promise<boolean> {
19 -
  const spinner = yoctoSpinner({ text: "Setting up RPC client..." }).start();
19 +
	const spinner = yoctoSpinner({ text: "Setting up RPC client..." }).start();
20 20
21 -
  try {
22 -
    // 1. Update client package.json to ensure hono client is installed
23 -
    const clientPkgPath = path.join(projectPath, "client", "package.json");
24 -
    const clientPkg = await fs.readJson(clientPkgPath);
21 +
	try {
22 +
		// 1. Update client package.json to ensure hono client is installed
23 +
		const clientPkgPath = path.join(projectPath, "client", "package.json");
24 +
		const clientPkg = await fs.readJson(clientPkgPath);
25 25
26 -
    if (!clientPkg.dependencies.hono) {
27 -
      await execa("bun", ["install", "hono"], { cwd: projectPath });
28 -
    }
26 +
		if (!clientPkg.dependencies.hono) {
27 +
			await execa("bun", ["install", "hono"], { cwd: projectPath });
28 +
		}
29 29
30 -
    await fs.writeJson(clientPkgPath, clientPkg, { spaces: 2 });
30 +
		await fs.writeJson(clientPkgPath, clientPkg, { spaces: 2 });
31 31
32 -
    // 2. Update server package.json dev script for RPC
33 -
    const serverPkgPath = path.join(projectPath, "server", "package.json");
34 -
    const serverPkg = await fs.readJson(serverPkgPath);
32 +
		// 2. Update server package.json dev script for RPC
33 +
		const serverPkgPath = path.join(projectPath, "server", "package.json");
34 +
		const serverPkg = await fs.readJson(serverPkgPath);
35 35
36 -
    // Update the dev script to include TypeScript compilation
37 -
    serverPkg.scripts.dev = "bun --watch run src/index.ts && tsc --watch";
36 +
		// Update the dev script to include TypeScript compilation
37 +
		serverPkg.scripts.dev = "bun --watch run src/index.ts && tsc --watch";
38 38
39 -
    await fs.writeJson(serverPkgPath, serverPkg, { spaces: 2 });
39 +
		await fs.writeJson(serverPkgPath, serverPkg, { spaces: 2 });
40 40
41 -
    // 3. Server modification for RPC export type (no client imports)
42 -
    const serverIndexPath = path.join(projectPath, "server", "src", "index.ts");
43 -
    await fs.writeFile(serverIndexPath, honoRpcTemplate, "utf8");
41 +
		// 3. Server modification for RPC export type (no client imports)
42 +
		const serverIndexPath = path.join(projectPath, "server", "src", "index.ts");
43 +
		await fs.writeFile(serverIndexPath, honoRpcTemplate, "utf8");
44 44
45 -
    // 4. Create separate client helper file
46 -
    const clientHelperPath = path.join(
47 -
      projectPath,
48 -
      "server",
49 -
      "src",
50 -
      "client.ts",
51 -
    );
52 -
    await fs.writeFile(clientHelperPath, honoClientTemplate, "utf8");
45 +
		// 4. Create separate client helper file
46 +
		const clientHelperPath = path.join(
47 +
			projectPath,
48 +
			"server",
49 +
			"src",
50 +
			"client.ts",
51 +
		);
52 +
		await fs.writeFile(clientHelperPath, honoClientTemplate, "utf8");
53 53
54 -
    // 5. Update App.tsx based on template selection using switch statement
55 -
    const appTsxPath = path.join(projectPath, "client", "src", "App.tsx");
54 +
		// 5. Update App.tsx based on template selection using switch statement
55 +
		const appTsxPath = path.join(projectPath, "client", "src", "App.tsx");
56 56
57 -
    // Determine template content based on the template type
58 -
    let updatedAppContent: string;
57 +
		// Determine template content based on the template type
58 +
		let updatedAppContent: string;
59 59
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 -
    }
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 72
73 -
    await fs.writeFile(appTsxPath, updatedAppContent, "utf8");
74 -
    spinner.success("RPC client setup completed");
75 -
    return true;
76 -
  } catch (err: unknown) {
77 -
    spinner.error("Failed to set up RPC client");
78 -
    if (err instanceof Error) {
79 -
      consola.error(pc.red("Error:"), err.message);
80 -
    } else {
81 -
      consola.error(pc.red("Error: Unknown error"));
82 -
    }
83 -
    return false;
84 -
  }
73 +
		await fs.writeFile(appTsxPath, updatedAppContent, "utf8");
74 +
		spinner.success("RPC client setup completed");
75 +
		return true;
76 +
	} catch (err: unknown) {
77 +
		spinner.error("Failed to set up RPC client");
78 +
		if (err instanceof Error) {
79 +
			consola.error(pc.red("Error:"), err.message);
80 +
		} else {
81 +
			consola.error(pc.red("Error: Unknown error"));
82 +
		}
83 +
		return false;
84 +
	}
85 85
}
src/lib/prompt-for-options.ts +77 −77
5 5
import { tryCatch } from "@/utils/try-catch";
6 6
7 7
export async function promptForOptions(
8 -
  options: ProjectOptions,
8 +
	options: ProjectOptions,
9 9
): Promise<ProjectOptions> {
10 -
  let projectName = options.projectName;
10 +
	let projectName = options.projectName;
11 11
12 -
  if (!projectName && !options.yes) {
13 -
    const { data, error } = await tryCatch(
14 -
      consola.prompt(pc.yellow("What is the name of your project?"), {
15 -
        type: "text",
16 -
        default: "my-bhvr-app",
17 -
        placeholder: "my-bhvr-app",
18 -
        cancel: "reject",
19 -
      }),
20 -
    );
12 +
	if (!projectName && !options.yes) {
13 +
		const { data, error } = await tryCatch(
14 +
			consola.prompt(pc.yellow("What is the name of your project?"), {
15 +
				type: "text",
16 +
				default: "my-bhvr-app",
17 +
				placeholder: "my-bhvr-app",
18 +
				cancel: "reject",
19 +
			}),
20 +
		);
21 21
22 -
    if (!data || error) {
23 -
      consola.error(pc.red("Project creation cancelled."));
24 -
      process.exit(1);
25 -
    }
22 +
		if (!data || error) {
23 +
			consola.error(pc.red("Project creation cancelled."));
24 +
			process.exit(1);
25 +
		}
26 26
27 -
    projectName = data;
28 -
  }
27 +
		projectName = data;
28 +
	}
29 29
30 -
  let templateChoice = options.template || "default";
30 +
	let templateChoice = options.template || "default";
31 31
32 -
  if (!options.yes && !options.branch) {
33 -
    const templateChoices = Object.keys(TEMPLATES).map((key) => ({
34 -
      label: `${key} (${TEMPLATES[key]?.description})`,
35 -
      value: key,
36 -
    }));
32 +
	if (!options.yes && !options.branch) {
33 +
		const templateChoices = Object.keys(TEMPLATES).map((key) => ({
34 +
			label: `${key} (${TEMPLATES[key]?.description})`,
35 +
			value: key,
36 +
		}));
37 37
38 -
    const { data, error } = await tryCatch(
39 -
      consola.prompt(pc.yellow("Select a template:"), {
40 -
        type: "select",
41 -
        options: templateChoices,
42 -
        initial: "default",
43 -
        cancel: "reject",
44 -
      }),
45 -
    );
38 +
		const { data, error } = await tryCatch(
39 +
			consola.prompt(pc.yellow("Select a template:"), {
40 +
				type: "select",
41 +
				options: templateChoices,
42 +
				initial: "default",
43 +
				cancel: "reject",
44 +
			}),
45 +
		);
46 46
47 -
    if (!data || error) {
48 -
      consola.error("Project creation cancelled.");
49 -
      process.exit(1);
50 -
    }
47 +
		if (!data || error) {
48 +
			consola.error("Project creation cancelled.");
49 +
			process.exit(1);
50 +
		}
51 51
52 -
    templateChoice = data;
53 -
  }
52 +
		templateChoice = data;
53 +
	}
54 54
55 -
  let useRpc = options.rpc;
55 +
	let useRpc = options.rpc;
56 56
57 -
  if (!options.yes && !options.rpc) {
58 -
    const { data: rpcResponse, error } = await tryCatch(
59 -
      consola.prompt("Use Hono RPC client for type-safe API communication?", {
60 -
        type: "confirm",
61 -
        initial: false,
62 -
      }),
63 -
    );
57 +
	if (!options.yes && !options.rpc) {
58 +
		const { data: rpcResponse, error } = await tryCatch(
59 +
			consola.prompt("Use Hono RPC client for type-safe API communication?", {
60 +
				type: "confirm",
61 +
				initial: false,
62 +
			}),
63 +
		);
64 64
65 -
    if (error) {
66 -
      consola.error("Project creation cancelled.");
67 -
      process.exit(1);
68 -
    }
65 +
		if (error) {
66 +
			consola.error("Project creation cancelled.");
67 +
			process.exit(1);
68 +
		}
69 69
70 -
    useRpc = rpcResponse;
71 -
  }
70 +
		useRpc = rpcResponse;
71 +
	}
72 72
73 -
  let linter = options.linter;
73 +
	let linter = options.linter;
74 74
75 -
  if (!options.yes && !options.linter) {
76 -
    const { data: linterResponse, error } = await tryCatch(
77 -
      consola.prompt("Select a linter:", {
78 -
        type: "select",
79 -
        options: [
80 -
          { label: "ESLint (default)", value: "eslint" },
81 -
          { label: "Biome", value: "biome" },
82 -
        ],
83 -
        initial: "eslint",
84 -
        cancel: "reject",
85 -
      }),
86 -
    );
75 +
	if (!options.yes && !options.linter) {
76 +
		const { data: linterResponse, error } = await tryCatch(
77 +
			consola.prompt("Select a linter:", {
78 +
				type: "select",
79 +
				options: [
80 +
					{ label: "ESLint (default)", value: "eslint" },
81 +
					{ label: "Biome", value: "biome" },
82 +
				],
83 +
				initial: "eslint",
84 +
				cancel: "reject",
85 +
			}),
86 +
		);
87 87
88 -
    if (error) {
89 -
      console.log(pc.yellow("Project creation cancelled."));
90 -
      process.exit(1);
91 -
    }
88 +
		if (error) {
89 +
			console.log(pc.yellow("Project creation cancelled."));
90 +
			process.exit(1);
91 +
		}
92 92
93 -
    linter = linterResponse as "eslint" | "biome";
94 -
  }
93 +
		linter = linterResponse as "eslint" | "biome";
94 +
	}
95 95
96 -
  return {
97 -
    ...options,
98 -
    projectName,
99 -
    template: templateChoice,
100 -
    rpc: useRpc,
101 -
    linter,
102 -
  };
96 +
	return {
97 +
		...options,
98 +
		projectName,
99 +
		template: templateChoice,
100 +
		rpc: useRpc,
101 +
		linter,
102 +
	};
103 103
}
src/lib/scaffold-template.ts +46 −46
10 10
import { setupBiome } from "./setup-biome";
11 11
12 12
export async function scaffoldTemplate(
13 -
  options: Required<ProjectOptions>,
13 +
	options: Required<ProjectOptions>,
14 14
): Promise<boolean> {
15 -
  const { projectName, template, repo, branch, rpc, linter } = options;
15 +
	const { projectName, template, repo, branch, rpc, linter } = options;
16 16
17 -
  const projectPath = path.resolve(process.cwd(), projectName);
17 +
	const projectPath = path.resolve(process.cwd(), projectName);
18 18
19 -
  if (fs.existsSync(projectPath)) {
20 -
    const files = fs.readdirSync(projectPath);
21 -
    if (files.length > 0) {
22 -
      await fs.emptyDir(projectPath);
23 -
    }
24 -
  }
19 +
	if (fs.existsSync(projectPath)) {
20 +
		const files = fs.readdirSync(projectPath);
21 +
		if (files.length > 0) {
22 +
			await fs.emptyDir(projectPath);
23 +
		}
24 +
	}
25 25
26 -
  fs.ensureDirSync(projectPath);
26 +
	fs.ensureDirSync(projectPath);
27 27
28 -
  const repoPath = repo || DEFAULT_REPO;
29 -
  const templateConfig =
30 -
    TEMPLATES[template as keyof typeof TEMPLATES] || TEMPLATES.default;
31 -
  const repoBranch = branch || (templateConfig?.branch ?? "main");
32 -
  const repoUrl = `${repoPath}#${repoBranch}`;
33 -
  const spinner = yoctoSpinner({ text: "Downloading template..." }).start();
28 +
	const repoPath = repo || DEFAULT_REPO;
29 +
	const templateConfig =
30 +
		TEMPLATES[template as keyof typeof TEMPLATES] || TEMPLATES.default;
31 +
	const repoBranch = branch || (templateConfig?.branch ?? "main");
32 +
	const repoUrl = `${repoPath}#${repoBranch}`;
33 +
	const spinner = yoctoSpinner({ text: "Downloading template..." }).start();
34 34
35 -
  try {
36 -
    const emitter = degit(repoUrl, {
37 -
      cache: false,
38 -
      force: true,
39 -
      verbose: false,
40 -
    });
35 +
	try {
36 +
		const emitter = degit(repoUrl, {
37 +
			cache: false,
38 +
			force: true,
39 +
			verbose: false,
40 +
		});
41 41
42 -
    await emitter.clone(projectPath);
43 -
    spinner.success(`Template downloaded successfully (${template} template)`);
42 +
		await emitter.clone(projectPath);
43 +
		spinner.success(`Template downloaded successfully (${template} template)`);
44 44
45 -
    const pkgJsonPath = path.join(projectPath, "package.json");
46 -
    if (fs.existsSync(pkgJsonPath)) {
47 -
      const pkgJson = await fs.readJson(pkgJsonPath);
48 -
      pkgJson.name = projectName;
49 -
      await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
50 -
    }
45 +
		const pkgJsonPath = path.join(projectPath, "package.json");
46 +
		if (fs.existsSync(pkgJsonPath)) {
47 +
			const pkgJson = await fs.readJson(pkgJsonPath);
48 +
			pkgJson.name = projectName;
49 +
			await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
50 +
		}
51 51
52 -
    const gitDir = path.join(projectPath, ".git");
53 -
    if (fs.existsSync(gitDir)) {
54 -
      await fs.remove(gitDir);
55 -
      console.log(pc.blue("Removed .git directory"));
56 -
    }
52 +
		const gitDir = path.join(projectPath, ".git");
53 +
		if (fs.existsSync(gitDir)) {
54 +
			await fs.remove(gitDir);
55 +
			console.log(pc.blue("Removed .git directory"));
56 +
		}
57 57
58 -
    if (rpc) {
59 -
      await patchFilesForRPC(projectPath, template);
60 -
    }
58 +
		if (rpc) {
59 +
			await patchFilesForRPC(projectPath, template);
60 +
		}
61 61
62 -
    if (linter === "biome") {
63 -
      await setupBiome(projectPath);
64 -
    }
62 +
		if (linter === "biome") {
63 +
			await setupBiome(projectPath);
64 +
		}
65 65
66 -
    return true;
67 -
  } catch (err) {
68 -
    spinner.error("Failed to download template");
69 -
    throw err;
70 -
  }
66 +
		return true;
67 +
	} catch (err) {
68 +
		spinner.error("Failed to download template");
69 +
		throw err;
70 +
	}
71 71
}
src/lib/setup-biome.ts +69 −69
5 5
import yoctoSpinner from "yocto-spinner";
6 6
7 7
export async function setupBiome(projectPath: string): Promise<void> {
8 -
  const spinner = yoctoSpinner({ text: "Setting up Biome..." }).start();
9 -
  try {
10 -
    const clientPath = path.join(projectPath, "client");
11 -
    const clientPkgJsonPath = path.join(clientPath, "package.json");
12 -
    const eslintConfigPath = path.join(clientPath, "eslint.config.js");
8 +
	const spinner = yoctoSpinner({ text: "Setting up Biome..." }).start();
9 +
	try {
10 +
		const clientPath = path.join(projectPath, "client");
11 +
		const clientPkgJsonPath = path.join(clientPath, "package.json");
12 +
		const eslintConfigPath = path.join(clientPath, "eslint.config.js");
13 13
14 -
    // Remove ESLint config file
15 -
    if (fs.existsSync(eslintConfigPath)) {
16 -
      await fs.remove(eslintConfigPath);
17 -
    }
14 +
		// Remove ESLint config file
15 +
		if (fs.existsSync(eslintConfigPath)) {
16 +
			await fs.remove(eslintConfigPath);
17 +
		}
18 18
19 -
    // Read client package.json and remove ESLint dependencies
20 -
    const clientPkgJson = await fs.readJson(clientPkgJsonPath);
21 -
    const devDependencies = clientPkgJson.devDependencies || {};
22 -
    const eslintDeps = Object.keys(devDependencies).filter(
23 -
      (dep) => dep.includes("eslint") || dep.includes("@eslint"),
24 -
    );
19 +
		// Read client package.json and remove ESLint dependencies
20 +
		const clientPkgJson = await fs.readJson(clientPkgJsonPath);
21 +
		const devDependencies = clientPkgJson.devDependencies || {};
22 +
		const eslintDeps = Object.keys(devDependencies).filter(
23 +
			(dep) => dep.includes("eslint") || dep.includes("@eslint"),
24 +
		);
25 25
26 -
    if (eslintDeps.length > 0) {
27 -
      spinner.text = "Replacing ESLint dependencies...";
28 -
      await execa("bun", ["remove", ...eslintDeps], { cwd: clientPath });
29 -
    }
26 +
		if (eslintDeps.length > 0) {
27 +
			spinner.text = "Replacing ESLint dependencies...";
28 +
			await execa("bun", ["remove", ...eslintDeps], { cwd: clientPath });
29 +
		}
30 30
31 -
    // Install Biome in the root of the project
32 -
    spinner.text = "Installing Biome...";
33 -
    await execa("bun", ["add", "-D", "@biomejs/biome"], { cwd: projectPath });
31 +
		// Install Biome in the root of the project
32 +
		spinner.text = "Installing Biome...";
33 +
		await execa("bun", ["add", "-D", "@biomejs/biome"], { cwd: projectPath });
34 34
35 -
    // Create biome.json in the root of the project
36 -
    spinner.text = "Creating biome.json...";
37 -
    const biomeConfig = {
38 -
      $schema: "https://biomejs.dev/schemas/1.7.3/schema.json",
39 -
      vcs: {
40 -
        enabled: true,
41 -
        clientKind: "git",
42 -
        useIgnoreFile: true,
43 -
      },
44 -
      files: { ignoreUnknown: false, ignore: [] },
45 -
      formatter: { enabled: true },
46 -
      organizeImports: { enabled: true },
47 -
      linter: {
48 -
        enabled: true,
49 -
        rules: {
50 -
          recommended: true,
51 -
        },
52 -
      },
53 -
    };
54 -
    const biomeConfigPath = path.join(projectPath, "biome.json");
55 -
    await fs.writeJson(biomeConfigPath, biomeConfig, { spaces: 2 });
35 +
		// Create biome.json in the root of the project
36 +
		spinner.text = "Creating biome.json...";
37 +
		const biomeConfig = {
38 +
			$schema: "https://biomejs.dev/schemas/1.7.3/schema.json",
39 +
			vcs: {
40 +
				enabled: true,
41 +
				clientKind: "git",
42 +
				useIgnoreFile: true,
43 +
			},
44 +
			files: { ignoreUnknown: false, ignore: [] },
45 +
			formatter: { enabled: true },
46 +
			organizeImports: { enabled: true },
47 +
			linter: {
48 +
				enabled: true,
49 +
				rules: {
50 +
					recommended: true,
51 +
				},
52 +
			},
53 +
		};
54 +
		const biomeConfigPath = path.join(projectPath, "biome.json");
55 +
		await fs.writeJson(biomeConfigPath, biomeConfig, { spaces: 2 });
56 56
57 -
    // Update client package.json scripts to remove lint
58 -
    spinner.text = "Updating scripts in client/package.json...";
59 -
    const newClientPkgJson = await fs.readJson(clientPkgJsonPath);
60 -
    if (newClientPkgJson.scripts || newClientPkgJson.scripts.lint) {
61 -
      delete newClientPkgJson.scripts.lint;
62 -
    }
63 -
    await fs.writeJson(clientPkgJsonPath, newClientPkgJson, { spaces: 2 });
57 +
		// Update client package.json scripts to remove lint
58 +
		spinner.text = "Updating scripts in client/package.json...";
59 +
		const newClientPkgJson = await fs.readJson(clientPkgJsonPath);
60 +
		if (newClientPkgJson.scripts || newClientPkgJson.scripts.lint) {
61 +
			delete newClientPkgJson.scripts.lint;
62 +
		}
63 +
		await fs.writeJson(clientPkgJsonPath, newClientPkgJson, { spaces: 2 });
64 64
65 -
    // Update root package.json with biome scripts
66 -
    spinner.text = "Updating scripts in root/package.json...";
67 -
    const rootPkgJsonPath = path.join(projectPath, "package.json");
68 -
    if (fs.existsSync(rootPkgJsonPath)) {
69 -
      const rootPkgJson = await fs.readJson(rootPkgJsonPath);
70 -
      rootPkgJson.scripts = rootPkgJson.scripts || {};
71 -
      rootPkgJson.scripts.format = "biome format . --write";
72 -
      rootPkgJson.scripts.lint = "biome lint .";
73 -
      await fs.writeJson(rootPkgJsonPath, rootPkgJson, { spaces: 2 });
74 -
    }
65 +
		// Update root package.json with biome scripts
66 +
		spinner.text = "Updating scripts in root/package.json...";
67 +
		const rootPkgJsonPath = path.join(projectPath, "package.json");
68 +
		if (fs.existsSync(rootPkgJsonPath)) {
69 +
			const rootPkgJson = await fs.readJson(rootPkgJsonPath);
70 +
			rootPkgJson.scripts = rootPkgJson.scripts || {};
71 +
			rootPkgJson.scripts.format = "biome format . --write";
72 +
			rootPkgJson.scripts.lint = "biome lint .";
73 +
			await fs.writeJson(rootPkgJsonPath, rootPkgJson, { spaces: 2 });
74 +
		}
75 75
76 -
    spinner.success("Biome setup complete.");
77 -
  } catch (error) {
78 -
    spinner.error("Biome setup failed.");
79 -
    if (error instanceof Error) {
80 -
      console.error(pc.red("\nError:"), error.message);
81 -
    } else {
82 -
      console.error(pc.red("\nError: Unknown error during Biome setup."));
83 -
    }
84 -
  }
76 +
		spinner.success("Biome setup complete.");
77 +
	} catch (error) {
78 +
		spinner.error("Biome setup failed.");
79 +
		if (error instanceof Error) {
80 +
			console.error(pc.red("\nError:"), error.message);
81 +
		} else {
82 +
			console.error(pc.red("\nError: Unknown error during Biome setup."));
83 +
		}
84 +
	}
85 85
}
src/types.test.ts +21 −21
2 2
import type { TemplateInfo, ProjectOptions, ProjectResult } from "./types";
3 3
4 4
describe("TypeScript types", () => {
5 -
  it("should define correct type structures", () => {
6 -
    // Test that types can be instantiated correctly
7 -
    const templateInfo: TemplateInfo = {
8 -
      branch: "main",
9 -
      description: "Test template"
10 -
    };
5 +
	it("should define correct type structures", () => {
6 +
		// Test that types can be instantiated correctly
7 +
		const templateInfo: TemplateInfo = {
8 +
			branch: "main",
9 +
			description: "Test template",
10 +
		};
11 11
12 -
    const options: ProjectOptions = {
13 -
      projectName: "test-project",
14 -
      linter: "biome"
15 -
    };
12 +
		const options: ProjectOptions = {
13 +
			projectName: "test-project",
14 +
			linter: "biome",
15 +
		};
16 16
17 -
    const result: ProjectResult = {
18 -
      projectName: "test-project",
19 -
      gitInitialized: true,
20 -
      dependenciesInstalled: false,
21 -
      template: "default"
22 -
    };
17 +
		const result: ProjectResult = {
18 +
			projectName: "test-project",
19 +
			gitInitialized: true,
20 +
			dependenciesInstalled: false,
21 +
			template: "default",
22 +
		};
23 23
24 -
    expect(templateInfo.branch).toBe("main");
25 -
    expect(options.linter).toBe("biome");
26 -
    expect(result.projectName).toBe("test-project");
27 -
  });
28 -
});
24 +
		expect(templateInfo.branch).toBe("main");
25 +
		expect(options.linter).toBe("biome");
26 +
		expect(result.projectName).toBe("test-project");
27 +
	});
28 +
});
src/types.ts +14 −14
1 1
export interface TemplateInfo {
2 -
  branch: string;
3 -
  description: string;
2 +
	branch: string;
3 +
	description: string;
4 4
}
5 5
6 6
export type ProjectOptions = {
7 -
  projectName?: string;
8 -
  yes?: boolean;
9 -
  typescript?: boolean;
10 -
  repo?: string;
11 -
  template?: string;
12 -
  branch?: string;
13 -
  rpc?: boolean;
14 -
  linter?: "eslint" | "biome";
7 +
	projectName?: string;
8 +
	yes?: boolean;
9 +
	typescript?: boolean;
10 +
	repo?: string;
11 +
	template?: string;
12 +
	branch?: string;
13 +
	rpc?: boolean;
14 +
	linter?: "eslint" | "biome";
15 15
};
16 16
17 17
export interface ProjectResult {
18 -
  projectName: string;
19 -
  gitInitialized: boolean;
20 -
  dependenciesInstalled: boolean;
21 -
  template: string;
18 +
	projectName: string;
19 +
	gitInitialized: boolean;
20 +
	dependenciesInstalled: boolean;
21 +
	template: string;
22 22
}
src/utils/constants.test.ts +10 −10
2 2
import { DEFAULT_REPO } from "./constants";
3 3
4 4
describe("constants", () => {
5 -
  it("should have correct default repository", () => {
6 -
    expect(DEFAULT_REPO).toBe("stevedylandev/bhvr");
7 -
  });
5 +
	it("should have correct default repository", () => {
6 +
		expect(DEFAULT_REPO).toBe("stevedylandev/bhvr");
7 +
	});
8 8
9 -
  it("should be a string", () => {
10 -
    expect(typeof DEFAULT_REPO).toBe("string");
11 -
  });
9 +
	it("should be a string", () => {
10 +
		expect(typeof DEFAULT_REPO).toBe("string");
11 +
	});
12 12
13 -
  it("should follow GitHub repo format", () => {
14 -
    expect(DEFAULT_REPO).toMatch(/^[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+$/);
15 -
  });
16 -
});
13 +
	it("should follow GitHub repo format", () => {
14 +
		expect(DEFAULT_REPO).toMatch(/^[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+$/);
15 +
	});
16 +
});
src/utils/index.ts +0 −1
1 1
export * from "./constants";
2 2
export * from "./templates";
3 3
export * from "./try-catch";
4 -
src/utils/try-catch.test.ts +36 −34
2 2
import { tryCatch } from "./try-catch";
3 3
4 4
describe("tryCatch", () => {
5 -
  it("should return success result for resolved promise", async () => {
6 -
    const result = await tryCatch(Promise.resolve("success"));
7 -
    
8 -
    expect(result.data).toBe("success");
9 -
    expect(result.error).toBeNull();
10 -
  });
5 +
	it("should return success result for resolved promise", async () => {
6 +
		const result = await tryCatch(Promise.resolve("success"));
11 7
12 -
  it("should return failure result for rejected promise", async () => {
13 -
    const error = new Error("test error");
14 -
    const result = await tryCatch(Promise.reject(error));
15 -
    
16 -
    expect(result.data).toBeNull();
17 -
    expect(result.error).toBe(error);
18 -
  });
8 +
		expect(result.data).toBe("success");
9 +
		expect(result.error).toBeNull();
10 +
	});
19 11
20 -
  it("should handle async function that throws", async () => {
21 -
    const asyncFunction = async () => {
22 -
      throw new Error("async error");
23 -
    };
24 -
    
25 -
    const result = await tryCatch(asyncFunction());
26 -
    
27 -
    expect(result.data).toBeNull();
28 -
    expect(result.error).toBeInstanceOf(Error);
29 -
    expect((result.error as Error).message).toBe("async error");
30 -
  });
12 +
	it("should return failure result for rejected promise", async () => {
13 +
		const error = new Error("test error");
14 +
		const result = await tryCatch(Promise.reject(error));
31 15
32 -
  it("should handle different data types", async () => {
33 -
    const objectResult = await tryCatch(Promise.resolve({ id: 1, name: "test" }));
34 -
    const numberResult = await tryCatch(Promise.resolve(42));
35 -
    const booleanResult = await tryCatch(Promise.resolve(true));
36 -
    
37 -
    expect(objectResult.data).toEqual({ id: 1, name: "test" });
38 -
    expect(numberResult.data).toBe(42);
39 -
    expect(booleanResult.data).toBe(true);
40 -
  });
41 -
});
16 +
		expect(result.data).toBeNull();
17 +
		expect(result.error).toBe(error);
18 +
	});
19 +
20 +
	it("should handle async function that throws", async () => {
21 +
		const asyncFunction = async () => {
22 +
			throw new Error("async error");
23 +
		};
24 +
25 +
		const result = await tryCatch(asyncFunction());
26 +
27 +
		expect(result.data).toBeNull();
28 +
		expect(result.error).toBeInstanceOf(Error);
29 +
		expect((result.error as Error).message).toBe("async error");
30 +
	});
31 +
32 +
	it("should handle different data types", async () => {
33 +
		const objectResult = await tryCatch(
34 +
			Promise.resolve({ id: 1, name: "test" }),
35 +
		);
36 +
		const numberResult = await tryCatch(Promise.resolve(42));
37 +
		const booleanResult = await tryCatch(Promise.resolve(true));
38 +
39 +
		expect(objectResult.data).toEqual({ id: 1, name: "test" });
40 +
		expect(numberResult.data).toBe(42);
41 +
		expect(booleanResult.data).toBe(true);
42 +
	});
43 +
});
src/utils/try-catch.ts +11 −11
1 1
type Success<T> = {
2 -
  data: T;
3 -
  error: null;
2 +
	data: T;
3 +
	error: null;
4 4
};
5 5
6 6
type Failure<E> = {
7 -
  data: null;
8 -
  error: E;
7 +
	data: null;
8 +
	error: E;
9 9
};
10 10
11 11
type Result<T, E = Error> = Success<T> | Failure<E>;
12 12
13 13
// Main wrapper function
14 14
export async function tryCatch<T, E = Error>(
15 -
  promise: Promise<T>,
15 +
	promise: Promise<T>,
16 16
): Promise<Result<T, E>> {
17 -
  try {
18 -
    const data = await promise;
19 -
    return { data, error: null };
20 -
  } catch (error) {
21 -
    return { data: null, error: error as E };
22 -
  }
17 +
	try {
18 +
		const data = await promise;
19 +
		return { data, error: null };
20 +
	} catch (error) {
21 +
		return { data: null, error: error as E };
22 +
	}
23 23
}