From 3c0a2a49eeccd9d68b6aed27db31e87504abd815 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Wed, 29 May 2024 16:21:55 +0900 Subject: [PATCH] WIP --- src/helper/mounting/index.test.ts | 204 ++++++++++++++++++++++++++++++ src/helper/mounting/index.ts | 79 ++++++++++++ 2 files changed, 283 insertions(+) create mode 100644 src/helper/mounting/index.test.ts create mode 100644 src/helper/mounting/index.ts diff --git a/src/helper/mounting/index.test.ts b/src/helper/mounting/index.test.ts new file mode 100644 index 00000000..6b53dbb8 --- /dev/null +++ b/src/helper/mounting/index.test.ts @@ -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', + }, + }) + }) +}) diff --git a/src/helper/mounting/index.ts b/src/helper/mounting/index.ts new file mode 100644 index 00000000..94c0edb3 --- /dev/null +++ b/src/helper/mounting/index.ts @@ -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 +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() + } +}