hono 1.2.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -52,7 +52,7 @@ Fastest is hono - regexp-router
52
52
  Routers used in Hono are really smart.
53
53
 
54
54
  - **TrieRouter**(default) - Implemented with Trie tree structure.
55
- - **RegExpRouter** - Match routes with one big Regex made before dispatching at once.
55
+ - **RegExpRouter** - Match the route with using one big Regex made before dispatch.
56
56
 
57
57
  ## Hono in 1 minute
58
58
 
@@ -93,16 +93,14 @@ app.use('*', etag(), logger())
93
93
  And, the routing of Hono is so flexible. It's easy to construct large web applications.
94
94
 
95
95
  ```ts
96
- import { Hono, Route } from 'hono'
97
- import { cors } from 'hono/cors'
98
-
99
- const app = new Hono()
96
+ import { Hono } from 'hono'
97
+ import { basicAuth } from 'hono/basic-auth'
100
98
 
101
- const v1 = new Route()
99
+ const v1 = new Hono()
102
100
  v1.get('/posts', (c) => {
103
101
  return c.text('list pots')
104
102
  })
105
- .post('/posts', cors(), (c) => {
103
+ .post(basicAuth({ username, password }), (c) => {
106
104
  return c.text('created!', 201)
107
105
  })
108
106
  .get('/posts/:id', (c) => {
@@ -110,6 +108,7 @@ v1.get('/posts', (c) => {
110
108
  return c.text(`your id is ${id}`)
111
109
  })
112
110
 
111
+ const app = new Hono()
113
112
  app.route('/v1', v1)
114
113
  ```
115
114
 
@@ -140,7 +139,7 @@ An instance of `Hono` has these methods.
140
139
 
141
140
  - app.**HTTP_METHOD**(\[path,\] handler|middleware...)
142
141
  - app.**all**(\[path,\] handler|middleware...)
143
- - app.**route**(path, \[Route\])
142
+ - app.**route**(path, \[app\])
144
143
  - app.**use**(\[path,\] middleware)
145
144
  - app.**notFound**(handler)
146
145
  - app.**onError**(err, handler)
@@ -156,6 +155,8 @@ An instance of `Hono` has these methods.
156
155
  // HTTP Methods
157
156
  app.get('/', (c) => c.text('GET /'))
158
157
  app.post('/', (c) => c.text('POST /'))
158
+ app.put('/', (c) => c.text('PUT /'))
159
+ app.delete('/', (c) => c.text('DELETE /'))
159
160
 
160
161
  // Wildcard
161
162
  app.get('/wild/*/card', (c) => {
@@ -175,12 +176,20 @@ app.get('/user/:name', (c) => {
175
176
  })
176
177
  ```
177
178
 
179
+ or all parameters at once:
180
+
181
+ ```ts
182
+ app.get('/posts/:id/comment/:comment_id', (c) => {
183
+ const { id, comment_id } = c.req.param()
184
+ ...
185
+ })
186
+ ```
187
+
178
188
  ### Regexp
179
189
 
180
190
  ```ts
181
191
  app.get('/post/:date{[0-9]+}/:title{[a-z]+}', (c) => {
182
- const date = c.req.param('date')
183
- const title = c.req.param('title')
192
+ const { date, title } = c.req.param()
184
193
  ...
185
194
  })
186
195
  ```
@@ -219,12 +228,12 @@ app.get('/fetch-url', async (c) => {
219
228
  })
220
229
  ```
221
230
 
222
- ## Route
231
+ ## Grouping
223
232
 
224
- `Route` object enables Nested route.
233
+ Group the routes with `Hono` instance and add them to the main app with `route` method.
225
234
 
226
235
  ```ts
227
- const book = new Route()
236
+ const book = new Hono()
228
237
 
229
238
  book.get('/', (c) => c.text('List Books')) // GET /book
230
239
  book.get('/:id', (c) => {
@@ -234,6 +243,7 @@ book.get('/:id', (c) => {
234
243
  })
235
244
  book.post('/', (c) => c.text('Create Book')) // POST /book
236
245
 
246
+ const app = new Hono()
237
247
  app.route('/book', book)
238
248
  ```
239
249
 
@@ -338,6 +348,12 @@ app.get('/search', (c) => {
338
348
  ...
339
349
  })
340
350
 
351
+ // Get all params at once
352
+ app.get('/search', (c) => {
353
+ const { q, limit, offset } = c.req.query()
354
+ ...
355
+ })
356
+
341
357
  // Captured params
342
358
  app.get('/entry/:id', (c) => {
343
359
  const id = c.req.param('id')
@@ -427,8 +443,8 @@ app.get('/redirect-permanently', (c) => c.redirect('/', 301))
427
443
 
428
444
  ```ts
429
445
  // Response object
430
- app.use('/', (c, next) => {
431
- next()
446
+ app.use('/', async (c, next) => {
447
+ await next()
432
448
  c.res.headers.append('X-Debug', 'Debug message')
433
449
  })
434
450
  ```
@@ -437,11 +453,11 @@ app.use('/', (c, next) => {
437
453
 
438
454
  ```ts
439
455
  // FetchEvent object
440
- app.use('*', async (c, next) => {
456
+ app.get('/foo', async (c) => {
441
457
  c.event.waitUntil(
442
- ...
458
+ c.env.KV.put(key, data)
443
459
  )
444
- await next()
460
+ ...
445
461
  })
446
462
  ```
447
463
 
@@ -559,7 +575,62 @@ To generate a project skelton, run this command.
559
575
  npx create-cloudflare my-app https://github.com/honojs/hono-minimal
560
576
  ```
561
577
 
562
- ## Examples
578
+ ## Practical Example
579
+
580
+ How about writing web API with Hono?
581
+
582
+ ```ts
583
+ import { Hono } from 'hono'
584
+ import { cors } from 'hono/cors'
585
+ import { basicAuth } from 'hono/basic-auth'
586
+ import { prettyJSON } from 'hono/pretty-json'
587
+ import { getPosts, getPosts, createPost } from './model'
588
+
589
+ const app = new Hono()
590
+ app.get('/', (c) => c.text('Pretty Blog API'))
591
+ app.use('*', prettyJSON())
592
+ app.notFound((c) => c.json({ message: 'Not Found', ok: false }, 404))
593
+
594
+ export interface Bindings {
595
+ USERNAME: string
596
+ PASSWORD: string
597
+ }
598
+
599
+ const api = new Hono<Bindings>()
600
+
601
+ api.get('/posts', (c) => {
602
+ const { limit, offset } = c.req.query()
603
+ const posts = getPosts({ limit, offset })
604
+ return c.json({ posts })
605
+ })
606
+
607
+ api.get('/posts/:id', (c) => {
608
+ const id = c.req.param('id')
609
+ const post = getPost({ id })
610
+ return c.json({ post })
611
+ })
612
+
613
+ api.post(
614
+ '/posts',
615
+ async (c, next) => {
616
+ const auth = basicAuth({ username: c.env.USERNAME, password: c.env.PASSWORD })
617
+ await auth(c, next)
618
+ },
619
+ async (c) => {
620
+ const post = await c.req.json<POST>()
621
+ const ok = createPost({ post })
622
+ return c.json({ ok })
623
+ }
624
+ )
625
+
626
+ app.use('/posts/*', cors())
627
+
628
+ app.route('/api', api)
629
+
630
+ export default app
631
+ ```
632
+
633
+ ## Other Examples
563
634
 
564
635
  - Hono Examples - <https://github.com/honojs/examples>
565
636
 
package/dist/context.d.ts CHANGED
@@ -3,7 +3,7 @@ import type { StatusCode } from './utils/http-status';
3
3
  declare type Headers = Record<string, string>;
4
4
  declare type Data = string | ArrayBuffer | ReadableStream;
5
5
  export declare type Env = Record<string, any>;
6
- export declare class Context<RequestParamKeyType = string, E = Env> {
6
+ export declare class Context<RequestParamKeyType extends string = string, E = Env> {
7
7
  req: Request<RequestParamKeyType>;
8
8
  res: Response;
9
9
  env: E;
package/dist/context.js CHANGED
@@ -16,13 +16,31 @@ class Context {
16
16
  Object.assign(this, opts);
17
17
  }
18
18
  initRequest(req) {
19
- req.header = (name) => {
20
- return req.headers.get(name);
21
- };
22
- req.query = (key) => {
19
+ req.header = ((name) => {
20
+ if (name) {
21
+ return req.headers.get(name);
22
+ }
23
+ else {
24
+ const result = {};
25
+ for (const [key, value] of req.headers) {
26
+ result[key] = value;
27
+ }
28
+ return result;
29
+ }
30
+ });
31
+ req.query = ((key) => {
23
32
  const url = new URL(req.url);
24
- return url.searchParams.get(key);
25
- };
33
+ if (key) {
34
+ return url.searchParams.get(key);
35
+ }
36
+ else {
37
+ const result = {};
38
+ for (const [key, value] of url.searchParams) {
39
+ result[key] = value;
40
+ }
41
+ return result;
42
+ }
43
+ });
26
44
  return req;
27
45
  }
28
46
  header(name, value) {
package/dist/hono.d.ts CHANGED
@@ -2,15 +2,23 @@
2
2
  import { Context } from './context';
3
3
  import type { Env } from './context';
4
4
  import type { Router } from './router';
5
- import { METHOD_NAME_ALL_LOWERCASE } from './router';
6
5
  declare global {
7
- interface Request<ParamKeyType = string> {
8
- param: (key: ParamKeyType) => string;
9
- query: (key: string) => string;
10
- header: (name: string) => string;
6
+ interface Request<ParamKeyType extends string = string> {
7
+ param: {
8
+ (key: ParamKeyType): string;
9
+ (): Record<ParamKeyType, string>;
10
+ };
11
+ query: {
12
+ (key: string): string;
13
+ (): Record<string, string>;
14
+ };
15
+ header: {
16
+ (name: string): string;
17
+ (): Record<string, string>;
18
+ };
11
19
  }
12
20
  }
13
- export declare type Handler<RequestParamKeyType = string, E = Env> = (c: Context<RequestParamKeyType, E>, next: Next) => Response | Promise<Response> | void | Promise<void>;
21
+ export declare type Handler<RequestParamKeyType extends string = string, E = Env> = (c: Context<RequestParamKeyType, E>, next: Next) => Response | Promise<Response> | void | Promise<void>;
14
22
  export declare type NotFoundHandler<E = Env> = (c: Context<string, E>) => Response;
15
23
  export declare type ErrorHandler<E = Env> = (err: Error, c: Context<string, E>) => Response;
16
24
  export declare type Next = () => Promise<void>;
@@ -23,29 +31,11 @@ interface HandlerInterface<T extends string, E = Env, U = Hono<E, T>> {
23
31
  <Path extends string>(...handlers: Handler<ParamKeys<Path> extends never ? string : ParamKeys<Path>, E>[]): U;
24
32
  (...handlers: Handler<string, E>[]): U;
25
33
  }
26
- declare const methods: readonly ["get", "post", "put", "delete", "head", "options", "patch"];
27
- declare type Methods = typeof methods[number] | typeof METHOD_NAME_ALL_LOWERCASE;
28
- interface Routing<E extends Env> {
34
+ interface Route<E extends Env> {
29
35
  path: string;
30
- method: Methods;
36
+ method: string;
31
37
  handler: Handler<string, E>;
32
38
  }
33
- declare const Route_base: new <E_1 extends Env, T extends string, U>() => {
34
- delete: HandlerInterface<T, E_1, U>;
35
- get: HandlerInterface<T, E_1, U>;
36
- all: HandlerInterface<T, E_1, U>;
37
- post: HandlerInterface<T, E_1, U>;
38
- put: HandlerInterface<T, E_1, U>;
39
- head: HandlerInterface<T, E_1, U>;
40
- options: HandlerInterface<T, E_1, U>;
41
- patch: HandlerInterface<T, E_1, U>;
42
- };
43
- export declare class Route<E = Env, P extends string = ''> extends Route_base<E, P, Route<E, P>> {
44
- #private;
45
- routes: Routing<E>[];
46
- constructor();
47
- private add;
48
- }
49
39
  declare const Hono_base: new <E_1 extends Env, T extends string, U>() => {
50
40
  delete: HandlerInterface<T, E_1, U>;
51
41
  get: HandlerInterface<T, E_1, U>;
@@ -56,17 +46,19 @@ declare const Hono_base: new <E_1 extends Env, T extends string, U>() => {
56
46
  options: HandlerInterface<T, E_1, U>;
57
47
  patch: HandlerInterface<T, E_1, U>;
58
48
  };
59
- export declare class Hono<E = Env, P extends string = ''> extends Hono_base<E, P, Hono<E, P>> {
60
- #private;
49
+ export declare class Hono<E = Env, P extends string = '/'> extends Hono_base<E, P, Hono<E, P>> {
61
50
  readonly routerClass: {
62
51
  new (): Router<any>;
63
52
  };
64
53
  readonly strict: boolean;
54
+ private _router;
55
+ private _tempPath;
65
56
  private path;
57
+ routes: Route<E>[];
66
58
  constructor(init?: Partial<Pick<Hono, 'routerClass' | 'strict'>>);
67
59
  private notFoundHandler;
68
60
  private errorHandler;
69
- route(path: string, route?: Route): Hono<E, P>;
61
+ route(path: string, app?: Hono<any>): Hono<E, P>;
70
62
  use(path: string, ...middleware: Handler<string, E>[]): Hono<E, P>;
71
63
  use(...middleware: Handler<string, E>[]): Hono<E, P>;
72
64
  onError(handler: ErrorHandler<E>): Hono<E, P>;
package/dist/hono.js CHANGED
@@ -1,18 +1,6 @@
1
1
  "use strict";
2
- var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
3
- if (kind === "m") throw new TypeError("Private method is not writable");
4
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
5
- if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
6
- return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
7
- };
8
- var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
9
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
10
- if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
11
- return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
12
- };
13
- var _Route_path, _Hono_router, _Hono_tempPath;
14
2
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.Hono = exports.Route = void 0;
3
+ exports.Hono = void 0;
16
4
  const compose_1 = require("./compose");
17
5
  const context_1 = require("./context");
18
6
  const router_1 = require("./router");
@@ -24,44 +12,13 @@ function defineDynamicClass() {
24
12
  return class {
25
13
  };
26
14
  }
27
- class Route extends defineDynamicClass() {
28
- constructor() {
29
- super();
30
- this.routes = [];
31
- _Route_path.set(this, '');
32
- const allMethods = [...methods, router_2.METHOD_NAME_ALL_LOWERCASE];
33
- allMethods.map((method) => {
34
- this[method] = (args1, ...args) => {
35
- if (typeof args1 === 'string') {
36
- __classPrivateFieldSet(this, _Route_path, args1, "f");
37
- }
38
- else {
39
- this.add(method, __classPrivateFieldGet(this, _Route_path, "f"), args1);
40
- }
41
- args.map((handler) => {
42
- if (typeof handler !== 'string') {
43
- this.add(method, __classPrivateFieldGet(this, _Route_path, "f"), handler);
44
- }
45
- });
46
- return this;
47
- };
48
- });
49
- }
50
- add(method, path, handler) {
51
- const r = { path: path, method: method, handler: handler };
52
- this.routes.push(r);
53
- return this;
54
- }
55
- }
56
- exports.Route = Route;
57
- _Route_path = new WeakMap();
58
15
  class Hono extends defineDynamicClass() {
59
16
  constructor(init = {}) {
60
17
  super();
61
18
  this.routerClass = trie_router_1.TrieRouter;
62
19
  this.strict = true; // strict routing - default is true
63
- _Hono_router.set(this, void 0);
64
- _Hono_tempPath.set(this, void 0);
20
+ this.path = '/';
21
+ this.routes = [];
65
22
  this.notFoundHandler = (c) => {
66
23
  const message = '404 Not Found';
67
24
  return c.text(message, 404);
@@ -89,19 +46,17 @@ class Hono extends defineDynamicClass() {
89
46
  };
90
47
  });
91
48
  Object.assign(this, init);
92
- __classPrivateFieldSet(this, _Hono_router, new this.routerClass(), "f");
93
- __classPrivateFieldSet(this, _Hono_tempPath, null, "f");
49
+ this._router = new this.routerClass();
50
+ this._tempPath = null;
94
51
  }
95
- route(path, route) {
96
- const newHono = new Hono();
97
- __classPrivateFieldSet(newHono, _Hono_tempPath, path, "f");
98
- __classPrivateFieldSet(newHono, _Hono_router, __classPrivateFieldGet(this, _Hono_router, "f"), "f");
99
- if (route) {
100
- route.routes.map((r) => {
101
- newHono.addRoute(r.method, r.path, r.handler);
52
+ route(path, app) {
53
+ this._tempPath = path;
54
+ if (app) {
55
+ app.routes.map((r) => {
56
+ this.addRoute(r.method, r.path, r.handler);
102
57
  });
103
58
  }
104
- return newHono;
59
+ return this;
105
60
  }
106
61
  use(arg1, ...handlers) {
107
62
  if (typeof arg1 === 'string') {
@@ -125,22 +80,30 @@ class Hono extends defineDynamicClass() {
125
80
  }
126
81
  addRoute(method, path, handler) {
127
82
  method = method.toUpperCase();
128
- if (__classPrivateFieldGet(this, _Hono_tempPath, "f")) {
129
- path = (0, url_1.mergePath)(__classPrivateFieldGet(this, _Hono_tempPath, "f"), path);
83
+ if (this._tempPath) {
84
+ path = (0, url_1.mergePath)(this._tempPath, path);
130
85
  }
131
- __classPrivateFieldGet(this, _Hono_router, "f").add(method, path, handler);
86
+ this._router.add(method, path, handler);
87
+ const r = { path: path, method: method, handler: handler };
88
+ this.routes.push(r);
132
89
  }
133
90
  async matchRoute(method, path) {
134
- return __classPrivateFieldGet(this, _Hono_router, "f").match(method, path);
91
+ return this._router.match(method, path);
135
92
  }
136
93
  async dispatch(request, event, env) {
137
94
  const path = (0, url_1.getPathFromURL)(request.url, { strict: this.strict });
138
95
  const method = request.method;
139
96
  const result = await this.matchRoute(method, path);
140
- request.param = (key) => {
141
- if (result)
142
- return result.params[key];
143
- };
97
+ request.param = ((key) => {
98
+ if (result) {
99
+ if (key) {
100
+ return result.params[key];
101
+ }
102
+ else {
103
+ return result.params;
104
+ }
105
+ }
106
+ });
144
107
  const handlers = result ? result.handlers : [this.notFoundHandler];
145
108
  const c = new context_1.Context(request, { env: env, event: event, res: undefined });
146
109
  c.notFound = () => this.notFoundHandler(c);
@@ -175,4 +138,3 @@ class Hono extends defineDynamicClass() {
175
138
  }
176
139
  }
177
140
  exports.Hono = Hono;
178
- _Hono_router = new WeakMap(), _Hono_tempPath = new WeakMap();
package/dist/hono.test.js CHANGED
@@ -152,44 +152,71 @@ describe('Routing', () => {
152
152
  });
153
153
  });
154
154
  describe('param and query', () => {
155
- const app = new hono_1.Hono();
156
- app.get('/entry/:id', (c) => {
157
- const id = c.req.param('id');
158
- return c.text(`id is ${id}`);
159
- });
160
- app.get('/date/:date{[0-9]+}', (c) => {
161
- const date = c.req.param('date');
162
- return c.text(`date is ${date}`);
163
- });
164
- app.get('/search', (c) => {
165
- const name = c.req.query('name');
166
- return c.text(`name is ${name}`);
167
- });
168
- app.get('/add-header', (c) => {
169
- const bar = c.req.header('X-Foo');
170
- return c.text(`foo is ${bar}`);
171
- });
172
- it('param of /entry/:id is found', async () => {
173
- const res = await app.request('http://localhost/entry/123');
174
- expect(res.status).toBe(200);
175
- expect(await res.text()).toBe('id is 123');
176
- });
177
- it('param of /date/:date is found', async () => {
178
- const res = await app.request('http://localhost/date/0401');
179
- expect(res.status).toBe(200);
180
- expect(await res.text()).toBe('date is 0401');
181
- });
182
- it('query of /search?name=sam is found', async () => {
183
- const res = await app.request('http://localhost/search?name=sam');
184
- expect(res.status).toBe(200);
185
- expect(await res.text()).toBe('name is sam');
186
- });
187
- it('/add-header header - X-Foo is Bar', async () => {
188
- const req = new Request('http://localhost/add-header');
189
- req.headers.append('X-Foo', 'Bar');
190
- const res = await app.request(req);
191
- expect(res.status).toBe(200);
192
- expect(await res.text()).toBe('foo is Bar');
155
+ const apps = {};
156
+ apps['get by name'] = (() => {
157
+ const app = new hono_1.Hono();
158
+ app.get('/entry/:id', (c) => {
159
+ const id = c.req.param('id');
160
+ return c.text(`id is ${id}`);
161
+ });
162
+ app.get('/date/:date{[0-9]+}', (c) => {
163
+ const date = c.req.param('date');
164
+ return c.text(`date is ${date}`);
165
+ });
166
+ app.get('/search', (c) => {
167
+ const name = c.req.query('name');
168
+ return c.text(`name is ${name}`);
169
+ });
170
+ app.get('/add-header', (c) => {
171
+ const bar = c.req.header('X-Foo');
172
+ return c.text(`foo is ${bar}`);
173
+ });
174
+ return app;
175
+ })();
176
+ apps['get all as an object'] = (() => {
177
+ const app = new hono_1.Hono();
178
+ app.get('/entry/:id', (c) => {
179
+ const { id } = c.req.param();
180
+ return c.text(`id is ${id}`);
181
+ });
182
+ app.get('/date/:date{[0-9]+}', (c) => {
183
+ const { date } = c.req.param();
184
+ return c.text(`date is ${date}`);
185
+ });
186
+ app.get('/search', (c) => {
187
+ const { name } = c.req.query();
188
+ return c.text(`name is ${name}`);
189
+ });
190
+ app.get('/add-header', (c) => {
191
+ const { 'x-foo': bar } = c.req.header();
192
+ return c.text(`foo is ${bar}`);
193
+ });
194
+ return app;
195
+ })();
196
+ describe.each(Object.keys(apps))('%s', (name) => {
197
+ const app = apps[name];
198
+ it('param of /entry/:id is found', async () => {
199
+ const res = await app.request('http://localhost/entry/123');
200
+ expect(res.status).toBe(200);
201
+ expect(await res.text()).toBe('id is 123');
202
+ });
203
+ it('param of /date/:date is found', async () => {
204
+ const res = await app.request('http://localhost/date/0401');
205
+ expect(res.status).toBe(200);
206
+ expect(await res.text()).toBe('date is 0401');
207
+ });
208
+ it('query of /search?name=sam is found', async () => {
209
+ const res = await app.request('http://localhost/search?name=sam');
210
+ expect(res.status).toBe(200);
211
+ expect(await res.text()).toBe('name is sam');
212
+ });
213
+ it('/add-header header - X-Foo is Bar', async () => {
214
+ const req = new Request('http://localhost/add-header');
215
+ req.headers.append('X-Foo', 'Bar');
216
+ const res = await app.request(req);
217
+ expect(res.status).toBe(200);
218
+ expect(await res.text()).toBe('foo is Bar');
219
+ });
193
220
  });
194
221
  });
195
222
  describe('Middleware', () => {
@@ -451,34 +478,57 @@ describe('Request methods with custom middleware', () => {
451
478
  expect(res.headers.get('X-Header-2')).toBe('bar');
452
479
  });
453
480
  });
454
- describe('`Route` with app.route', () => {
455
- const app = new hono_1.Hono();
481
+ describe('Hono with `app.route`', () => {
456
482
  describe('Basic', () => {
457
- const route = new hono_1.Route();
458
- route.get('/post', (c) => c.text('GET /POST'));
459
- route.post('/post', (c) => c.text('POST /POST'));
460
- app.route('/v1', route);
461
- it('Should return 200 response - GET /v1/post', async () => {
462
- const res = await app.request('http://localhost/v1/post');
463
- expect(res.status).toBe(200);
464
- expect(await res.text()).toBe('GET /POST');
483
+ const app = new hono_1.Hono();
484
+ const api = new hono_1.Hono();
485
+ const middleware = new hono_1.Hono();
486
+ api.get('/posts', (c) => c.text('List'));
487
+ api.post('/posts', (c) => c.text('Create'));
488
+ api.get('/posts/:id', (c) => c.text(`GET ${c.req.param('id')}`));
489
+ api.use('*', async (c, next) => {
490
+ await next();
491
+ c.res.headers.append('x-custom-a', 'a');
465
492
  });
466
- it('Should return 200 response - POST /v1/post', async () => {
467
- const res = await app.request('http://localhost/v1/post', { method: 'POST' });
468
- expect(res.status).toBe(200);
469
- expect(await res.text()).toBe('POST /POST');
493
+ app.route('/api', api);
494
+ middleware.use('*', async (c, next) => {
495
+ await next();
496
+ c.res.headers.append('x-custom-b', 'b');
470
497
  });
471
- it('Should return 404 response - DELETE /v1/post', async () => {
472
- const res = await app.request('http://localhost/v1/post', { method: 'DELETE' });
498
+ app.route('/api', middleware);
499
+ it('Should return not found response', async () => {
500
+ const res = await app.request('http://localhost/');
473
501
  expect(res.status).toBe(404);
474
502
  });
475
- it('Should return 404 response - GET /post', async () => {
476
- const res = await app.request('http://localhost/post');
503
+ it('Should return not found response', async () => {
504
+ const res = await app.request('http://localhost/posts');
477
505
  expect(res.status).toBe(404);
478
506
  });
507
+ test('GET /api/posts', async () => {
508
+ const res = await app.request('http://localhost/api/posts');
509
+ expect(res.status).toBe(200);
510
+ expect(await res.text()).toBe('List');
511
+ });
512
+ test('Custom header by middleware', async () => {
513
+ const res = await app.request('http://localhost/api/posts');
514
+ expect(res.status).toBe(200);
515
+ expect(res.headers.get('x-custom-a')).toBe('a');
516
+ expect(res.headers.get('x-custom-b')).toBe('b');
517
+ });
518
+ test('POST /api/posts', async () => {
519
+ const res = await app.request('http://localhost/api/posts', { method: 'POST' });
520
+ expect(res.status).toBe(200);
521
+ expect(await res.text()).toBe('Create');
522
+ });
523
+ test('GET /api/posts/123', async () => {
524
+ const res = await app.request('http://localhost/api/posts/123');
525
+ expect(res.status).toBe(200);
526
+ expect(await res.text()).toBe('GET 123');
527
+ });
479
528
  });
480
529
  describe('Chaining', () => {
481
- const route = new hono_1.Route();
530
+ const app = new hono_1.Hono();
531
+ const route = new hono_1.Hono();
482
532
  route.get('/post', (c) => c.text('GET /POST v2')).post((c) => c.text('POST /POST v2'));
483
533
  app.route('/v2', route);
484
534
  it('Should return 200 response - GET /v2/post', async () => {
@@ -496,21 +546,35 @@ describe('`Route` with app.route', () => {
496
546
  expect(res.status).toBe(404);
497
547
  });
498
548
  });
499
- describe('Named parameter', () => {
500
- const route = new hono_1.Route();
501
- route.get('/post/:id', (c) => {
502
- const id = c.req.param('id');
503
- return c.text(`GET /post/${id} v3`);
549
+ describe('Nested', () => {
550
+ const app = new hono_1.Hono();
551
+ const api = new hono_1.Hono();
552
+ const book = new hono_1.Hono();
553
+ book.get('/', (c) => c.text('list books'));
554
+ book.get('/:id', (c) => c.text(`book ${c.req.param('id')}`));
555
+ api.get('/', (c) => c.text('this is API'));
556
+ api.route('/book', book);
557
+ app.get('/', (c) => c.text('root'));
558
+ app.route('/v2', api);
559
+ it('Should return 200 response - GET /', async () => {
560
+ const res = await app.request('http://localhost/');
561
+ expect(res.status).toBe(200);
562
+ expect(await res.text()).toBe('root');
504
563
  });
505
- app.route('/v3', route);
506
- it('Should return 200 response - GET /v3/post/1', async () => {
507
- const res = await app.request('http://localhost/v3/post/1');
564
+ it('Should return 200 response - GET /v2', async () => {
565
+ const res = await app.request('http://localhost/v2');
508
566
  expect(res.status).toBe(200);
509
- expect(await res.text()).toBe('GET /post/1 v3');
567
+ expect(await res.text()).toBe('this is API');
510
568
  });
511
- it('Should return 404 response - GET /v3/post', async () => {
512
- const res = await app.request('http://localhost/v3/post/abc/def');
513
- expect(res.status).toBe(404);
569
+ it('Should return 200 response - GET /v2/book', async () => {
570
+ const res = await app.request('http://localhost/v2/book');
571
+ expect(res.status).toBe(200);
572
+ expect(await res.text()).toBe('list books');
573
+ });
574
+ it('Should return 200 response - GET /v2/book/123', async () => {
575
+ const res = await app.request('http://localhost/v2/book/123');
576
+ expect(res.status).toBe(200);
577
+ expect(await res.text()).toBe('book 123');
514
578
  });
515
579
  });
516
580
  });
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { Hono, Route } from './hono';
1
+ export { Hono } from './hono';
2
2
  export type { Handler, Next } from './hono';
3
3
  export { Context } from './context';
4
4
  export type { Env } from './context';
package/dist/index.js CHANGED
@@ -1,8 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Context = exports.Route = exports.Hono = void 0;
3
+ exports.Context = exports.Hono = void 0;
4
4
  var hono_1 = require("./hono");
5
5
  Object.defineProperty(exports, "Hono", { enumerable: true, get: function () { return hono_1.Hono; } });
6
- Object.defineProperty(exports, "Route", { enumerable: true, get: function () { return hono_1.Route; } });
7
6
  var context_1 = require("./context");
8
7
  Object.defineProperty(exports, "Context", { enumerable: true, get: function () { return context_1.Context; } });
@@ -2,7 +2,10 @@ import type { Context } from '../../context';
2
2
  import type { Next } from '../../hono';
3
3
  declare global {
4
4
  interface Request {
5
- cookie: (name: string) => string;
5
+ cookie: {
6
+ (name: string): string;
7
+ (): Record<string, string>;
8
+ };
6
9
  }
7
10
  }
8
11
  declare module '../../context' {
@@ -3,12 +3,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.cookie = void 0;
4
4
  const cookie = () => {
5
5
  return async (c, next) => {
6
- c.req.cookie = (name) => {
6
+ c.req.cookie = ((name) => {
7
7
  const cookie = c.req.headers.get('Cookie');
8
8
  const obj = parse(cookie);
9
- const value = obj[name];
10
- return value;
11
- };
9
+ if (name) {
10
+ const value = obj[name];
11
+ return value;
12
+ }
13
+ else {
14
+ return obj;
15
+ }
16
+ });
12
17
  c.cookie = (name, value, opt) => {
13
18
  const cookie = serialize(name, value, opt);
14
19
  c.header('Set-Cookie', cookie);
@@ -3,52 +3,76 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const hono_1 = require("../../hono");
4
4
  const _1 = require(".");
5
5
  describe('Cookie Middleware', () => {
6
- const app = new hono_1.Hono();
7
- app.use('/cookie', (0, _1.cookie)());
8
- app.get('/cookie', (c) => {
9
- const yummyCookie = c.req.cookie('yummy_cookie');
10
- const tastyCookie = c.req.cookie('tasty_cookie');
11
- const res = new Response('Good cookie');
12
- res.headers.set('Yummy-Cookie', yummyCookie);
13
- res.headers.set('Tasty-Cookie', tastyCookie);
14
- return res;
15
- });
16
- it('Parse cookie on c.req.cookie', async () => {
17
- const req = new Request('http://localhost/cookie');
18
- const cookieString = 'yummy_cookie=choco; tasty_cookie = strawberry ';
19
- req.headers.set('Cookie', cookieString);
20
- const res = await app.request(req);
21
- expect(res.headers.get('Yummy-Cookie')).toBe('choco');
22
- expect(res.headers.get('Tasty-Cookie')).toBe('strawberry');
23
- });
24
- app.use('/set-cookie', (0, _1.cookie)());
25
- app.get('/set-cookie', (c) => {
26
- c.cookie('delicious_cookie', 'macha');
27
- return c.text('Give cookie');
28
- });
29
- it('Set cookie on c.cookie', async () => {
30
- const res = await app.request('http://localhost/set-cookie');
31
- expect(res.status).toBe(200);
32
- const header = res.headers.get('Set-Cookie');
33
- expect(header).toBe('delicious_cookie=macha');
34
- });
35
- app.use('/set-cookie-complex', (0, _1.cookie)());
36
- app.get('/set-cookie-complex', (c) => {
37
- c.cookie('great_cookie', 'banana', {
38
- path: '/',
39
- secure: true,
40
- domain: 'example.com',
41
- httpOnly: true,
42
- maxAge: 1000,
43
- expires: new Date(Date.UTC(2000, 11, 24, 10, 30, 59, 900)),
44
- sameSite: 'Strict',
6
+ describe('Parse cookie', () => {
7
+ const apps = {};
8
+ apps['get by name'] = (() => {
9
+ const app = new hono_1.Hono();
10
+ app.use('/cookie', (0, _1.cookie)());
11
+ app.get('/cookie', (c) => {
12
+ const yummyCookie = c.req.cookie('yummy_cookie');
13
+ const tastyCookie = c.req.cookie('tasty_cookie');
14
+ const res = new Response('Good cookie');
15
+ res.headers.set('Yummy-Cookie', yummyCookie);
16
+ res.headers.set('Tasty-Cookie', tastyCookie);
17
+ return res;
18
+ });
19
+ return app;
20
+ })();
21
+ apps['get all as an object'] = (() => {
22
+ const app = new hono_1.Hono();
23
+ app.use('/cookie', (0, _1.cookie)());
24
+ app.get('/cookie', (c) => {
25
+ const { yummy_cookie: yummyCookie, tasty_cookie: tastyCookie } = c.req.cookie();
26
+ const res = new Response('Good cookie');
27
+ res.headers.set('Yummy-Cookie', yummyCookie);
28
+ res.headers.set('Tasty-Cookie', tastyCookie);
29
+ return res;
30
+ });
31
+ return app;
32
+ })();
33
+ describe.each(Object.keys(apps))('%s', (name) => {
34
+ const app = apps[name];
35
+ it('Parse cookie on c.req.cookie', async () => {
36
+ const req = new Request('http://localhost/cookie');
37
+ const cookieString = 'yummy_cookie=choco; tasty_cookie = strawberry ';
38
+ req.headers.set('Cookie', cookieString);
39
+ const res = await app.request(req);
40
+ expect(res.headers.get('Yummy-Cookie')).toBe('choco');
41
+ expect(res.headers.get('Tasty-Cookie')).toBe('strawberry');
42
+ });
45
43
  });
46
- return c.text('Give cookie');
47
44
  });
48
- it('Complex pattern', async () => {
49
- const res = await app.request('http://localhost/set-cookie-complex');
50
- expect(res.status).toBe(200);
51
- const header = res.headers.get('Set-Cookie');
52
- expect(header).toBe('great_cookie=banana; Max-Age=1000; Domain=example.com; Path=/; Expires=Sun, 24 Dec 2000 10:30:59 GMT; HttpOnly; Secure; SameSite=Strict');
45
+ describe('Set cookie', () => {
46
+ const app = new hono_1.Hono();
47
+ app.use('/set-cookie', (0, _1.cookie)());
48
+ app.get('/set-cookie', (c) => {
49
+ c.cookie('delicious_cookie', 'macha');
50
+ return c.text('Give cookie');
51
+ });
52
+ it('Set cookie on c.cookie', async () => {
53
+ const res = await app.request('http://localhost/set-cookie');
54
+ expect(res.status).toBe(200);
55
+ const header = res.headers.get('Set-Cookie');
56
+ expect(header).toBe('delicious_cookie=macha');
57
+ });
58
+ app.use('/set-cookie-complex', (0, _1.cookie)());
59
+ app.get('/set-cookie-complex', (c) => {
60
+ c.cookie('great_cookie', 'banana', {
61
+ path: '/',
62
+ secure: true,
63
+ domain: 'example.com',
64
+ httpOnly: true,
65
+ maxAge: 1000,
66
+ expires: new Date(Date.UTC(2000, 11, 24, 10, 30, 59, 900)),
67
+ sameSite: 'Strict',
68
+ });
69
+ return c.text('Give cookie');
70
+ });
71
+ it('Complex pattern', async () => {
72
+ const res = await app.request('http://localhost/set-cookie-complex');
73
+ expect(res.status).toBe(200);
74
+ const header = res.headers.get('Set-Cookie');
75
+ expect(header).toBe('great_cookie=banana; Max-Age=1000; Domain=example.com; Path=/; Expires=Sun, 24 Dec 2000 10:30:59 GMT; HttpOnly; Secure; SameSite=Strict');
76
+ });
53
77
  });
54
78
  });
package/dist/utils/url.js CHANGED
@@ -75,6 +75,10 @@ const mergePath = (...paths) => {
75
75
  else if (path !== '/') {
76
76
  p = `${p}${path}`;
77
77
  }
78
+ /* ['/', '/'] => `/` */
79
+ if (path === '/' && p === '') {
80
+ p = '/';
81
+ }
78
82
  }
79
83
  return p;
80
84
  };
@@ -83,5 +83,14 @@ describe('url', () => {
83
83
  expect((0, url_1.mergePath)('book', 'hey')).toBe('/book/hey');
84
84
  expect((0, url_1.mergePath)('book', 'hey/')).toBe('/book/hey/');
85
85
  });
86
+ it('Should be `/book`', () => {
87
+ expect((0, url_1.mergePath)('/', 'book')).toBe('/book');
88
+ });
89
+ it('Should be `/book`', () => {
90
+ expect((0, url_1.mergePath)('/', '/book')).toBe('/book');
91
+ });
92
+ it('Should be `/`', () => {
93
+ expect((0, url_1.mergePath)('/', '/')).toBe('/');
94
+ });
86
95
  });
87
96
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hono",
3
- "version": "1.2.2",
3
+ "version": "1.3.0",
4
4
  "description": "Ultrafast web framework for Cloudflare Workers.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",