feat: Added Hono RPC support 35364462
Steve · 2025-05-03 19:11 1 file(s) · +139 −0
index.js +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 +
}