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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ hono build [entry] [options]
With the `--optimize` option, it also applies Hono-specific optimizations to reduce bundle size:

- **Router optimization**: Replaces the router with a prebuilt router for your routes
- **Request body API removal**: Removes request body APIs (`c.req.json()`, `c.req.formData()`, etc.) when your application only uses GET, HEAD, or OPTIONS methods
- **Request body API removal**: Removes request body APIs (`c.req.json()`, `c.req.formData()`, etc.) when every route method is strictly GET, HEAD, or OPTIONS. A route or middleware registered with `all()` or `use()` keeps the APIs, because it may read the request body
- **Context response API removal**: Removes unused response utility APIs (`c.body()`, `c.json()`, `c.text()`, `c.html()`, `c.redirect()`) from Context object
- **Hono API removal**: Removes unused Hono methods (`route`, `mount`, `fire`) that are only used during application initialization

Expand All @@ -121,7 +121,7 @@ With the `--optimize` option, it also applies Hono-specific optimizations to red
- `-m, --minify` - minify output file
- `-t, --target [target]` - environment target
- `--optimize` - apply Hono-specific optimizations
- `--no-request-body-api-removal` - Disable request body API removal optimization
- `--request-body-api-removal <mode>` - Request body API removal mode: `auto` (default), `force`, or `disable`
- `--no-context-response-api-removal` - Disable response utility API removal from Context object
- `--no-hono-api-removal` - Disable Hono API removal optimization
- `--plain` - human-readable output instead of JSON
Expand Down
103 changes: 101 additions & 2 deletions src/commands/build/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,8 +339,55 @@ describe('buildCommand', () => {
expect(content).toMatch(/parseBody/)
})

it('should keep request body APIs when all method is used', { timeout: 0 }, async () => {
writePackageJSON(dir)
await npmInstall()
writeFileSync(
join(dir, './src/index.ts'),
`
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hello'))
app.all('/data', async (c) => {
const body = await c.req.json()
return c.json(body)
})
export default app
`
)
await program.parseAsync(['node', 'hono', 'build', '--optimize'])

const content = readFileSync(join(dir, './dist/index.js'), 'utf-8')
expect(content).toMatch(/parseBody/)
})

it('should keep request body APIs when middleware is used', { timeout: 0 }, async () => {
writePackageJSON(dir)
await npmInstall()
writeFileSync(
join(dir, './src/index.ts'),
`
import { Hono } from 'hono'
const app = new Hono()
app.use(async (c, next) => {
if (c.req.method === 'POST') {
const body = await c.req.json()
c.set('body', body)
}
await next()
})
app.get('/', (c) => c.text('Hello'))
export default app
`
)
await program.parseAsync(['node', 'hono', 'build', '--optimize'])

const content = readFileSync(join(dir, './dist/index.js'), 'utf-8')
expect(content).toMatch(/parseBody/)
})

it(
'should keep request body APIs when --no-request-body-api-removal is specified',
'should keep request body APIs when --request-body-api-removal disable is specified',
{ timeout: 0 },
async () => {
writePackageJSON(dir)
Expand All @@ -359,13 +406,65 @@ describe('buildCommand', () => {
'hono',
'build',
'--optimize',
'--no-request-body-api-removal',
'--request-body-api-removal',
'disable',
])

const content = readFileSync(join(dir, './dist/index.js'), 'utf-8')
expect(content).toMatch(/parseBody/)
}
)

it(
'should remove request body APIs when --request-body-api-removal force is specified',
{ timeout: 0 },
async () => {
writePackageJSON(dir)
await npmInstall()
writeFileSync(
join(dir, './src/index.ts'),
`
import { Hono } from 'hono'
const app = new Hono()
app.post('/data', async (c) => {
const body = await c.req.json()
return c.json(body)
})
export default app
`
)
await program.parseAsync([
'node',
'hono',
'build',
'--optimize',
'--request-body-api-removal',
'force',
])

const content = readFileSync(join(dir, './dist/index.js'), 'utf-8')
expect(content).not.toMatch(/parseBody/)
expect(content).not.toMatch(/#cachedBody/)
}
)

it('should print a JSON error with an invalid mode', async () => {
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
await program.parseAsync([
'node',
'hono',
'build',
'--optimize',
'--request-body-api-removal',
'hoge',
])
const parsed = JSON.parse(log.mock.calls[0][0] as string)
expect(parsed.ok).toBe(false)
expect(parsed.error.code).toBe('INVALID_OPTION')
expect(process.exitCode).toBe(1)
process.exitCode = undefined
log.mockRestore()
})
})

describe('Hono API removal', () => {
Expand Down
25 changes: 17 additions & 8 deletions src/commands/build/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { Command } from 'commander'
import * as esbuild from 'esbuild'
import type { Hono } from 'hono'
import { METHOD_NAME_ALL } from 'hono/router'
import { buildInitParams, serializeInitParams } from 'hono/router/reg-exp-router'
import { execFile } from 'node:child_process'
import { existsSync, realpathSync, statSync, readFileSync } from 'node:fs'
Expand Down Expand Up @@ -30,7 +29,7 @@ interface BuildOptions {
minify?: boolean
target: string
optimize?: boolean
requestBodyApiRemoval: boolean
requestBodyApiRemoval: 'auto' | 'force' | 'disable'
honoApiRemoval: boolean
contextResponseApiRemoval: boolean
plain?: boolean
Expand All @@ -57,8 +56,9 @@ export function buildCommand(program: Command) {
.option('-m, --minify', 'minify output file')
.option('--optimize', 'apply Hono-specific optimizations')
.option(
'--no-request-body-api-removal',
'do not remove request body APIs even if they are not needed'
'--request-body-api-removal <mode>',
'request body API removal mode (auto | force | disable)',
'auto'
)
.option('--no-hono-api-removal', 'do not remove Hono APIs even if they are not used')
.option(
Expand All @@ -69,6 +69,13 @@ export function buildCommand(program: Command) {
.option('--plain', 'human-readable output instead of JSON')
.action(
handleErrors(async (entry: string, options: BuildOptions) => {
if (!['auto', 'force', 'disable'].includes(options.requestBodyApiRemoval)) {
throw new CliError(
'INVALID_OPTION',
`Invalid mode for --request-body-api-removal: ${options.requestBodyApiRemoval}`,
'Use one of: auto, force, disable'
)
}
if (!entry) {
entry =
DEFAULT_ENTRY_CANDIDATES.find((entry) => existsSync(entry)) ??
Expand Down Expand Up @@ -269,11 +276,13 @@ export class Hono extends HonoBase {
assignRouterStatement = 'this.router = new TrieRouter()'
}

// "auto" removes the APIs only when every route method is strictly
// GET/HEAD/OPTIONS. A route or middleware registered with ALL may read
// the request body (#64), so its presence keeps the APIs.
const removeRequestBodyApi =
options.requestBodyApiRemoval !== false &&
app.routes.every(({ method }) =>
[METHOD_NAME_ALL, 'GET', 'HEAD', 'OPTIONS'].includes(method.toUpperCase())
)
options.requestBodyApiRemoval === 'force' ||
(options.requestBodyApiRemoval === 'auto' &&
app.routes.every(({ method }) => ['GET', 'HEAD', 'OPTIONS'].includes(method.toUpperCase())))
const unusedHonoMethods: Record<string, number> = (
app as Hono & { unusedMethods: Record<string, number> }
).unusedMethods
Expand Down
Loading