Skip to content

Commit df611e7

Browse files
authored
feat: create command (npm#18)
* feat: `create` command * lint & format * fix the test * format
1 parent 7cff397 commit df611e7

4 files changed

Lines changed: 219 additions & 5 deletions

File tree

README.md

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# @hono/cli
1+
# Hono CLI
22

33
CLI for Hono
44

@@ -14,21 +14,77 @@ npm install -g @hono/cli
1414
# Show help
1515
hono --help
1616

17+
# Create a new Hono project
18+
hono create
19+
1720
# Say hello
1821
hono hello
1922

20-
# Say hello to someone
21-
hono hello yusuke
22-
23-
# Open documentation in browser
23+
# Open documentation
2424
hono docs
2525
```
2626

2727
## Commands
2828

29+
- `create [target]` - Create a new Hono project
2930
- `hello [name]` - Say hello (default: Hono)
3031
- `docs` - Open Hono documentation in browser
3132

33+
### `create`
34+
35+
Create a new Hono project using [create-hono](https://github.com/honojs/create-hono).
36+
37+
```bash
38+
hono create [target] [options]
39+
```
40+
41+
**Arguments:**
42+
43+
- `target` - Target directory (optional)
44+
45+
**Options:**
46+
47+
- `-t, --template <template>` - Template to use (aws-lambda, bun, cloudflare-workers, cloudflare-workers+vite, deno, fastly, lambda-edge, netlify, nextjs, nodejs, vercel, cloudflare-pages, x-basic)
48+
- `-i, --install` - Install dependencies
49+
- `-p, --pm <pm>` - Package manager to use (npm, bun, deno, pnpm, yarn)
50+
- `-o, --offline` - Use offline mode
51+
52+
**Examples:**
53+
54+
```bash
55+
# Interactive project creation
56+
hono create
57+
58+
# Create project in specific directory
59+
hono create my-app
60+
61+
# Create with Cloudflare Workers template
62+
hono create my-app --template cloudflare-workers
63+
64+
# Create and install dependencies with Bun
65+
hono create my-app --pm bun --install
66+
```
67+
68+
### `hello`
69+
70+
Say hello command for testing purposes.
71+
72+
```bash
73+
hono hello [name]
74+
```
75+
76+
**Arguments:**
77+
78+
- `name` - Name to greet (default: Hono)
79+
80+
### `docs`
81+
82+
Open Hono documentation in your default browser.
83+
84+
```bash
85+
hono docs
86+
```
87+
3288
## Authors
3389

3490
- Yusuke Wada https://github.com/yusukebe

src/cli.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Command } from 'commander'
22
import { readFileSync } from 'node:fs'
33
import { dirname, join } from 'node:path'
44
import { fileURLToPath } from 'node:url'
5+
import { createCommand } from './commands/create/index.js'
56
import { docsCommand } from './commands/docs/index.js'
67
import { helloCommand } from './commands/hello/index.js'
78

@@ -19,6 +20,7 @@ program
1920
.version(packageJson.version, '-v, --version', 'display version number')
2021

2122
// Register commands
23+
createCommand(program)
2224
helloCommand(program)
2325
docsCommand(program)
2426

src/commands/create/index.test.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { Command } from 'commander'
2+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
3+
4+
vi.mock('node:child_process', () => ({
5+
spawn: vi.fn(),
6+
}))
7+
8+
import { createCommand } from './index.js'
9+
10+
describe('createCommand', () => {
11+
let program: Command
12+
let consoleErrorSpy: ReturnType<typeof vi.spyOn>
13+
14+
beforeEach(() => {
15+
program = new Command()
16+
createCommand(program)
17+
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
18+
vi.clearAllMocks()
19+
})
20+
21+
afterEach(() => {
22+
consoleErrorSpy.mockRestore()
23+
})
24+
25+
it('should spawn npm create hono@latest with no arguments', async () => {
26+
const { spawn } = await import('node:child_process')
27+
const mockChildProcess = {
28+
on: vi.fn((event, callback) => {
29+
if (event === 'exit') {
30+
callback(0)
31+
}
32+
return mockChildProcess
33+
}),
34+
}
35+
vi.mocked(spawn).mockReturnValue(mockChildProcess as never)
36+
37+
await program.parseAsync(['node', 'test', 'create'])
38+
39+
// Currently, supporting only npm
40+
expect(spawn).toHaveBeenCalledWith('npm', ['create', 'hono@latest'], {
41+
stdio: 'inherit',
42+
})
43+
})
44+
45+
it('should spawn npm create hono@latest with target directory', async () => {
46+
const { spawn } = await import('node:child_process')
47+
const mockChildProcess = {
48+
on: vi.fn((event, callback) => {
49+
if (event === 'exit') {
50+
callback(0)
51+
}
52+
return mockChildProcess
53+
}),
54+
}
55+
vi.mocked(spawn).mockReturnValue(mockChildProcess as never)
56+
57+
await program.parseAsync(['node', 'test', 'create', 'my-app'])
58+
59+
expect(spawn).toHaveBeenCalledWith('npm', ['create', 'hono@latest', 'my-app'], {
60+
stdio: 'inherit',
61+
})
62+
})
63+
64+
it('should handle spawn error gracefully', async () => {
65+
const { spawn } = await import('node:child_process')
66+
const mockChildProcess = {
67+
on: vi.fn((event, callback) => {
68+
if (event === 'error') {
69+
callback(new Error('spawn ENOENT'))
70+
}
71+
return mockChildProcess
72+
}),
73+
}
74+
vi.mocked(spawn).mockReturnValue(mockChildProcess as never)
75+
76+
await expect(program.parseAsync(['node', 'test', 'create'])).rejects.toThrow(
77+
'Failed to execute npm: spawn ENOENT'
78+
)
79+
80+
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to execute npm: spawn ENOENT')
81+
})
82+
83+
it('should throw error when npm exits with non-zero code', async () => {
84+
const { spawn } = await import('node:child_process')
85+
const mockChildProcess = {
86+
on: vi.fn((event, callback) => {
87+
if (event === 'exit') {
88+
callback(1)
89+
}
90+
return mockChildProcess
91+
}),
92+
}
93+
vi.mocked(spawn).mockReturnValue(mockChildProcess as never)
94+
95+
await expect(program.parseAsync(['node', 'test', 'create'])).rejects.toThrow(
96+
'npm create hono@latest exited with code 1'
97+
)
98+
})
99+
})

src/commands/create/index.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { Command } from 'commander'
2+
import { spawn } from 'node:child_process'
3+
4+
export function createCommand(program: Command) {
5+
program
6+
.command('create')
7+
.description('Create a new Hono project')
8+
.argument('[target]', 'target directory')
9+
.option(
10+
'-t, --template <template>',
11+
'Template to use (aws-lambda, bun, cloudflare-workers, cloudflare-workers+vite, deno, fastly, lambda-edge, netlify, nextjs, nodejs, vercel, cloudflare-pages, x-basic)'
12+
)
13+
.option('-i, --install', 'Install dependencies')
14+
.option('-p, --pm <pm>', 'Package manager to use (npm, bun, deno, pnpm, yarn)')
15+
.option('-o, --offline', 'Use offline mode')
16+
.action(
17+
(
18+
target: string,
19+
options: { template?: string; install?: boolean; pm?: string; offline?: boolean }
20+
) => {
21+
const args = ['create', 'hono@latest']
22+
23+
if (target) {
24+
args.push(target)
25+
}
26+
27+
// Add known options
28+
if (options.template) {
29+
args.push('--template', options.template)
30+
}
31+
if (options.install) {
32+
args.push('--install')
33+
}
34+
if (options.pm) {
35+
args.push('--pm', options.pm)
36+
}
37+
if (options.offline) {
38+
args.push('--offline')
39+
}
40+
41+
const npm = spawn('npm', args, {
42+
stdio: 'inherit',
43+
})
44+
45+
npm.on('error', (error) => {
46+
console.error(`Failed to execute npm: ${error.message}`)
47+
throw new Error(`Failed to execute npm: ${error.message}`)
48+
})
49+
50+
npm.on('exit', (code) => {
51+
if (code !== 0) {
52+
throw new Error(`npm create hono@latest exited with code ${code}`)
53+
}
54+
})
55+
}
56+
)
57+
}

0 commit comments

Comments
 (0)