Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ hono request

# Start server
hono serve

# Generate an optimized Hono app
hono optimize
```

## Commands
Expand All @@ -33,6 +36,7 @@ hono serve
- `search <query>` - Search Hono documentation
- `request [file]` - Send request to Hono app using `app.request()`
- `serve [entry]` - Start server for Hono app
- `optimize [entry]` - Generate an optimized Hono app

### `docs`

Expand Down Expand Up @@ -231,6 +235,35 @@ hono serve \
--use "serveStatic({ root: './' })"
```

### `optimize`

Generate an optimized Hono class and export bundled file.

```bash
hono optimize [entry] [options]
```

**Arguments:**

- `entry` - Entry file for your Hono app (TypeScript/JSX supported, optional)

**Options:**

- `-o, --outfile <outfile>` - Output file

**Examples:**

```bash
# Generate an optimized Hono class and export bundled file to dist/index.js
hono optimize

# Specify entry file and output file
hono optimize -o dist/app.js src/app.ts

# Export bundled file with minification
hono optimize -m
```

## Tips

### Using Hono CLI with AI Code Agents
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,4 @@
"vitest": "^3.2.4"
},
"packageManager": "bun@1.2.20"
}
}
2 changes: 2 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { readFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { docsCommand } from './commands/docs/index.js'
import { optimizeCommand } from './commands/optimize/index.js'
import { requestCommand } from './commands/request/index.js'
import { searchCommand } from './commands/search/index.js'
import { serveCommand } from './commands/serve/index.js'
Expand All @@ -22,6 +23,7 @@ program

// Register commands
docsCommand(program)
optimizeCommand(program)
searchCommand(program)
requestCommand(program)
serveCommand(program)
Expand Down
230 changes: 230 additions & 0 deletions src/commands/optimize/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import { Command } from 'commander'
import { describe, it, expect, beforeEach } from 'vitest'
import { execFile } from 'node:child_process'
import { mkdirSync, mkdtempSync, writeFileSync, readFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { optimizeCommand } from './index'

const program = new Command()
optimizeCommand(program)

const npmInstall = async () =>
new Promise<void>((resolve) => {
const child = execFile('npm', ['install'])
child.on('exit', () => {
resolve()
})
})

describe('optimizeCommand', () => {
let dir: string
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), 'hono-cli-optimize-test'))
mkdirSync(join(dir, 'src'), { recursive: true })
process.chdir(dir)
})

it('should throws an error if entry file not found', async () => {
await expect(
program.parseAsync(['node', 'hono', 'optimize', './non-existent-file.ts'])
).rejects.toThrowError()
})

it.each([
{
name: 'src/index.ts',
files: [
{
path: './src/index.ts',
content: `
import { Hono } from 'hono'
const app = new Hono<{ Bindings: { FOO: string } }>()
app.get('/', (c) => c.text('Hello, World!'))
export default app
`,
},
],
result: {
path: './dist/index.js',
content: `this.router = new PreparedRegExpRouter(...routerParams)`,
},
},
{
name: 'src/index.tsx',
files: [
{
path: './src/index.tsx',
content: `
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.html(<div>Hello, World!</div>))
export default app
`,
},
],
result: {
path: './dist/index.js',
content: `this.router = new PreparedRegExpRouter(...routerParams)`,
},
},
{
name: 'src/index.js',
files: [
{
path: './src/index.js',
content: `
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hello, World!'))
export default app
`,
},
],
result: {
path: './dist/index.js',
content: `this.router = new PreparedRegExpRouter(...routerParams)`,
},
},
{
name: 'src/index.jsx',
files: [
{
path: './src/index.jsx',
content: `
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.html(<div>Hello, World!</div>))
export default app
`,
},
],
result: {
path: './dist/index.js',
content: `this.router = new PreparedRegExpRouter(...routerParams)`,
},
},
{
name: 'specify entry file',
args: ['./src/app.ts'],
files: [
{
path: './src/app.ts',
content: `
import { Hono } from 'hono'
const app = new Hono<{ Bindings: { FOO: string } }>()
app.get('/', (c) => c.text('Hello, World!'))
export default app
`,
},
],
result: {
path: './dist/index.js',
content: `this.router = new PreparedRegExpRouter(...routerParams)`,
},
},
{
name: 'specify outfile option',
args: ['-o', './dist/app.js'],
files: [
{
path: './src/index.ts',
content: `
import { Hono } from 'hono'
const app = new Hono<{ Bindings: { FOO: string } }>()
app.get('/', (c) => c.text('Hello, World!'))
export default app
`,
},
],
result: {
path: './dist/app.js',
content: `this.router = new PreparedRegExpRouter(...routerParams)`,
},
},
{
name: 'fallback to TrieRouter',
files: [
{
path: './src/index.ts',
content: `
import { Hono } from 'hono'
const app = new Hono<{ Bindings: { FOO: string } }>()
app.get('/foo/:capture{ba(r|z)}', (c) => c.text('Hello, World!'))
export default app
`,
},
],
result: {
path: './dist/index.js',
content: `this.router = new TrieRouter()`,
},
},
{
name: 'hono@4.9.10 does not have PreparedRegExpRouter',
honoVersion: '4.9.10',
files: [
{
path: './src/index.ts',
content: `
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hello, World!'))
export default app
`,
},
],
result: {
path: './dist/index.js',
content: `this.router = new RegExpRouter()`,
},
},
{
name: 'specify minify option',
args: ['-m'],
files: [
{
path: './src/index.ts',
content: `
import { Hono } from 'hono'
const app = new Hono<{ Bindings: { FOO: string } }>()
app.get('/', (c) => c.text('Hello, World!'))
export default app
`,
},
],
result: {
path: './dist/index.js',
lineCount: 2,
},
},
])(
'should success to optimize: $name',
{ timeout: 0 },
async ({ honoVersion, files, result, args }) => {
writeFileSync(
join(dir, 'package.json'),
JSON.stringify({
name: 'hono-cli-optimize-test',
type: 'module',
dependencies: {
hono: honoVersion ?? '4.9.11',
},
})
)
await npmInstall()
for (const file of files) {
writeFileSync(join(dir, file.path), file.content)
}
await program.parseAsync(['node', 'hono', 'optimize', ...(args ?? [])])

const content = readFileSync(join(dir, result.path), 'utf-8')
if (result.lineCount) {
expect(content.split('\n').length).toBe(result.lineCount)
}
if (result.content) {
expect(content).toMatch(result.content)
}
}
)
})
Loading