hono-base.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. // src/hono-base.ts
  2. import { compose } from "./compose.js";
  3. import { Context } from "./context.js";
  4. import { METHODS, METHOD_NAME_ALL, METHOD_NAME_ALL_LOWERCASE } from "./router.js";
  5. import { COMPOSED_HANDLER } from "./utils/constants.js";
  6. import { getPath, getPathNoStrict, mergePath } from "./utils/url.js";
  7. var notFoundHandler = (c) => {
  8. return c.text("404 Not Found", 404);
  9. };
  10. var errorHandler = (err, c) => {
  11. if ("getResponse" in err) {
  12. const res = err.getResponse();
  13. return c.newResponse(res.body, res);
  14. }
  15. console.error(err);
  16. return c.text("Internal Server Error", 500);
  17. };
  18. var Hono = class _Hono {
  19. get;
  20. post;
  21. put;
  22. delete;
  23. options;
  24. patch;
  25. all;
  26. on;
  27. use;
  28. /*
  29. This class is like an abstract class and does not have a router.
  30. To use it, inherit the class and implement router in the constructor.
  31. */
  32. router;
  33. getPath;
  34. // Cannot use `#` because it requires visibility at JavaScript runtime.
  35. _basePath = "/";
  36. #path = "/";
  37. routes = [];
  38. constructor(options = {}) {
  39. const allMethods = [...METHODS, METHOD_NAME_ALL_LOWERCASE];
  40. allMethods.forEach((method) => {
  41. this[method] = (args1, ...args) => {
  42. if (typeof args1 === "string") {
  43. this.#path = args1;
  44. } else {
  45. this.#addRoute(method, this.#path, args1);
  46. }
  47. args.forEach((handler) => {
  48. this.#addRoute(method, this.#path, handler);
  49. });
  50. return this;
  51. };
  52. });
  53. this.on = (method, path, ...handlers) => {
  54. for (const p of [path].flat()) {
  55. this.#path = p;
  56. for (const m of [method].flat()) {
  57. handlers.map((handler) => {
  58. this.#addRoute(m.toUpperCase(), this.#path, handler);
  59. });
  60. }
  61. }
  62. return this;
  63. };
  64. this.use = (arg1, ...handlers) => {
  65. if (typeof arg1 === "string") {
  66. this.#path = arg1;
  67. } else {
  68. this.#path = "*";
  69. handlers.unshift(arg1);
  70. }
  71. handlers.forEach((handler) => {
  72. this.#addRoute(METHOD_NAME_ALL, this.#path, handler);
  73. });
  74. return this;
  75. };
  76. const { strict, ...optionsWithoutStrict } = options;
  77. Object.assign(this, optionsWithoutStrict);
  78. this.getPath = strict ?? true ? options.getPath ?? getPath : getPathNoStrict;
  79. }
  80. #clone() {
  81. const clone = new _Hono({
  82. router: this.router,
  83. getPath: this.getPath
  84. });
  85. clone.errorHandler = this.errorHandler;
  86. clone.#notFoundHandler = this.#notFoundHandler;
  87. clone.routes = this.routes;
  88. return clone;
  89. }
  90. #notFoundHandler = notFoundHandler;
  91. // Cannot use `#` because it requires visibility at JavaScript runtime.
  92. errorHandler = errorHandler;
  93. /**
  94. * `.route()` allows grouping other Hono instance in routes.
  95. *
  96. * @see {@link https://hono.dev/docs/api/routing#grouping}
  97. *
  98. * @param {string} path - base Path
  99. * @param {Hono} app - other Hono instance
  100. * @returns {Hono} routed Hono instance
  101. *
  102. * @example
  103. * ```ts
  104. * const app = new Hono()
  105. * const app2 = new Hono()
  106. *
  107. * app2.get("/user", (c) => c.text("user"))
  108. * app.route("/api", app2) // GET /api/user
  109. * ```
  110. */
  111. route(path, app) {
  112. const subApp = this.basePath(path);
  113. app.routes.map((r) => {
  114. let handler;
  115. if (app.errorHandler === errorHandler) {
  116. handler = r.handler;
  117. } else {
  118. handler = async (c, next) => (await compose([], app.errorHandler)(c, () => r.handler(c, next))).res;
  119. handler[COMPOSED_HANDLER] = r.handler;
  120. }
  121. subApp.#addRoute(r.method, r.path, handler);
  122. });
  123. return this;
  124. }
  125. /**
  126. * `.basePath()` allows base paths to be specified.
  127. *
  128. * @see {@link https://hono.dev/docs/api/routing#base-path}
  129. *
  130. * @param {string} path - base Path
  131. * @returns {Hono} changed Hono instance
  132. *
  133. * @example
  134. * ```ts
  135. * const api = new Hono().basePath('/api')
  136. * ```
  137. */
  138. basePath(path) {
  139. const subApp = this.#clone();
  140. subApp._basePath = mergePath(this._basePath, path);
  141. return subApp;
  142. }
  143. /**
  144. * `.onError()` handles an error and returns a customized Response.
  145. *
  146. * @see {@link https://hono.dev/docs/api/hono#error-handling}
  147. *
  148. * @param {ErrorHandler} handler - request Handler for error
  149. * @returns {Hono} changed Hono instance
  150. *
  151. * @example
  152. * ```ts
  153. * app.onError((err, c) => {
  154. * console.error(`${err}`)
  155. * return c.text('Custom Error Message', 500)
  156. * })
  157. * ```
  158. */
  159. onError = (handler) => {
  160. this.errorHandler = handler;
  161. return this;
  162. };
  163. /**
  164. * `.notFound()` allows you to customize a Not Found Response.
  165. *
  166. * @see {@link https://hono.dev/docs/api/hono#not-found}
  167. *
  168. * @param {NotFoundHandler} handler - request handler for not-found
  169. * @returns {Hono} changed Hono instance
  170. *
  171. * @example
  172. * ```ts
  173. * app.notFound((c) => {
  174. * return c.text('Custom 404 Message', 404)
  175. * })
  176. * ```
  177. */
  178. notFound = (handler) => {
  179. this.#notFoundHandler = handler;
  180. return this;
  181. };
  182. /**
  183. * `.mount()` allows you to mount applications built with other frameworks into your Hono application.
  184. *
  185. * @see {@link https://hono.dev/docs/api/hono#mount}
  186. *
  187. * @param {string} path - base Path
  188. * @param {Function} applicationHandler - other Request Handler
  189. * @param {MountOptions} [options] - options of `.mount()`
  190. * @returns {Hono} mounted Hono instance
  191. *
  192. * @example
  193. * ```ts
  194. * import { Router as IttyRouter } from 'itty-router'
  195. * import { Hono } from 'hono'
  196. * // Create itty-router application
  197. * const ittyRouter = IttyRouter()
  198. * // GET /itty-router/hello
  199. * ittyRouter.get('/hello', () => new Response('Hello from itty-router'))
  200. *
  201. * const app = new Hono()
  202. * app.mount('/itty-router', ittyRouter.handle)
  203. * ```
  204. *
  205. * @example
  206. * ```ts
  207. * const app = new Hono()
  208. * // Send the request to another application without modification.
  209. * app.mount('/app', anotherApp, {
  210. * replaceRequest: (req) => req,
  211. * })
  212. * ```
  213. */
  214. mount(path, applicationHandler, options) {
  215. let replaceRequest;
  216. let optionHandler;
  217. if (options) {
  218. if (typeof options === "function") {
  219. optionHandler = options;
  220. } else {
  221. optionHandler = options.optionHandler;
  222. if (options.replaceRequest === false) {
  223. replaceRequest = (request) => request;
  224. } else {
  225. replaceRequest = options.replaceRequest;
  226. }
  227. }
  228. }
  229. const getOptions = optionHandler ? (c) => {
  230. const options2 = optionHandler(c);
  231. return Array.isArray(options2) ? options2 : [options2];
  232. } : (c) => {
  233. let executionContext = void 0;
  234. try {
  235. executionContext = c.executionCtx;
  236. } catch {
  237. }
  238. return [c.env, executionContext];
  239. };
  240. replaceRequest ||= (() => {
  241. const mergedPath = mergePath(this._basePath, path);
  242. const pathPrefixLength = mergedPath === "/" ? 0 : mergedPath.length;
  243. return (request) => {
  244. const url = new URL(request.url);
  245. url.pathname = url.pathname.slice(pathPrefixLength) || "/";
  246. return new Request(url, request);
  247. };
  248. })();
  249. const handler = async (c, next) => {
  250. const res = await applicationHandler(replaceRequest(c.req.raw), ...getOptions(c));
  251. if (res) {
  252. return res;
  253. }
  254. await next();
  255. };
  256. this.#addRoute(METHOD_NAME_ALL, mergePath(path, "*"), handler);
  257. return this;
  258. }
  259. #addRoute(method, path, handler) {
  260. method = method.toUpperCase();
  261. path = mergePath(this._basePath, path);
  262. const r = { basePath: this._basePath, path, method, handler };
  263. this.router.add(method, path, [handler, r]);
  264. this.routes.push(r);
  265. }
  266. #handleError(err, c) {
  267. if (err instanceof Error) {
  268. return this.errorHandler(err, c);
  269. }
  270. throw err;
  271. }
  272. #dispatch(request, executionCtx, env, method) {
  273. if (method === "HEAD") {
  274. return (async () => new Response(null, await this.#dispatch(request, executionCtx, env, "GET")))();
  275. }
  276. const path = this.getPath(request, { env });
  277. const matchResult = this.router.match(method, path);
  278. const c = new Context(request, {
  279. path,
  280. matchResult,
  281. env,
  282. executionCtx,
  283. notFoundHandler: this.#notFoundHandler
  284. });
  285. if (matchResult[0].length === 1) {
  286. let res;
  287. try {
  288. res = matchResult[0][0][0][0](c, async () => {
  289. c.res = await this.#notFoundHandler(c);
  290. });
  291. } catch (err) {
  292. return this.#handleError(err, c);
  293. }
  294. return res instanceof Promise ? res.then(
  295. (resolved) => resolved || (c.finalized ? c.res : this.#notFoundHandler(c))
  296. ).catch((err) => this.#handleError(err, c)) : res ?? this.#notFoundHandler(c);
  297. }
  298. const composed = compose(matchResult[0], this.errorHandler, this.#notFoundHandler);
  299. return (async () => {
  300. try {
  301. const context = await composed(c);
  302. if (!context.finalized) {
  303. throw new Error(
  304. "Context is not finalized. Did you forget to return a Response object or `await next()`?"
  305. );
  306. }
  307. return context.res;
  308. } catch (err) {
  309. return this.#handleError(err, c);
  310. }
  311. })();
  312. }
  313. /**
  314. * `.fetch()` will be entry point of your app.
  315. *
  316. * @see {@link https://hono.dev/docs/api/hono#fetch}
  317. *
  318. * @param {Request} request - request Object of request
  319. * @param {Env} Env - env Object
  320. * @param {ExecutionContext} - context of execution
  321. * @returns {Response | Promise<Response>} response of request
  322. *
  323. */
  324. fetch = (request, ...rest) => {
  325. return this.#dispatch(request, rest[1], rest[0], request.method);
  326. };
  327. /**
  328. * `.request()` is a useful method for testing.
  329. * You can pass a URL or pathname to send a GET request.
  330. * app will return a Response object.
  331. * ```ts
  332. * test('GET /hello is ok', async () => {
  333. * const res = await app.request('/hello')
  334. * expect(res.status).toBe(200)
  335. * })
  336. * ```
  337. * @see https://hono.dev/docs/api/hono#request
  338. */
  339. request = (input, requestInit, Env, executionCtx) => {
  340. if (input instanceof Request) {
  341. return this.fetch(requestInit ? new Request(input, requestInit) : input, Env, executionCtx);
  342. }
  343. input = input.toString();
  344. return this.fetch(
  345. new Request(
  346. /^https?:\/\//.test(input) ? input : `http://localhost${mergePath("/", input)}`,
  347. requestInit
  348. ),
  349. Env,
  350. executionCtx
  351. );
  352. };
  353. /**
  354. * `.fire()` automatically adds a global fetch event listener.
  355. * This can be useful for environments that adhere to the Service Worker API, such as non-ES module Cloudflare Workers.
  356. * @deprecated
  357. * Use `fire` from `hono/service-worker` instead.
  358. * ```ts
  359. * import { Hono } from 'hono'
  360. * import { fire } from 'hono/service-worker'
  361. *
  362. * const app = new Hono()
  363. * // ...
  364. * fire(app)
  365. * ```
  366. * @see https://hono.dev/docs/api/hono#fire
  367. * @see https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API
  368. * @see https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/
  369. */
  370. fire = () => {
  371. addEventListener("fetch", (event) => {
  372. event.respondWith(this.#dispatch(event.request, event, void 0, event.request.method));
  373. });
  374. };
  375. };
  376. export {
  377. Hono as HonoBase
  378. };