h3 0.3.2 → 0.3.6

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
@@ -1,24 +1,25 @@
1
- [![d](https://img.shields.io/npm/dm/h3.svg?style=flat-square)](https://npmjs.com/package/h3)
2
- [![v](https://img.shields.io/npm/v/h3/latest.svg?style=flat-square)](https://npmjs.com/package/h3)
3
- [![b](https://img.shields.io/bundlephobia/min/h3/latest.svg?style=flat-square)](https://bundlephobia.com/result?p=h3)
4
- [![a](https://img.shields.io/github/workflow/status/unjs/h3/ci/main?style=flat-square)](https://github.com/unjs/h3/actions)
5
- [![c](https://img.shields.io/codecov/c/gh/unjs/h3/main?style=flat-square)](https://codecov.io/gh/unjs/h3)
1
+ [![npm downloads](https://img.shields.io/npm/dm/h3.svg?style=flat-square)](https://npmjs.com/package/h3)
2
+ [![version](https://img.shields.io/npm/v/h3/latest.svg?style=flat-square)](https://npmjs.com/package/h3)
3
+ [![bundlephobia](https://img.shields.io/bundlephobia/min/h3/latest.svg?style=flat-square)](https://bundlephobia.com/result?p=h3)
4
+ [![build status](https://img.shields.io/github/workflow/status/unjs/h3/ci/main?style=flat-square)](https://github.com/unjs/h3/actions)
5
+ [![coverage](https://img.shields.io/codecov/c/gh/unjs/h3/main?style=flat-square)](https://codecov.io/gh/unjs/h3)
6
+ [![jsDocs.io](https://img.shields.io/badge/jsDocs.io-reference-blue?style=flat-square)](https://www.jsdocs.io/package/h3)
6
7
 
7
8
  > H3 is a minimal h(ttp) framework built for high performance and portability
8
9
 
9
10
  <!-- ![h3 - Tiny JavaScript Server](.github/banner.svg) -->
10
11
 
11
- **Features**
12
+ ## Features
12
13
 
13
- ✔️ **Portable:** Works perfectly in Serverless, Workers, and Node.js
14
+ ✔️ &nbsp;**Portable:** Works perfectly in Serverless, Workers, and Node.js
14
15
 
15
- ✔️ **Compatible:** Support connect/express middleware
16
+ ✔️ &nbsp;**Compatible:** Support connect/express middleware
16
17
 
17
- ✔️ **Minimal:** Small, tree-shakable and zero-dependency
18
+ ✔️ &nbsp;**Minimal:** Small, tree-shakable and zero-dependency
18
19
 
19
- ✔️ **Modern:** Native promise support
20
+ ✔️ &nbsp;**Modern:** Native promise support
20
21
 
21
- ✔️**Extendable:** Ships with a set of composable utilities but can be extended
22
+ ✔️ &nbsp;**Extendable:** Ships with a set of composable utilities but can be extended
22
23
 
23
24
  ## Install
24
25
 
@@ -42,7 +43,19 @@ app.use('/', () => 'Hello world!')
42
43
  createServer(app).listen(process.env.PORT || 3000)
43
44
  ```
44
45
 
45
- **Tip:** you may try [listhen](https://github.com/unjs/listhen) for a more elegant and advanced listener.
46
+ <details>
47
+ <summary>Example using <a href="https://github.com/unjs/listhen">listhen</a> for an elegant listener.</summary>
48
+
49
+ ```ts
50
+ import { createApp } from 'h3'
51
+ import { listen } from 'listhen'
52
+
53
+ const app = createApp()
54
+ app.use('/', () => 'Hello world!')
55
+
56
+ listen(app)
57
+ ```
58
+ </details>
46
59
 
47
60
  ## Examples
48
61
 
@@ -85,6 +98,13 @@ Instead of adding helpers to `req` and `res`, h3 exposes them as composable util
85
98
  - `appendHeader(res, name, value)`
86
99
  - `createError({ statusCode, statusMessage, data? }`
87
100
  - `sendError(res, error, debug?)`
101
+ - `defineHandle(handle)`
102
+ - `defineMiddleware(middlware)`
103
+ - `useMethod(req, default?)`
104
+ - `isMethod(req, expected, allowHead?)`
105
+ - `assertMethod(req, expected, allowHead?)`
106
+
107
+ 👉 You can learn more about usage in [JSDocs Documentation](https://www.jsdocs.io/package/h3#package-functions).
88
108
 
89
109
  ## How it works?
90
110
 
package/dist/index.cjs CHANGED
@@ -83,8 +83,8 @@ function parseURL(input = "", defaultProto) {
83
83
  return defaultProto ? parseURL(defaultProto + input) : parsePath(input);
84
84
  }
85
85
  const [protocol = "", auth, hostAndPath] = (input.match(/([^:/]+:)?\/\/([^/@]+@)?(.*)/) || []).splice(1);
86
- const [host = "", path = ""] = (hostAndPath.match(/([^/]*)(.*)?/) || []).splice(1);
87
- const {pathname, search, hash} = parsePath(path);
86
+ const [host = "", path = ""] = (hostAndPath.match(/([^/?]*)(.*)?/) || []).splice(1);
87
+ const { pathname, search, hash } = parsePath(path);
88
88
  return {
89
89
  protocol,
90
90
  auth: auth ? auth.substr(0, auth.length - 1) : "",
@@ -103,6 +103,8 @@ function parsePath(input = "") {
103
103
  };
104
104
  }
105
105
 
106
+ const defineHandle = (handler) => handler;
107
+ const defineMiddleware = (middleware) => middleware;
106
108
  function promisifyHandle(handle) {
107
109
  return function(req, res) {
108
110
  return callHandle(handle, req, res);
@@ -193,26 +195,61 @@ function destr(val) {
193
195
  }
194
196
  }
195
197
 
198
+ function useQuery(req) {
199
+ return getQuery(req.url || "");
200
+ }
201
+ function useMethod(req, defaultMethod = "GET") {
202
+ return (req.method || defaultMethod).toUpperCase();
203
+ }
204
+ function isMethod(req, expected, allowHead) {
205
+ const method = useMethod(req);
206
+ if (allowHead && method === "HEAD") {
207
+ return true;
208
+ }
209
+ if (typeof expected === "string") {
210
+ if (method === expected) {
211
+ return true;
212
+ }
213
+ } else if (expected.includes(method)) {
214
+ return true;
215
+ }
216
+ return false;
217
+ }
218
+ function assertMethod(req, expected, allowHead) {
219
+ if (!isMethod(req, expected, allowHead)) {
220
+ throw createError({
221
+ statusCode: 405,
222
+ statusMessage: "HTTP method is not allowed."
223
+ });
224
+ }
225
+ }
226
+
196
227
  const RawBodySymbol = Symbol("h3RawBody");
197
228
  const ParsedBodySymbol = Symbol("h3RawBody");
229
+ const PayloadMethods = ["PATCH", "POST", "PUT"];
198
230
  function useRawBody(req, encoding = "utf-8") {
199
- if (req[RawBodySymbol]) {
200
- return Promise.resolve(encoding ? req[RawBodySymbol].toString(encoding) : req[RawBodySymbol]);
231
+ assertMethod(req, PayloadMethods);
232
+ if (RawBodySymbol in req) {
233
+ const promise2 = Promise.resolve(req[RawBodySymbol]);
234
+ return encoding ? promise2.then((buff) => buff.toString(encoding)) : promise2;
201
235
  }
202
- return new Promise((resolve, reject) => {
236
+ if ("_body" in req) {
237
+ return Promise.resolve(req._body);
238
+ }
239
+ const promise = req[RawBodySymbol] = new Promise((resolve, reject) => {
203
240
  const bodyData = [];
204
241
  req.on("error", (err) => {
205
242
  reject(err);
206
243
  }).on("data", (chunk) => {
207
244
  bodyData.push(chunk);
208
245
  }).on("end", () => {
209
- req[RawBodySymbol] = Buffer.concat(bodyData);
210
- resolve(encoding ? req[RawBodySymbol].toString(encoding) : req[RawBodySymbol]);
246
+ resolve(Buffer.concat(bodyData));
211
247
  });
212
248
  });
249
+ return encoding ? promise.then((buff) => buff.toString(encoding)) : promise;
213
250
  }
214
251
  async function useBody(req) {
215
- if (req[ParsedBodySymbol]) {
252
+ if (ParsedBodySymbol in req) {
216
253
  return req[ParsedBodySymbol];
217
254
  }
218
255
  const body = await useRawBody(req);
@@ -346,11 +383,7 @@ function serialize(name, val, options) {
346
383
 
347
384
  if (null != opt.maxAge) {
348
385
  var maxAge = opt.maxAge - 0;
349
-
350
- if (isNaN(maxAge) || !isFinite(maxAge)) {
351
- throw new TypeError('option maxAge is invalid')
352
- }
353
-
386
+ if (isNaN(maxAge)) throw new Error('maxAge should be a Number');
354
387
  str += '; Max-Age=' + Math.floor(maxAge);
355
388
  }
356
389
 
@@ -472,10 +505,6 @@ function setCookie(res, name, value, serializeOptions) {
472
505
  appendHeader(res, "Set-Cookie", cookieStr);
473
506
  }
474
507
 
475
- function useQuery(req) {
476
- return getQuery(req.url || "");
477
- }
478
-
479
508
  class H3Error extends Error {
480
509
  constructor() {
481
510
  super(...arguments);
@@ -609,11 +638,15 @@ function normalizeLayer(layer) {
609
638
  exports.H3Error = H3Error;
610
639
  exports.MIMES = MIMES;
611
640
  exports.appendHeader = appendHeader;
641
+ exports.assertMethod = assertMethod;
612
642
  exports.callHandle = callHandle;
613
643
  exports.createApp = createApp;
614
644
  exports.createError = createError;
615
645
  exports.createHandle = createHandle;
616
646
  exports.defaultContentType = defaultContentType;
647
+ exports.defineHandle = defineHandle;
648
+ exports.defineMiddleware = defineMiddleware;
649
+ exports.isMethod = isMethod;
617
650
  exports.lazyHandle = lazyHandle;
618
651
  exports.promisifyHandle = promisifyHandle;
619
652
  exports.send = send;
@@ -625,5 +658,6 @@ exports.useBase = useBase;
625
658
  exports.useBody = useBody;
626
659
  exports.useCookie = useCookie;
627
660
  exports.useCookies = useCookies;
661
+ exports.useMethod = useMethod;
628
662
  exports.useQuery = useQuery;
629
663
  exports.useRawBody = useRawBody;
package/dist/index.d.ts CHANGED
@@ -7,6 +7,8 @@ declare type Handle<T = any> = (req: IncomingMessage, res: ServerResponse) => T;
7
7
  declare type PHandle = Handle<Promise<any>>;
8
8
  declare type Middleware = (req: IncomingMessage, res: ServerResponse, next: (err?: Error) => any) => any;
9
9
  declare type LazyHandle = () => Handle | Promise<Handle>;
10
+ declare const defineHandle: <T>(handler: Handle<T>) => Handle<T>;
11
+ declare const defineMiddleware: (middleware: Middleware) => Middleware;
10
12
  declare function promisifyHandle(handle: Handle | Middleware): PHandle;
11
13
  declare function callHandle(handle: Middleware, req: IncomingMessage, res: ServerResponse): Promise<unknown>;
12
14
  declare function lazyHandle(handle: LazyHandle, promisify?: boolean): PHandle;
@@ -82,24 +84,32 @@ declare function createError(input: Partial<H3Error>): H3Error;
82
84
  */
83
85
  declare function sendError(res: ServerResponse, error: Error | H3Error, debug?: boolean): void;
84
86
 
87
+ declare const RawBodySymbol: unique symbol;
88
+ interface _IncomingMessage extends IncomingMessage {
89
+ [RawBodySymbol]?: Promise<Buffer>;
90
+ ParsedBodySymbol?: any;
91
+ _body?: any;
92
+ }
85
93
  /**
86
94
  * Reads body of the request and returns encoded raw string (default) or `Buffer` if encoding if falsy.
87
- * @param req {IncomingMessage} An IncomingMessage object is created by
88
- * <a href="https://nodejs.org/api/http.html#http_class_http_server">http.Server</a>
95
+ * @param req {IncomingMessage} An IncomingMessage object is created by [http.Server](https://nodejs.org/api/http.html#http_class_http_server)
89
96
  * @param encoding {Encoding} encoding="utf-8" - The character encoding to use.
90
97
  *
91
98
  * @return {String|Buffer} Encoded raw string or raw Buffer of the body
92
99
  */
93
- declare function useRawBody(req: IncomingMessage, encoding?: Encoding): Encoding extends false ? Buffer : Promise<string>;
100
+ declare function useRawBody(req: _IncomingMessage, encoding?: Encoding): Encoding extends false ? Buffer : Promise<string>;
94
101
  /**
95
- * Reads request body and try to safely parse using {@link https://github.com/unjs/destr destr}
96
- * @param req {IncomingMessage} An IncomingMessage object is created by
97
- * <a href="https://nodejs.org/api/http.html#http_class_http_server">http.Server</a>
102
+ * Reads request body and try to safely parse using [destr](https://github.com/unjs/destr)
103
+ * @param req {IncomingMessage} An IncomingMessage object created by [http.Server](https://nodejs.org/api/http.html#http_class_http_server)
98
104
  * @param encoding {Encoding} encoding="utf-8" - The character encoding to use.
99
105
  *
100
- * @return {*} The Object, Array, string, number, boolean, or null value corresponding to the request JSON body
106
+ * @return {*} The `Object`, `Array`, `String`, `Number`, `Boolean`, or `null` value corresponding to the request JSON body
107
+ *
108
+ * ```ts
109
+ * const body = await useBody(req)
110
+ * ```
101
111
  */
102
- declare function useBody<T = any>(req: IncomingMessage): Promise<T>;
112
+ declare function useBody<T = any>(req: _IncomingMessage): Promise<T>;
103
113
 
104
114
  declare const MIMES: {
105
115
  html: string;
@@ -192,15 +202,46 @@ interface CookieSerializeOptions {
192
202
  secure?: boolean;
193
203
  }
194
204
 
205
+ /**
206
+ * Parse the request to get HTTP Cookie header string and returning an object of all cookie name-value pairs.
207
+ * @param req {IncomingMessage} An IncomingMessage object created by [http.Server](https://nodejs.org/api/http.html#http_class_http_server)
208
+ * @returns Object of cookie name-value pairs
209
+ * ```ts
210
+ * const cookies = useCookies(req)
211
+ * ```
212
+ */
195
213
  declare function useCookies(req: IncomingMessage): Record<string, string>;
214
+ /**
215
+ * Get a cookie value by name.
216
+ * @param req {IncomingMessage} An IncomingMessage object created by [http.Server](https://nodejs.org/api/http.html#http_class_http_server)
217
+ * @param name Name of the cookie to get
218
+ * @returns {*} Value of the cookie (String or undefined)
219
+ * ```ts
220
+ * const authorization = useCookie(request, 'Authorization')
221
+ * ```
222
+ */
196
223
  declare function useCookie(req: IncomingMessage, name: string): string | undefined;
197
- declare function setCookie(res: ServerResponse, name: string, value: string, serializeOptions: CookieSerializeOptions): void;
224
+ /**
225
+ * Set a cookie value by name.
226
+ * @param res {ServerResponse} A ServerResponse object created by [http.Server](https://nodejs.org/api/http.html#http_class_http_server)
227
+ * @param name Name of the cookie to set
228
+ * @param value Value of the cookie to set
229
+ * @param serializeOptions {CookieSerializeOptions} Options for serializing the cookie
230
+ * ```ts
231
+ * setCookie(res, 'Authorization', '1234567')
232
+ * ```
233
+ */
234
+ declare function setCookie(res: ServerResponse, name: string, value: string, serializeOptions?: CookieSerializeOptions): void;
198
235
 
199
236
  declare function useQuery(req: IncomingMessage): ufo.QueryObject;
237
+ declare type HTTPMethod = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'CONNECT' | 'OPTIONS' | 'TRACE';
238
+ declare function useMethod(req: IncomingMessage, defaultMethod?: HTTPMethod): HTTPMethod;
239
+ declare function isMethod(req: IncomingMessage, expected: HTTPMethod | HTTPMethod[], allowHead?: boolean): boolean;
240
+ declare function assertMethod(req: IncomingMessage, expected: HTTPMethod | HTTPMethod[], allowHead?: boolean): void;
200
241
 
201
242
  declare function send(res: ServerResponse, data: any, type?: string): Promise<unknown>;
202
243
  declare function defaultContentType(res: ServerResponse, type?: string): void;
203
244
  declare function sendRedirect(res: ServerResponse, location: string, code?: number): Promise<unknown>;
204
245
  declare function appendHeader(res: ServerResponse, name: string, value: string): void;
205
246
 
206
- export { App, AppOptions, AppUse, H3Error, Handle, InputLayer, InputStack, Layer, LazyHandle, MIMES, Matcher, Middleware, PHandle, Stack, appendHeader, callHandle, createApp, createError, createHandle, defaultContentType, lazyHandle, promisifyHandle, send, sendError, sendRedirect, setCookie, use, useBase, useBody, useCookie, useCookies, useQuery, useRawBody };
247
+ export { App, AppOptions, AppUse, H3Error, HTTPMethod, Handle, InputLayer, InputStack, Layer, LazyHandle, MIMES, Matcher, Middleware, PHandle, Stack, appendHeader, assertMethod, callHandle, createApp, createError, createHandle, defaultContentType, defineHandle, defineMiddleware, isMethod, lazyHandle, promisifyHandle, send, sendError, sendRedirect, setCookie, use, useBase, useBody, useCookie, useCookies, useMethod, useQuery, useRawBody };
package/dist/index.mjs CHANGED
@@ -79,8 +79,8 @@ function parseURL(input = "", defaultProto) {
79
79
  return defaultProto ? parseURL(defaultProto + input) : parsePath(input);
80
80
  }
81
81
  const [protocol = "", auth, hostAndPath] = (input.match(/([^:/]+:)?\/\/([^/@]+@)?(.*)/) || []).splice(1);
82
- const [host = "", path = ""] = (hostAndPath.match(/([^/]*)(.*)?/) || []).splice(1);
83
- const {pathname, search, hash} = parsePath(path);
82
+ const [host = "", path = ""] = (hostAndPath.match(/([^/?]*)(.*)?/) || []).splice(1);
83
+ const { pathname, search, hash } = parsePath(path);
84
84
  return {
85
85
  protocol,
86
86
  auth: auth ? auth.substr(0, auth.length - 1) : "",
@@ -99,6 +99,8 @@ function parsePath(input = "") {
99
99
  };
100
100
  }
101
101
 
102
+ const defineHandle = (handler) => handler;
103
+ const defineMiddleware = (middleware) => middleware;
102
104
  function promisifyHandle(handle) {
103
105
  return function(req, res) {
104
106
  return callHandle(handle, req, res);
@@ -189,26 +191,61 @@ function destr(val) {
189
191
  }
190
192
  }
191
193
 
194
+ function useQuery(req) {
195
+ return getQuery(req.url || "");
196
+ }
197
+ function useMethod(req, defaultMethod = "GET") {
198
+ return (req.method || defaultMethod).toUpperCase();
199
+ }
200
+ function isMethod(req, expected, allowHead) {
201
+ const method = useMethod(req);
202
+ if (allowHead && method === "HEAD") {
203
+ return true;
204
+ }
205
+ if (typeof expected === "string") {
206
+ if (method === expected) {
207
+ return true;
208
+ }
209
+ } else if (expected.includes(method)) {
210
+ return true;
211
+ }
212
+ return false;
213
+ }
214
+ function assertMethod(req, expected, allowHead) {
215
+ if (!isMethod(req, expected, allowHead)) {
216
+ throw createError({
217
+ statusCode: 405,
218
+ statusMessage: "HTTP method is not allowed."
219
+ });
220
+ }
221
+ }
222
+
192
223
  const RawBodySymbol = Symbol("h3RawBody");
193
224
  const ParsedBodySymbol = Symbol("h3RawBody");
225
+ const PayloadMethods = ["PATCH", "POST", "PUT"];
194
226
  function useRawBody(req, encoding = "utf-8") {
195
- if (req[RawBodySymbol]) {
196
- return Promise.resolve(encoding ? req[RawBodySymbol].toString(encoding) : req[RawBodySymbol]);
227
+ assertMethod(req, PayloadMethods);
228
+ if (RawBodySymbol in req) {
229
+ const promise2 = Promise.resolve(req[RawBodySymbol]);
230
+ return encoding ? promise2.then((buff) => buff.toString(encoding)) : promise2;
197
231
  }
198
- return new Promise((resolve, reject) => {
232
+ if ("_body" in req) {
233
+ return Promise.resolve(req._body);
234
+ }
235
+ const promise = req[RawBodySymbol] = new Promise((resolve, reject) => {
199
236
  const bodyData = [];
200
237
  req.on("error", (err) => {
201
238
  reject(err);
202
239
  }).on("data", (chunk) => {
203
240
  bodyData.push(chunk);
204
241
  }).on("end", () => {
205
- req[RawBodySymbol] = Buffer.concat(bodyData);
206
- resolve(encoding ? req[RawBodySymbol].toString(encoding) : req[RawBodySymbol]);
242
+ resolve(Buffer.concat(bodyData));
207
243
  });
208
244
  });
245
+ return encoding ? promise.then((buff) => buff.toString(encoding)) : promise;
209
246
  }
210
247
  async function useBody(req) {
211
- if (req[ParsedBodySymbol]) {
248
+ if (ParsedBodySymbol in req) {
212
249
  return req[ParsedBodySymbol];
213
250
  }
214
251
  const body = await useRawBody(req);
@@ -342,11 +379,7 @@ function serialize(name, val, options) {
342
379
 
343
380
  if (null != opt.maxAge) {
344
381
  var maxAge = opt.maxAge - 0;
345
-
346
- if (isNaN(maxAge) || !isFinite(maxAge)) {
347
- throw new TypeError('option maxAge is invalid')
348
- }
349
-
382
+ if (isNaN(maxAge)) throw new Error('maxAge should be a Number');
350
383
  str += '; Max-Age=' + Math.floor(maxAge);
351
384
  }
352
385
 
@@ -468,10 +501,6 @@ function setCookie(res, name, value, serializeOptions) {
468
501
  appendHeader(res, "Set-Cookie", cookieStr);
469
502
  }
470
503
 
471
- function useQuery(req) {
472
- return getQuery(req.url || "");
473
- }
474
-
475
504
  class H3Error extends Error {
476
505
  constructor() {
477
506
  super(...arguments);
@@ -602,4 +631,4 @@ function normalizeLayer(layer) {
602
631
  };
603
632
  }
604
633
 
605
- export { H3Error, MIMES, appendHeader, callHandle, createApp, createError, createHandle, defaultContentType, lazyHandle, promisifyHandle, send, sendError, sendRedirect, setCookie, use, useBase, useBody, useCookie, useCookies, useQuery, useRawBody };
634
+ export { H3Error, MIMES, appendHeader, assertMethod, callHandle, createApp, createError, createHandle, defaultContentType, defineHandle, defineMiddleware, isMethod, lazyHandle, promisifyHandle, send, sendError, sendRedirect, setCookie, use, useBase, useBody, useCookie, useCookies, useMethod, useQuery, useRawBody };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "h3",
3
- "version": "0.3.2",
3
+ "version": "0.3.6",
4
4
  "description": "Tiny JavaScript Server",
5
5
  "repository": "unjs/h3",
6
6
  "license": "MIT",
@@ -21,8 +21,8 @@
21
21
  "build": "siroc build",
22
22
  "dev": "jiti test/playground",
23
23
  "lint": "eslint --ext ts .",
24
- "release": "yarn test && yarn build && standard-version && npm publish && git push --follow-tags",
25
24
  "profile": "0x -o -D .profile -P 'autocannon -c 100 -p 10 -d 40 http://localhost:$PORT' ./hello.js",
25
+ "release": "yarn test && yarn build && standard-version && npm publish && git push --follow-tags",
26
26
  "test": "yarn lint && jest"
27
27
  },
28
28
  "devDependencies": {
@@ -35,11 +35,11 @@
35
35
  "@types/supertest": "latest",
36
36
  "autocannon": "latest",
37
37
  "connect": "latest",
38
- "cookie": "latest",
38
+ "cookie-es": "latest",
39
39
  "destr": "latest",
40
40
  "eslint": "latest",
41
41
  "express": "latest",
42
- "get-port": "latest",
42
+ "get-port": "^5.0.0",
43
43
  "jest": "latest",
44
44
  "jiti": "latest",
45
45
  "listhen": "latest",