Skip to content

Commit 2bf49f2

Browse files
authored
feat(optimize): enable to remove request body APIs if they are not needed (#49)
* feat(optimize): enable to remove request body APIs if they are not needed * feat(optimize): enable to remove response utility APIs from Context object * feat(optimize): enable to remove unused methods that are only used during application initialization * feat(utils): enable to pass plugins to build function * chore: Update bun.lock * feat(optimize): Detect unused context response method automatically * test(optimize): add tests for API removal feature * fix(optimize): fix help message for "--no-context-response-api-removal"
1 parent 114f9c0 commit 2bf49f2

File tree

5 files changed

+575
-34
lines changed

5 files changed

+575
-34
lines changed

bun.lock

Lines changed: 21 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,12 @@
3939
"hono": "^4.9.12"
4040
},
4141
"devDependencies": {
42+
"@babel/parser": "^7.28.5",
43+
"@babel/types": "^7.28.5",
4244
"@hono/eslint-config": "^1.1.1",
4345
"@types/node": "^24.7.0",
4446
"eslint": "^9.37.0",
47+
"magic-string": "^0.30.21",
4548
"np": "^10.2.0",
4649
"pkg-pr-new": "^0.0.62",
4750
"prettier": "^3.6.2",

src/commands/optimize/index.test.ts

Lines changed: 238 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@ import { optimizeCommand } from './index'
99
const program = new Command()
1010
optimizeCommand(program)
1111

12+
const writePackageJSON = (dir: string, honoVersion: string = 'latest') => {
13+
writeFileSync(
14+
join(dir, 'package.json'),
15+
JSON.stringify({
16+
name: 'hono-cli-optimize-test',
17+
type: 'module',
18+
dependencies: { hono: honoVersion },
19+
})
20+
)
21+
}
22+
1223
const npmInstall = async () =>
1324
new Promise<void>((resolve) => {
1425
const child = execFile('npm', ['install'])
@@ -202,16 +213,7 @@ describe('optimizeCommand', () => {
202213
'should success to optimize: $name',
203214
{ timeout: 0 },
204215
async ({ honoVersion, files, result, args }) => {
205-
writeFileSync(
206-
join(dir, 'package.json'),
207-
JSON.stringify({
208-
name: 'hono-cli-optimize-test',
209-
type: 'module',
210-
dependencies: {
211-
hono: honoVersion ?? '4.9.11',
212-
},
213-
})
214-
)
216+
writePackageJSON(dir, honoVersion)
215217
await npmInstall()
216218
for (const file of files) {
217219
writeFileSync(join(dir, file.path), file.content)
@@ -245,16 +247,7 @@ describe('optimizeCommand', () => {
245247
target: target,
246248
}))
247249
)('$name', { timeout: 0 }, async ({ target }) => {
248-
writeFileSync(
249-
join(dir, 'package.json'),
250-
JSON.stringify({
251-
name: 'hono-cli-optimize-test',
252-
type: 'module',
253-
dependencies: {
254-
hono: 'latest',
255-
},
256-
})
257-
)
250+
writePackageJSON(dir)
258251
await npmInstall()
259252
writeFileSync(
260253
join(dir, './src/index.ts'),
@@ -271,16 +264,7 @@ describe('optimizeCommand', () => {
271264
})
272265

273266
it('should throw an error with invalid environment target', async () => {
274-
writeFileSync(
275-
join(dir, 'package.json'),
276-
JSON.stringify({
277-
name: 'hono-cli-optimize-test',
278-
type: 'module',
279-
dependencies: {
280-
hono: 'latest',
281-
},
282-
})
283-
)
267+
writePackageJSON(dir)
284268
await npmInstall()
285269
writeFileSync(
286270
join(dir, './src/index.ts'),
@@ -295,4 +279,228 @@ describe('optimizeCommand', () => {
295279
const promise = program.parseAsync(['node', 'hono', 'optimize', '-t', 'hoge'])
296280
await expect(promise).rejects.toThrowError()
297281
})
282+
283+
describe('request body API removal', () => {
284+
it(
285+
'should remove request body APIs when only GET/OPTIONS methods are used',
286+
{ timeout: 0 },
287+
async () => {
288+
writePackageJSON(dir)
289+
await npmInstall()
290+
writeFileSync(
291+
join(dir, './src/index.ts'),
292+
`
293+
import { Hono } from 'hono'
294+
const app = new Hono()
295+
app.get('/', (c) => c.text('Hello'))
296+
app.options('/cors', (c) => c.text('OK'))
297+
export default app
298+
`
299+
)
300+
await program.parseAsync(['node', 'hono', 'optimize'])
301+
302+
const content = readFileSync(join(dir, './dist/index.js'), 'utf-8')
303+
expect(content).not.toMatch(/parseBody/)
304+
expect(content).not.toMatch(/#cachedBody/)
305+
}
306+
)
307+
308+
it('should keep request body APIs when POST method is used', { timeout: 0 }, async () => {
309+
writePackageJSON(dir)
310+
await npmInstall()
311+
writeFileSync(
312+
join(dir, './src/index.ts'),
313+
`
314+
import { Hono } from 'hono'
315+
const app = new Hono()
316+
app.get('/', (c) => c.text('Hello'))
317+
app.post('/data', async (c) => {
318+
const body = await c.req.json()
319+
return c.json(body)
320+
})
321+
export default app
322+
`
323+
)
324+
await program.parseAsync(['node', 'hono', 'optimize'])
325+
326+
const content = readFileSync(join(dir, './dist/index.js'), 'utf-8')
327+
expect(content).toMatch(/parseBody/)
328+
})
329+
330+
it(
331+
'should keep request body APIs when --no-request-body-api-removal is specified',
332+
{ timeout: 0 },
333+
async () => {
334+
writePackageJSON(dir)
335+
await npmInstall()
336+
writeFileSync(
337+
join(dir, './src/index.ts'),
338+
`
339+
import { Hono } from 'hono'
340+
const app = new Hono()
341+
app.get('/', (c) => c.text('Hello'))
342+
export default app
343+
`
344+
)
345+
await program.parseAsync(['node', 'hono', 'optimize', '--no-request-body-api-removal'])
346+
347+
const content = readFileSync(join(dir, './dist/index.js'), 'utf-8')
348+
expect(content).toMatch(/parseBody/)
349+
}
350+
)
351+
})
352+
353+
describe('Hono API removal', () => {
354+
it('should remove unused Hono APIs (route, mount, fire)', { timeout: 0 }, async () => {
355+
writePackageJSON(dir)
356+
await npmInstall()
357+
writeFileSync(
358+
join(dir, './src/index.ts'),
359+
`
360+
import { Hono } from 'hono'
361+
const app = new Hono()
362+
app.get('/', (c) => c.text('Hello'))
363+
export default app
364+
`
365+
)
366+
await program.parseAsync(['node', 'hono', 'optimize', '-m'])
367+
368+
const content = readFileSync(join(dir, './dist/index.js'), 'utf-8')
369+
// These methods should be removed when unused
370+
expect(content).not.toMatch(/\bfire\s*\(/)
371+
})
372+
373+
it('should keep Hono APIs when route() is used', { timeout: 0 }, async () => {
374+
writePackageJSON(dir)
375+
await npmInstall()
376+
writeFileSync(
377+
join(dir, './src/index.ts'),
378+
`
379+
import { Hono } from 'hono'
380+
const app = new Hono()
381+
const subApp = new Hono()
382+
subApp.get('/', (c) => c.text('Sub'))
383+
app.route('/sub', subApp)
384+
export default app
385+
`
386+
)
387+
await program.parseAsync(['node', 'hono', 'optimize', '-m'])
388+
389+
const content = readFileSync(join(dir, './dist/index.js'), 'utf-8')
390+
// route method should be kept when used
391+
expect(content).toMatch(/\broute\b/)
392+
})
393+
394+
it('should keep Hono APIs when mount() is used', { timeout: 0 }, async () => {
395+
writePackageJSON(dir)
396+
await npmInstall()
397+
writeFileSync(
398+
join(dir, './src/index.ts'),
399+
`
400+
import { Hono } from 'hono'
401+
const app = new Hono()
402+
app.mount('/static', (req) => new Response('static'))
403+
export default app
404+
`
405+
)
406+
await program.parseAsync(['node', 'hono', 'optimize', '-m'])
407+
408+
const content = readFileSync(join(dir, './dist/index.js'), 'utf-8')
409+
// mount method should be kept when used
410+
expect(content).toMatch(/\bmount\b/)
411+
})
412+
413+
it(
414+
'should keep Hono APIs when --no-hono-api-removal is specified',
415+
{ timeout: 0 },
416+
async () => {
417+
writePackageJSON(dir)
418+
await npmInstall()
419+
writeFileSync(
420+
join(dir, './src/index.ts'),
421+
`
422+
import { Hono } from 'hono'
423+
const app = new Hono()
424+
app.get('/', (c) => c.text('Hello'))
425+
export default app
426+
`
427+
)
428+
await program.parseAsync(['node', 'hono', 'optimize', '-m', '--no-hono-api-removal'])
429+
430+
const content = readFileSync(join(dir, './dist/index.js'), 'utf-8')
431+
// fire method should be kept when --no-hono-api-removal is specified
432+
expect(content).toMatch(/\bfire\b/)
433+
}
434+
)
435+
})
436+
437+
describe('context response API removal', () => {
438+
it('should remove unused context response APIs', { timeout: 0 }, async () => {
439+
writePackageJSON(dir)
440+
await npmInstall()
441+
writeFileSync(
442+
join(dir, './src/index.ts'),
443+
`
444+
import { Hono } from 'hono'
445+
const app = new Hono()
446+
app.get('/', (c) => c.text('Hello'))
447+
export default app
448+
`
449+
)
450+
await program.parseAsync(['node', 'hono', 'optimize'])
451+
452+
const content = readFileSync(join(dir, './dist/index.js'), 'utf-8')
453+
// Unused response methods should be removed (json, html, redirect)
454+
// text is used, so it should remain
455+
expect(content).toMatch(/\btext\s*\(/)
456+
})
457+
458+
it('should keep context response APIs when they are used', { timeout: 0 }, async () => {
459+
writePackageJSON(dir)
460+
await npmInstall()
461+
writeFileSync(
462+
join(dir, './src/index.ts'),
463+
`
464+
import { Hono } from 'hono'
465+
const app = new Hono()
466+
app.get('/', (c) => c.json({ message: 'Hello' }))
467+
app.get('/html', (c) => c.html('<h1>Hello</h1>'))
468+
app.get('/redirect', (c) => c.redirect('/'))
469+
export default app
470+
`
471+
)
472+
await program.parseAsync(['node', 'hono', 'optimize'])
473+
474+
const content = readFileSync(join(dir, './dist/index.js'), 'utf-8')
475+
// Used response methods should be kept
476+
expect(content).toMatch(/\bjson\s*\(/)
477+
expect(content).toMatch(/\bhtml\s*\(/)
478+
expect(content).toMatch(/\bredirect\s*\(/)
479+
})
480+
481+
it(
482+
'should keep context response APIs when --no-context-response-api-removal is specified',
483+
{ timeout: 0 },
484+
async () => {
485+
writePackageJSON(dir)
486+
await npmInstall()
487+
writeFileSync(
488+
join(dir, './src/index.ts'),
489+
`
490+
import { Hono } from 'hono'
491+
const app = new Hono()
492+
app.get('/', (c) => c.text('Hello'))
493+
export default app
494+
`
495+
)
496+
await program.parseAsync(['node', 'hono', 'optimize', '--no-context-response-api-removal'])
497+
498+
const content = readFileSync(join(dir, './dist/index.js'), 'utf-8')
499+
// All response methods should be kept when --no-context-response-api-removal is specified
500+
expect(content).toMatch(/\bjson\s*\(/)
501+
expect(content).toMatch(/\bhtml\s*\(/)
502+
expect(content).toMatch(/\bredirect\s*\(/)
503+
}
504+
)
505+
})
298506
})

0 commit comments

Comments
 (0)