Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Mounting Helper #2853

Closed
wants to merge 1 commit into from
Closed
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
204 changes: 204 additions & 0 deletions src/helper/mounting/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import type { ExecutionContext } from '../../context'
import { Hono } from '../../hono'
import { getPath } from '../../utils/url'
import { toHandler } from './index'

const createAnotherApp = (basePath: string = '') => {
return (req: Request, params: unknown) => {
const path = getPath(req)
if (path === `${basePath === '' ? '/' : basePath}`) {
return new Response('AnotherApp')
}
if (path === `${basePath}/hello`) {
return new Response('Hello from AnotherApp')
}
if (path === `${basePath}/header`) {
const message = req.headers.get('x-message')
return new Response(message)
}
if (path === `${basePath}/with-query`) {
const queryStrings = new URL(req.url).searchParams.toString()
return new Response(queryStrings)
}
if (path == `${basePath}/with-params`) {
return new Response(
JSON.stringify({
params,
}),
{
headers: {
'Content-Type': 'application.json',
},
}
)
}
if (path === `${basePath}/path`) {
return new Response(getPath(req))
}
return new Response('Not Found from AnotherApp', {
status: 404,
})
}
}

const testAnotherApp = (app: Hono) => {
it('Should return 200 from AnotherApp - /app', async () => {
const res = await app.request('/app')
expect(res.status).toBe(200)
expect(res.headers.get('x-message')).toBe('Foo')
expect(await res.text()).toBe('AnotherApp')
})

it('Should return 200 from AnotherApp - /app/hello', async () => {
const res = await app.request('/app/hello')
expect(res.status).toBe(200)
expect(res.headers.get('x-message')).toBe('Foo')
expect(await res.text()).toBe('Hello from AnotherApp')
})

it('Should return 200 from AnotherApp - /app/header', async () => {
const res = await app.request('/app/header', {
headers: {
'x-message': 'Message Foo!',
},
})
expect(res.status).toBe(200)
expect(res.headers.get('x-message')).toBe('Foo')
expect(await res.text()).toBe('Message Foo!')
})

it('Should return 404 from AnotherApp - /app/not-found', async () => {
const res = await app.request('/app/not-found')
expect(res.status).toBe(404)
expect(res.headers.get('x-message')).toBe('Foo')
expect(await res.text()).toBe('Not Found from AnotherApp')
})

it('Should return 200 from AnotherApp - /app/with-query?foo=bar&baz-qux', async () => {
const res = await app.request('/app/with-query?foo=bar&baz=qux')
expect(res.status).toBe(200)
expect(await res.text()).toBe('foo=bar&baz=qux')
})

it('Should return 200 from AnotherApp - /app/with-params', async () => {
const res = await app.request('/app/with-params')
expect(res.status).toBe(200)
expect(await res.json()).toEqual({
params: 'params',
})
})
}

describe('Basic', () => {
const anotherApp = createAnotherApp('/app')
const app = new Hono()
app.use('*', async (c, next) => {
await next()
c.header('x-message', 'Foo')
})
app.get('/', (c) => c.text('Hono'))
app.all(
'/app/*',
toHandler(anotherApp, {
optionHandler: () => 'params',
})
)

it('Should return 200 from Hono app', async () => {
const res = await app.request('/')
expect(res.status).toBe(200)
expect(res.headers.get('x-message')).toBe('Foo')
expect(await res.text()).toBe('Hono')
})

testAnotherApp(app)

it('Should return 200 from AnotherApp - /app/path', async () => {
const res = await app.request('/app/path')
expect(res.status).toBe(200)
expect(await res.text()).toBe('/app/path')
})
})

describe('With basePath', () => {
const anotherApp = createAnotherApp()
const app = new Hono()
app.use('*', async (c, next) => {
await next()
c.header('x-message', 'Foo')
})
app.all(
'/app/*',
toHandler(anotherApp, {
optionHandler: () => 'params',
basePath: '/app',
})
)

testAnotherApp(app)

it('Should return 200 from AnotherApp - /app/path', async () => {
const res = await app.request('/app/path')
expect(res.status).toBe(200)
expect(await res.text()).toBe('/path')
})
})

describe('With fetch', () => {
const anotherApp = async (req: Request, env: {}, executionContext: ExecutionContext) => {
const path = getPath(req)
if (path === '/') {
return new Response(
JSON.stringify({
env,
executionContext,
}),
{
headers: {
'Content-Type': 'application/json',
},
}
)
}
return new Response('Not Found from AnotherApp', {
status: 404,
})
}

const app = new Hono()
app.all(
'/another-app/*',
toHandler(anotherApp, {
basePath: '/another-app',
})
)

it('Should handle Env and ExecuteContext', async () => {
const request = new Request('http://localhost/another-app')
const res = await app.fetch(
request,
{
TOKEN: 'foo',
},
{
// Force mocking!
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
waitUntil: 'waitUntil',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
passThroughOnException: 'passThroughOnException',
}
)
expect(res.status).toBe(200)
expect(await res.json()).toEqual({
env: {
TOKEN: 'foo',
},
executionContext: {
waitUntil: 'waitUntil',
passThroughOnException: 'passThroughOnException',
},
})
})
})
79 changes: 79 additions & 0 deletions src/helper/mounting/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* @module
* Mounting Helper for Hono.
*/

import type { Context, ExecutionContext } from '../../context'
import type { MiddlewareHandler } from '../../types'
import { getQueryStrings, mergePath } from '../../utils/url'

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Application = (request: Request, ...args: any) => Response | Promise<Response>
type OptionHandler = (c: Context) => unknown
type ToHandlerOptions = {
optionHandler?: OptionHandler
basePath?: string
}

/**
* @param {Application} application - The application handler to be used.
* @param {ToHandlerOptions} [options] - Optional configurations for the handler.
* @param {OptionHandler} [options.optionHandler] - A function to handle additional options.
* @param {string} [options.basePath] - The base path to be used for the application.
* @returns {MiddlewareHandler} The middleware handler function.
*
* @example
* ```ts
* const app = new Hono()
*
* app.all(
* '/another-app/*',
* toHandler(anotherApp.fetch, {
* basePath: '/another-app',
* })
* )
* ```
*/
export const toHandler = (
application: Application,
options?: ToHandlerOptions
): MiddlewareHandler => {
return async (c, next) => {
let executionContext: ExecutionContext | undefined = undefined
try {
executionContext = c.executionCtx
} catch {} // Do nothing

let applicationOptions: unknown[] = []
if (options?.optionHandler) {
const result = options.optionHandler(c)
applicationOptions = Array.isArray(result) ? result : [result]
} else {
applicationOptions = [c.env, executionContext]
}

let path: string
if (options?.basePath) {
const basePath = mergePath('/', options.basePath)
const regexp = new RegExp(`^${basePath}`)
path = c.req.path.replace(regexp, '')
if (path === '') {
path = '/'
}
} else {
path = c.req.path
}

const queryStrings = getQueryStrings(c.req.url)
const res = await application(
new Request(new URL(path + queryStrings, c.req.url), c.req.raw),
...applicationOptions
)

if (res) {
return res
}

await next()
}
}