Futher codebase breakdown, and CONTRIBUTING.md
b394b26a
8 file(s) · +357 −284
| 1 | + | # Contributing to create-bhvr |
|
| 2 | + | ||
| 3 | + | First off, thank you for considering contributing to `create-bhvr`. |
|
| 4 | + | ||
| 5 | + | ## Code of Conduct |
|
| 6 | + | ||
| 7 | + | This project and everyone participating in it is governed by the [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. |
|
| 8 | + | ||
| 9 | + | ## How Can I Contribute? |
|
| 10 | + | ||
| 11 | + | ### Reporting Bugs |
|
| 12 | + | ||
| 13 | + | If you find a bug, please make sure the bug has not already been reported by searching on GitHub under [Issues](https://github.com/stevedylandev/bhvr/issues). If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/stevedylandev/bhvr/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring. |
|
| 14 | + | ||
| 15 | + | ### Suggesting Enhancements |
|
| 16 | + | ||
| 17 | + | If you have an idea for an enhancement, please make sure the enhancement has not already been suggested by searching on GitHub under [Issues](https://github.com/stevedylandev/bhvr/issues). If you're unable to find an open issue addressing the suggestion, [open a new one](https://github.com/stevedylandev/bhvr/issues/new). Be sure to include a **title and clear description** of the enhancement you're suggesting. |
|
| 18 | + | ||
| 19 | + | ### Submitting a Pull Request |
|
| 20 | + | ||
| 21 | + | 1. Fork the repository and create your branch from `main`. |
|
| 22 | + | 2. Run `bun install` to install the dependencies. |
|
| 23 | + | 3. Make your changes. |
|
| 24 | + | 4. Run `bun run build` to make sure your changes build correctly. |
|
| 25 | + | 5. Issue that pull request! |
|
| 26 | + | ||
| 27 | + | ## License |
|
| 28 | + | ||
| 29 | + | By contributing, you agree that your contributions will be licensed under its MIT License. |
| 1 | - | import path from "node:path"; |
|
| 2 | - | import { consola } from "consola"; |
|
| 3 | - | import degit from "degit"; |
|
| 4 | - | import { execa } from "execa"; |
|
| 5 | - | import fs from "fs-extra"; |
|
| 6 | - | import ora from "ora"; |
|
| 7 | - | import pc from "picocolors"; |
|
| 8 | 1 | import type { ProjectOptions, ProjectResult } from "@/types"; |
|
| 9 | - | import { DEFAULT_REPO } from "@/utils/constants"; |
|
| 10 | - | import { TEMPLATES } from "@/utils/templates"; |
|
| 11 | - | import { patchFilesForRPC } from "./patch-files-rpc"; |
|
| 12 | - | import { setupBiome } from "./setup-biome"; |
|
| 13 | - | import { tryCatch } from "@/utils/try-catch"; |
|
| 2 | + | import { initializeGit } from "./initialize-git"; |
|
| 3 | + | import { installDependencies } from "./install-dependencies"; |
|
| 4 | + | import { promptForOptions } from "./prompt-for-options"; |
|
| 5 | + | import { scaffoldTemplate } from "./scaffold-template"; |
|
| 14 | 6 | ||
| 15 | 7 | export async function createProject( |
|
| 16 | 8 | projectDirectory: string, |
|
| 17 | 9 | options: ProjectOptions, |
|
| 18 | 10 | ): Promise<ProjectResult | null> { |
|
| 19 | - | let projectName = projectDirectory; |
|
| 20 | - | ||
| 21 | - | if (!projectName && !options.yes) { |
|
| 22 | - | const { data, error } = await tryCatch( |
|
| 23 | - | consola.prompt(pc.yellow("What is the name of your project?"), { |
|
| 24 | - | type: "text", |
|
| 25 | - | default: "my-bhvr-app", |
|
| 26 | - | placeholder: "my-bhvr-app", |
|
| 27 | - | cancel: "reject", |
|
| 28 | - | }), |
|
| 29 | - | ); |
|
| 11 | + | const projectOptions = await promptForOptions({ |
|
| 12 | + | ...options, |
|
| 13 | + | projectName: projectDirectory, |
|
| 14 | + | }); |
|
| 30 | 15 | ||
| 31 | - | if (!data || error) { |
|
| 32 | - | consola.error(pc.red("Project creation cancelled.")); |
|
| 33 | - | return null; |
|
| 34 | - | } |
|
| 35 | - | ||
| 36 | - | projectName = data; |
|
| 16 | + | if (!projectOptions) { |
|
| 17 | + | return null; |
|
| 37 | 18 | } |
|
| 38 | 19 | ||
| 39 | - | let templateChoice = options.template || "default"; |
|
| 20 | + | const scaffolded = await scaffoldTemplate(projectOptions); |
|
| 40 | 21 | ||
| 41 | - | if (!options.yes && !options.branch) { |
|
| 42 | - | const templateChoices = Object.keys(TEMPLATES).map((key) => ({ |
|
| 43 | - | label: `${key} (${TEMPLATES[key]?.description})`, |
|
| 44 | - | value: key, |
|
| 45 | - | })); |
|
| 46 | - | ||
| 47 | - | const { data, error } = await tryCatch( |
|
| 48 | - | consola.prompt(pc.yellow("Select a template:"), { |
|
| 49 | - | type: "select", |
|
| 50 | - | options: templateChoices, |
|
| 51 | - | initial: "default", |
|
| 52 | - | cancel: "reject", |
|
| 53 | - | }), |
|
| 54 | - | ); |
|
| 55 | - | ||
| 56 | - | if (!data || error) { |
|
| 57 | - | consola.error("Project creation cancelled."); |
|
| 58 | - | return null; |
|
| 59 | - | } |
|
| 60 | - | ||
| 61 | - | templateChoice = data; |
|
| 22 | + | if (!scaffolded) { |
|
| 23 | + | return null; |
|
| 62 | 24 | } |
|
| 63 | 25 | ||
| 64 | - | const projectPath = path.resolve(process.cwd(), projectName); |
|
| 65 | - | ||
| 66 | - | if (fs.existsSync(projectPath)) { |
|
| 67 | - | const files = fs.readdirSync(projectPath); |
|
| 68 | - | ||
| 69 | - | if (files.length > 0 && !options.yes) { |
|
| 70 | - | const { data: overwrite, error } = await tryCatch( |
|
| 71 | - | consola.prompt( |
|
| 72 | - | `The directory ${projectName} already exists and is not empty. Do you want to overwrite it?`, |
|
| 73 | - | { |
|
| 74 | - | type: "confirm", |
|
| 75 | - | initial: false, |
|
| 76 | - | }, |
|
| 77 | - | ), |
|
| 78 | - | ); |
|
| 79 | - | ||
| 80 | - | if (!overwrite || error) { |
|
| 81 | - | consola.error("Project creation cancelled."); |
|
| 82 | - | return null; |
|
| 83 | - | } |
|
| 84 | - | ||
| 85 | - | await fs.emptyDir(projectPath); |
|
| 86 | - | } |
|
| 87 | - | } |
|
| 88 | - | ||
| 89 | - | fs.ensureDirSync(projectPath); |
|
| 90 | - | ||
| 91 | - | const repoPath = options.repo || DEFAULT_REPO; |
|
| 92 | - | const templateConfig = |
|
| 93 | - | TEMPLATES[templateChoice as keyof typeof TEMPLATES] || TEMPLATES.default; |
|
| 94 | - | const branch = options.branch || (templateConfig?.branch ?? "main"); |
|
| 95 | - | const repoUrl = `${repoPath}#${branch}`; |
|
| 96 | - | const spinner = ora("Downloading template...").start(); |
|
| 97 | - | ||
| 98 | - | try { |
|
| 99 | - | const emitter = degit(repoUrl, { |
|
| 100 | - | cache: false, |
|
| 101 | - | force: true, |
|
| 102 | - | verbose: false, |
|
| 103 | - | }); |
|
| 104 | - | ||
| 105 | - | await emitter.clone(projectPath); |
|
| 106 | - | spinner.succeed( |
|
| 107 | - | `Template downloaded successfully (${templateChoice} template)`, |
|
| 108 | - | ); |
|
| 109 | - | ||
| 110 | - | const pkgJsonPath = path.join(projectPath, "package.json"); |
|
| 111 | - | if (fs.existsSync(pkgJsonPath)) { |
|
| 112 | - | const pkgJson = await fs.readJson(pkgJsonPath); |
|
| 113 | - | pkgJson.name = projectName; |
|
| 114 | - | await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 }); |
|
| 115 | - | } |
|
| 116 | - | ||
| 117 | - | const gitDir = path.join(projectPath, ".git"); |
|
| 118 | - | if (fs.existsSync(gitDir)) { |
|
| 119 | - | await fs.remove(gitDir); |
|
| 120 | - | console.log(pc.blue("Removed .git directory")); |
|
| 121 | - | } |
|
| 122 | - | ||
| 123 | - | let useRpc = options.rpc; |
|
| 124 | - | ||
| 125 | - | if (!options.yes && !options.rpc) { |
|
| 126 | - | const { data: rpcResponse, error } = await tryCatch( |
|
| 127 | - | consola.prompt("Use Hono RPC client for type-safe API communication?", { |
|
| 128 | - | type: "confirm", |
|
| 129 | - | initial: false, |
|
| 130 | - | }), |
|
| 131 | - | ); |
|
| 132 | - | ||
| 133 | - | if (!rpcResponse || error) { |
|
| 134 | - | consola.error("Project creation cancelled."); |
|
| 135 | - | return null; |
|
| 136 | - | } |
|
| 137 | - | ||
| 138 | - | useRpc = rpcResponse; |
|
| 139 | - | } |
|
| 140 | - | ||
| 141 | - | if (useRpc) { |
|
| 142 | - | await patchFilesForRPC(projectPath, templateChoice); |
|
| 143 | - | } |
|
| 144 | - | ||
| 145 | - | let linter = options.linter; |
|
| 146 | - | ||
| 147 | - | if (!options.yes && !options.linter) { |
|
| 148 | - | const { data: linterResponse, error } = await tryCatch( |
|
| 149 | - | consola.prompt("Select a linter:", { |
|
| 150 | - | type: "select", |
|
| 151 | - | options: [ |
|
| 152 | - | { label: "ESLint (default)", value: "eslint" }, |
|
| 153 | - | { label: "Biome", value: "biome" }, |
|
| 154 | - | ], |
|
| 155 | - | initial: "eslint", |
|
| 156 | - | cancel: "reject", |
|
| 157 | - | }), |
|
| 158 | - | ); |
|
| 159 | - | ||
| 160 | - | if (!linterResponse || error) { |
|
| 161 | - | console.log(pc.yellow("Project creation cancelled.")); |
|
| 162 | - | return null; |
|
| 163 | - | } |
|
| 164 | - | ||
| 165 | - | linter = linterResponse as "eslint" | "biome"; |
|
| 166 | - | } |
|
| 167 | - | ||
| 168 | - | if (linter === "biome") { |
|
| 169 | - | await setupBiome(projectPath); |
|
| 170 | - | } |
|
| 26 | + | const gitInitialized = await initializeGit( |
|
| 27 | + | projectOptions.projectName, |
|
| 28 | + | projectOptions.yes, |
|
| 29 | + | ); |
|
| 30 | + | const dependenciesInstalled = await installDependencies( |
|
| 31 | + | projectOptions.projectName, |
|
| 32 | + | projectOptions.yes, |
|
| 33 | + | ); |
|
| 171 | 34 | ||
| 172 | - | let gitInitialized = false; |
|
| 173 | - | ||
| 174 | - | if (!options.yes) { |
|
| 175 | - | const { data: gitResponse, error } = await tryCatch( |
|
| 176 | - | consola.prompt("Initialize a git repository?", { |
|
| 177 | - | type: "confirm", |
|
| 178 | - | ||
| 179 | - | initial: true, |
|
| 180 | - | cancel: "reject", |
|
| 181 | - | }), |
|
| 182 | - | ); |
|
| 183 | - | ||
| 184 | - | if (error) { |
|
| 185 | - | console.log(pc.yellow("Project creation cancelled.")); |
|
| 186 | - | return null; |
|
| 187 | - | } |
|
| 188 | - | ||
| 189 | - | if (gitResponse) { |
|
| 190 | - | try { |
|
| 191 | - | spinner.start("Initializing git repository..."); |
|
| 192 | - | await execa("git", ["init"], { cwd: projectPath }); |
|
| 193 | - | spinner.succeed("Git repository initialized"); |
|
| 194 | - | gitInitialized = true; |
|
| 195 | - | } catch (err: unknown) { |
|
| 196 | - | spinner.fail( |
|
| 197 | - | "Failed to initialize git repository. Is git installed?", |
|
| 198 | - | ); |
|
| 199 | - | if (err instanceof Error) { |
|
| 200 | - | consola.error(pc.red("Git error:"), err.message); |
|
| 201 | - | } else { |
|
| 202 | - | consola.error(pc.red("Git error: Unknown error")); |
|
| 203 | - | } |
|
| 204 | - | } |
|
| 205 | - | } |
|
| 206 | - | } else { |
|
| 207 | - | try { |
|
| 208 | - | spinner.start("Initializing git repository..."); |
|
| 209 | - | await execa("git", ["init"], { cwd: projectPath }); |
|
| 210 | - | spinner.succeed("Git repository initialized"); |
|
| 211 | - | gitInitialized = true; |
|
| 212 | - | } catch (_err) { |
|
| 213 | - | spinner.fail("Failed to initialize git repository. Is git installed?"); |
|
| 214 | - | } |
|
| 215 | - | } |
|
| 216 | - | ||
| 217 | - | let dependenciesInstalled = false; |
|
| 218 | - | ||
| 219 | - | if (!options.yes) { |
|
| 220 | - | const { data: depsResponse, error } = await tryCatch( |
|
| 221 | - | consola.prompt("Install dependencies?", { |
|
| 222 | - | type: "confirm", |
|
| 223 | - | initial: true, |
|
| 224 | - | cancel: "reject", |
|
| 225 | - | }), |
|
| 226 | - | ); |
|
| 227 | - | ||
| 228 | - | if (error) { |
|
| 229 | - | console.log(pc.yellow("Project creation cancelled.")); |
|
| 230 | - | return null; |
|
| 231 | - | } |
|
| 232 | - | ||
| 233 | - | if (depsResponse) { |
|
| 234 | - | spinner.start("Installing dependencies..."); |
|
| 235 | - | try { |
|
| 236 | - | await execa("bun", ["install"], { cwd: projectPath }); |
|
| 237 | - | spinner.succeed("Dependencies installed with bun"); |
|
| 238 | - | dependenciesInstalled = true; |
|
| 239 | - | } catch (_bunErr) { |
|
| 240 | - | try { |
|
| 241 | - | spinner.text = "Installing dependencies with npm..."; |
|
| 242 | - | await execa("npm", ["install"], { cwd: projectPath }); |
|
| 243 | - | spinner.succeed("Dependencies installed with npm"); |
|
| 244 | - | dependenciesInstalled = true; |
|
| 245 | - | } catch (_npmErr) { |
|
| 246 | - | spinner.fail("Failed to install dependencies."); |
|
| 247 | - | console.log( |
|
| 248 | - | pc.yellow( |
|
| 249 | - | "You can install them manually after navigating to the project directory.", |
|
| 250 | - | ), |
|
| 251 | - | ); |
|
| 252 | - | } |
|
| 253 | - | } |
|
| 254 | - | } |
|
| 255 | - | } else { |
|
| 256 | - | spinner.start("Installing dependencies..."); |
|
| 257 | - | try { |
|
| 258 | - | await execa("bun", ["install"], { cwd: projectPath }); |
|
| 259 | - | spinner.succeed("Dependencies installed with bun"); |
|
| 260 | - | dependenciesInstalled = true; |
|
| 261 | - | } catch (_bunErr) { |
|
| 262 | - | try { |
|
| 263 | - | spinner.text = "Installing dependencies with npm..."; |
|
| 264 | - | await execa("npm", ["install"], { cwd: projectPath }); |
|
| 265 | - | spinner.succeed("Dependencies installed with npm"); |
|
| 266 | - | dependenciesInstalled = true; |
|
| 267 | - | } catch (_npmErr) { |
|
| 268 | - | spinner.fail( |
|
| 269 | - | "Failed to install dependencies. You can install them manually later.", |
|
| 270 | - | ); |
|
| 271 | - | } |
|
| 272 | - | } |
|
| 273 | - | } |
|
| 274 | - | ||
| 275 | - | return { |
|
| 276 | - | projectName, |
|
| 277 | - | gitInitialized, |
|
| 278 | - | dependenciesInstalled, |
|
| 279 | - | template: templateChoice, |
|
| 280 | - | }; |
|
| 281 | - | } catch (err) { |
|
| 282 | - | spinner.fail("Failed to download template"); |
|
| 283 | - | throw err; |
|
| 284 | - | } |
|
| 35 | + | return { |
|
| 36 | + | projectName: projectOptions.projectName, |
|
| 37 | + | gitInitialized, |
|
| 38 | + | dependenciesInstalled, |
|
| 39 | + | template: projectOptions.template, |
|
| 40 | + | }; |
|
| 285 | 41 | } |
| 1 | 1 | export * from "./create-project"; |
|
| 2 | 2 | export * from "./display-banner"; |
|
| 3 | + | export * from "./initialize-git"; |
|
| 4 | + | export * from "./install-dependencies"; |
|
| 3 | 5 | export * from "./patch-files-rpc"; |
|
| 6 | + | export * from "./prompt-for-options"; |
|
| 7 | + | export * from "./scaffold-template"; |
|
| 4 | 8 | export * from "./setup-biome"; |
| 1 | + | import { consola } from "consola"; |
|
| 2 | + | import { execa } from "execa"; |
|
| 3 | + | import ora from "ora"; |
|
| 4 | + | import pc from "picocolors"; |
|
| 5 | + | import { tryCatch } from "@/utils/try-catch"; |
|
| 6 | + | ||
| 7 | + | export async function initializeGit( |
|
| 8 | + | projectPath: string, |
|
| 9 | + | skipConfirmation?: boolean, |
|
| 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 | + | ); |
|
| 19 | + | ||
| 20 | + | if (error) { |
|
| 21 | + | console.log(pc.yellow("Project creation cancelled.")); |
|
| 22 | + | return false; |
|
| 23 | + | } |
|
| 24 | + | ||
| 25 | + | if (!gitResponse) { |
|
| 26 | + | return false; |
|
| 27 | + | } |
|
| 28 | + | } |
|
| 29 | + | ||
| 30 | + | const spinner = ora("Initializing git repository...").start(); |
|
| 31 | + | try { |
|
| 32 | + | await execa("git", ["init"], { cwd: projectPath }); |
|
| 33 | + | spinner.succeed("Git repository initialized"); |
|
| 34 | + | return true; |
|
| 35 | + | } catch (err: unknown) { |
|
| 36 | + | spinner.fail("Failed to initialize git repository. Is git installed?"); |
|
| 37 | + | if (err instanceof Error) { |
|
| 38 | + | consola.error(pc.red("Git error:"), err.message); |
|
| 39 | + | } else { |
|
| 40 | + | consola.error(pc.red("Git error: Unknown error")); |
|
| 41 | + | } |
|
| 42 | + | return false; |
|
| 43 | + | } |
|
| 44 | + | } |
| 1 | + | import { consola } from "consola"; |
|
| 2 | + | import { execa } from "execa"; |
|
| 3 | + | import ora from "ora"; |
|
| 4 | + | import pc from "picocolors"; |
|
| 5 | + | import { tryCatch } from "@/utils/try-catch"; |
|
| 6 | + | ||
| 7 | + | async function getPackageManager(): Promise<"bun" | "pnpm" | "npm"> { |
|
| 8 | + | try { |
|
| 9 | + | await execa("bun", ["--version"]); |
|
| 10 | + | return "bun"; |
|
| 11 | + | } catch (e) { |
|
| 12 | + | // bun is not installed |
|
| 13 | + | } |
|
| 14 | + | ||
| 15 | + | try { |
|
| 16 | + | await execa("pnpm", ["--version"]); |
|
| 17 | + | return "pnpm"; |
|
| 18 | + | } catch (e) { |
|
| 19 | + | // pnpm is not installed |
|
| 20 | + | } |
|
| 21 | + | ||
| 22 | + | return "npm"; |
|
| 23 | + | } |
|
| 24 | + | ||
| 25 | + | export async function installDependencies( |
|
| 26 | + | projectPath: string, |
|
| 27 | + | skipConfirmation?: boolean, |
|
| 28 | + | ): Promise<boolean> { |
|
| 29 | + | if (!skipConfirmation) { |
|
| 30 | + | const { data: depsResponse, error } = await tryCatch( |
|
| 31 | + | consola.prompt("Install dependencies?", { |
|
| 32 | + | type: "confirm", |
|
| 33 | + | initial: true, |
|
| 34 | + | cancel: "reject", |
|
| 35 | + | }), |
|
| 36 | + | ); |
|
| 37 | + | ||
| 38 | + | if (error) { |
|
| 39 | + | console.log(pc.yellow("Project creation cancelled.")); |
|
| 40 | + | return false; |
|
| 41 | + | } |
|
| 42 | + | ||
| 43 | + | if (!depsResponse) { |
|
| 44 | + | return false; |
|
| 45 | + | } |
|
| 46 | + | } |
|
| 47 | + | ||
| 48 | + | const packageManager = await getPackageManager(); |
|
| 49 | + | const spinner = ora( |
|
| 50 | + | `Installing dependencies with ${packageManager}...`, |
|
| 51 | + | ).start(); |
|
| 52 | + | ||
| 53 | + | try { |
|
| 54 | + | await execa(packageManager, ["install"], { cwd: projectPath }); |
|
| 55 | + | spinner.succeed(`Dependencies installed with ${packageManager}`); |
|
| 56 | + | return true; |
|
| 57 | + | } catch (err) { |
|
| 58 | + | spinner.fail("Failed to install dependencies."); |
|
| 59 | + | console.log( |
|
| 60 | + | pc.yellow( |
|
| 61 | + | "You can install them manually after navigating to the project directory.", |
|
| 62 | + | ), |
|
| 63 | + | ); |
|
| 64 | + | return false; |
|
| 65 | + | } |
|
| 66 | + | } |
| 1 | + | import { consola } from "consola"; |
|
| 2 | + | import pc from "picocolors"; |
|
| 3 | + | import type { ProjectOptions } from "@/types"; |
|
| 4 | + | import { TEMPLATES } from "@/utils/templates"; |
|
| 5 | + | import { tryCatch } from "@/utils/try-catch"; |
|
| 6 | + | ||
| 7 | + | export async function promptForOptions( |
|
| 8 | + | options: ProjectOptions, |
|
| 9 | + | ): Promise<ProjectOptions | null> { |
|
| 10 | + | let projectName = options.projectName; |
|
| 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 | + | ); |
|
| 21 | + | ||
| 22 | + | if (!data || error) { |
|
| 23 | + | consola.error(pc.red("Project creation cancelled.")); |
|
| 24 | + | return null; |
|
| 25 | + | } |
|
| 26 | + | ||
| 27 | + | projectName = data; |
|
| 28 | + | } |
|
| 29 | + | ||
| 30 | + | let templateChoice = options.template || "default"; |
|
| 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 | + | })); |
|
| 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 | + | ); |
|
| 46 | + | ||
| 47 | + | if (!data || error) { |
|
| 48 | + | consola.error("Project creation cancelled."); |
|
| 49 | + | return null; |
|
| 50 | + | } |
|
| 51 | + | ||
| 52 | + | templateChoice = data; |
|
| 53 | + | } |
|
| 54 | + | ||
| 55 | + | let useRpc = options.rpc; |
|
| 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 | + | ); |
|
| 64 | + | ||
| 65 | + | if (error) { |
|
| 66 | + | consola.error("Project creation cancelled."); |
|
| 67 | + | return null; |
|
| 68 | + | } |
|
| 69 | + | ||
| 70 | + | useRpc = rpcResponse; |
|
| 71 | + | } |
|
| 72 | + | ||
| 73 | + | let linter = options.linter; |
|
| 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 | + | ); |
|
| 87 | + | ||
| 88 | + | if (error) { |
|
| 89 | + | console.log(pc.yellow("Project creation cancelled.")); |
|
| 90 | + | return null; |
|
| 91 | + | } |
|
| 92 | + | ||
| 93 | + | linter = linterResponse as "eslint" | "biome"; |
|
| 94 | + | } |
|
| 95 | + | ||
| 96 | + | return { |
|
| 97 | + | ...options, |
|
| 98 | + | projectName, |
|
| 99 | + | template: templateChoice, |
|
| 100 | + | rpc: useRpc, |
|
| 101 | + | linter, |
|
| 102 | + | }; |
|
| 103 | + | } |
| 1 | + | import path from "node:path"; |
|
| 2 | + | import degit from "degit"; |
|
| 3 | + | import fs from "fs-extra"; |
|
| 4 | + | import ora from "ora"; |
|
| 5 | + | import pc from "picocolors"; |
|
| 6 | + | import type { ProjectOptions } from "@/types"; |
|
| 7 | + | import { DEFAULT_REPO } from "@/utils/constants"; |
|
| 8 | + | import { TEMPLATES } from "@/utils/templates"; |
|
| 9 | + | import { patchFilesForRPC } from "./patch-files-rpc"; |
|
| 10 | + | import { setupBiome } from "./setup-biome"; |
|
| 11 | + | ||
| 12 | + | export async function scaffoldTemplate( |
|
| 13 | + | options: ProjectOptions, |
|
| 14 | + | ): Promise<boolean> { |
|
| 15 | + | const { projectName, template, repo, branch, rpc, linter } = options; |
|
| 16 | + | const projectPath = path.resolve(process.cwd(), projectName); |
|
| 17 | + | ||
| 18 | + | if (fs.existsSync(projectPath)) { |
|
| 19 | + | const files = fs.readdirSync(projectPath); |
|
| 20 | + | if (files.length > 0) { |
|
| 21 | + | await fs.emptyDir(projectPath); |
|
| 22 | + | } |
|
| 23 | + | } |
|
| 24 | + | ||
| 25 | + | fs.ensureDirSync(projectPath); |
|
| 26 | + | ||
| 27 | + | const repoPath = repo || DEFAULT_REPO; |
|
| 28 | + | const templateConfig = |
|
| 29 | + | TEMPLATES[template as keyof typeof TEMPLATES] || TEMPLATES.default; |
|
| 30 | + | const repoBranch = branch || (templateConfig?.branch ?? "main"); |
|
| 31 | + | const repoUrl = `${repoPath}#${repoBranch}`; |
|
| 32 | + | const spinner = ora("Downloading template...").start(); |
|
| 33 | + | ||
| 34 | + | try { |
|
| 35 | + | const emitter = degit(repoUrl, { |
|
| 36 | + | cache: false, |
|
| 37 | + | force: true, |
|
| 38 | + | verbose: false, |
|
| 39 | + | }); |
|
| 40 | + | ||
| 41 | + | await emitter.clone(projectPath); |
|
| 42 | + | spinner.succeed(`Template downloaded successfully (${template} template)`); |
|
| 43 | + | ||
| 44 | + | const pkgJsonPath = path.join(projectPath, "package.json"); |
|
| 45 | + | if (fs.existsSync(pkgJsonPath)) { |
|
| 46 | + | const pkgJson = await fs.readJson(pkgJsonPath); |
|
| 47 | + | pkgJson.name = projectName; |
|
| 48 | + | await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 }); |
|
| 49 | + | } |
|
| 50 | + | ||
| 51 | + | const gitDir = path.join(projectPath, ".git"); |
|
| 52 | + | if (fs.existsSync(gitDir)) { |
|
| 53 | + | await fs.remove(gitDir); |
|
| 54 | + | console.log(pc.blue("Removed .git directory")); |
|
| 55 | + | } |
|
| 56 | + | ||
| 57 | + | if (rpc) { |
|
| 58 | + | await patchFilesForRPC(projectPath, template); |
|
| 59 | + | } |
|
| 60 | + | ||
| 61 | + | if (linter === "biome") { |
|
| 62 | + | await setupBiome(projectPath); |
|
| 63 | + | } |
|
| 64 | + | ||
| 65 | + | return true; |
|
| 66 | + | } catch (err) { |
|
| 67 | + | spinner.fail("Failed to download template"); |
|
| 68 | + | throw err; |
|
| 69 | + | } |
|
| 70 | + | } |
| 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 | - | yes?: boolean; |
|
| 8 | - | typescript?: boolean; |
|
| 9 | - | repo?: string; |
|
| 10 | - | template?: string; |
|
| 11 | - | branch?: string; |
|
| 12 | - | rpc?: boolean; |
|
| 13 | - | 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"; |
|
| 14 | 15 | }; |
|
| 15 | 16 | ||
| 16 | 17 | export interface ProjectResult { |
|
| 17 | - | projectName: string; |
|
| 18 | - | gitInitialized: boolean; |
|
| 19 | - | dependenciesInstalled: boolean; |
|
| 20 | - | template: string; |
|
| 18 | + | projectName: string; |
|
| 19 | + | gitInitialized: boolean; |
|
| 20 | + | dependenciesInstalled: boolean; |
|
| 21 | + | template: string; |
|
| 21 | 22 | } |