feat: Added Hono RPC support
35364462
1 file(s) · +139 −0
| 50 | 50 | .option('--repo <repo>', 'specify a custom GitHub repository as source', DEFAULT_REPO) |
|
| 51 | 51 | .option('--template <template>', 'specify a template (default, tailwind, shadcn)', 'default') |
|
| 52 | 52 | .option('--branch <branch>', 'specify a branch to use from the repository') |
|
| 53 | + | .option('--rpc', 'use Hono RPC client for type-safe API communication') |
|
| 53 | 54 | .action(async (projectDirectory, options) => { |
|
| 54 | 55 | try { |
|
| 55 | 56 | displayBanner(); |
|
| 186 | 187 | console.log(chalk.blue('Removed .git directory')); |
|
| 187 | 188 | } |
|
| 188 | 189 | ||
| 190 | + | if (options.rpc) { |
|
| 191 | + | await patchFilesForRPC(projectPath); |
|
| 192 | + | } |
|
| 193 | + | ||
| 189 | 194 | // Initialize git repository? |
|
| 190 | 195 | let gitInitialized = false; |
|
| 191 | 196 | ||
| 274 | 279 | } |
|
| 275 | 280 | } |
|
| 276 | 281 | ||
| 282 | + | if (!options.yes && !options.rpc) { |
|
| 283 | + | const { useRpc } = await prompts({ |
|
| 284 | + | type: 'confirm', |
|
| 285 | + | name: 'useRpc', |
|
| 286 | + | message: 'Use Hono RPC client for type-safe API communication?', |
|
| 287 | + | initial: false |
|
| 288 | + | }); |
|
| 289 | + | ||
| 290 | + | if (useRpc) { |
|
| 291 | + | await patchFilesForRPC(projectPath); |
|
| 292 | + | } |
|
| 293 | + | } |
|
| 294 | + | ||
| 277 | 295 | return { |
|
| 278 | 296 | projectName, |
|
| 279 | 297 | gitInitialized, |
|
| 285 | 303 | throw err; |
|
| 286 | 304 | } |
|
| 287 | 305 | } |
|
| 306 | + | ||
| 307 | + | async function patchFilesForRPC(projectPath) { |
|
| 308 | + | const spinner = ora('Setting up RPC client...').start(); |
|
| 309 | + | ||
| 310 | + | try { |
|
| 311 | + | // 1. Update client package.json to ensure hono client is installed |
|
| 312 | + | const clientPkgPath = path.join(projectPath, 'client', 'package.json'); |
|
| 313 | + | const clientPkg = await fs.readJson(clientPkgPath); |
|
| 314 | + | ||
| 315 | + | // Make sure hono client is in dependencies |
|
| 316 | + | if (!clientPkg.dependencies.hono) { |
|
| 317 | + | clientPkg.dependencies.hono = "^4.7.7"; |
|
| 318 | + | } |
|
| 319 | + | ||
| 320 | + | await fs.writeJson(clientPkgPath, clientPkg, { spaces: 2 }); |
|
| 321 | + | ||
| 322 | + | // 2. Server modification - targeted approach based on known structure |
|
| 323 | + | const serverIndexPath = path.join(projectPath, 'server', 'src', 'index.ts'); |
|
| 324 | + | let serverContent = await fs.readFile(serverIndexPath, 'utf8'); |
|
| 325 | + | ||
| 326 | + | // If the server doesn't already have the RPC structure, update it |
|
| 327 | + | if (!serverContent.includes('export type AppType')) { |
|
| 328 | + | // Create the target server content based on the template |
|
| 329 | + | const updatedServerContent = `import { Hono } from 'hono' |
|
| 330 | + | import { cors } from 'hono/cors' |
|
| 331 | + | import type { ApiResponse } from 'shared/dist' |
|
| 332 | + | ||
| 333 | + | const app = new Hono() |
|
| 334 | + | ||
| 335 | + | app.use(cors()) |
|
| 336 | + | ||
| 337 | + | const routes = app.get('/', (c) => { |
|
| 338 | + | return c.text('Hello Hono!') |
|
| 339 | + | }) |
|
| 340 | + | ||
| 341 | + | .get('/hello', async (c) => { |
|
| 342 | + | ||
| 343 | + | const data: ApiResponse = { |
|
| 344 | + | message: "Hello BHVR!", |
|
| 345 | + | success: true |
|
| 346 | + | } |
|
| 347 | + | ||
| 348 | + | return c.json(data, { status: 200 }) |
|
| 349 | + | }) |
|
| 350 | + | ||
| 351 | + | export type AppType = typeof routes |
|
| 352 | + | export default app`; |
|
| 353 | + | ||
| 354 | + | await fs.writeFile(serverIndexPath, updatedServerContent, 'utf8'); |
|
| 355 | + | } |
|
| 356 | + | ||
| 357 | + | // 3. Update App.tsx with RPC implementation |
|
| 358 | + | const appTsxPath = path.join(projectPath, 'client', 'src', 'App.tsx'); |
|
| 359 | + | let appTsxContent = await fs.readFile(appTsxPath, 'utf8'); |
|
| 360 | + | ||
| 361 | + | // Only make changes if RPC isn't already set up |
|
| 362 | + | if (!appTsxContent.includes('import { hc } from \'hono/client\'')) { |
|
| 363 | + | // Find the key parts of the file we need to preserve |
|
| 364 | + | const importReactMatch = appTsxContent.match(/import\s+{\s*useState\s*}.*?from\s+['"]react['"]/); |
|
| 365 | + | const importBeaverMatch = appTsxContent.match(/import\s+beaver\s+from\s+['"]\.\/assets\/beaver\.svg['"]/); |
|
| 366 | + | const importSharedMatch = appTsxContent.match(/import.*?from\s+['"]shared['"]/); |
|
| 367 | + | const importCssMatch = appTsxContent.match(/import\s+['"]\.\/App\.css['"]/); |
|
| 368 | + | ||
| 369 | + | // Make sure we found the required parts |
|
| 370 | + | if (importReactMatch && importBeaverMatch && importCssMatch) { |
|
| 371 | + | // Get the current return JSX part |
|
| 372 | + | const returnJsxMatch = appTsxContent.match(/return\s*\(\s*<>([\s\S]*?)<\/>/); |
|
| 373 | + | ||
| 374 | + | if (returnJsxMatch) { |
|
| 375 | + | // Create the updated App.tsx content |
|
| 376 | + | const updatedAppContent = `import { useState } from 'react' |
|
| 377 | + | import beaver from './assets/beaver.svg' |
|
| 378 | + | import type { AppType } from '../../server/src' |
|
| 379 | + | import { hc } from 'hono/client' |
|
| 380 | + | ${importSharedMatch ? importSharedMatch[0] : 'import { ApiResponse } from \'shared\''} |
|
| 381 | + | import './App.css' |
|
| 382 | + | ||
| 383 | + | const SERVER_URL = import.meta.env.VITE_SERVER_URL || "http://localhost:3000" |
|
| 384 | + | ||
| 385 | + | const client = hc<AppType>(SERVER_URL); |
|
| 386 | + | ||
| 387 | + | type ResponseType = Awaited<ReturnType<typeof client.hello.$get>>; |
|
| 388 | + | ||
| 389 | + | function App() { |
|
| 390 | + | const [data, setData] = useState<Awaited<ReturnType<ResponseType["json"]>> | undefined>() |
|
| 391 | + | ||
| 392 | + | async function sendRequest() { |
|
| 393 | + | try { |
|
| 394 | + | const res = await client.hello.$get() |
|
| 395 | + | ||
| 396 | + | if(!res.ok){ |
|
| 397 | + | console.log("Error fetching data") |
|
| 398 | + | return |
|
| 399 | + | } |
|
| 400 | + | ||
| 401 | + | const data = await res.json() |
|
| 402 | + | setData(data) |
|
| 403 | + | } catch (error) { |
|
| 404 | + | console.log(error) |
|
| 405 | + | } |
|
| 406 | + | } |
|
| 407 | + | ||
| 408 | + | ${returnJsxMatch[0]} |
|
| 409 | + | ||
| 410 | + | ) |
|
| 411 | + | } |
|
| 412 | + | export default App`; |
|
| 413 | + | ||
| 414 | + | await fs.writeFile(appTsxPath, updatedAppContent, 'utf8'); |
|
| 415 | + | } |
|
| 416 | + | } |
|
| 417 | + | } |
|
| 418 | + | ||
| 419 | + | spinner.succeed('RPC client setup completed'); |
|
| 420 | + | return true; |
|
| 421 | + | } catch (err) { |
|
| 422 | + | spinner.fail('Failed to set up RPC client'); |
|
| 423 | + | console.error(chalk.red('Error:'), err.message); |
|
| 424 | + | return false; |
|
| 425 | + | } |
|
| 426 | + | } |
|