h3 0.3.8 → 0.4.1

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
@@ -21,6 +21,8 @@
21
21
 
22
22
  ✔️  **Extendable:** Ships with a set of composable utilities but can be extended
23
23
 
24
+ ✔️  **Router:** Super fast route matching using [unjs/radix3](https://github.com/unjs/radix3)
25
+
24
26
  ## Install
25
27
 
26
28
  ```bash
@@ -57,7 +59,29 @@ listen(app)
57
59
  ```
58
60
  </details>
59
61
 
60
- ## Examples
62
+ ## Router
63
+
64
+ The `app` instance created by `h3` uses a middleware stack (see [how it works](#how-it-works)) with the ability to match route prefix and apply matched middleware.
65
+
66
+ To opt-in using a more advanced and convenient routing system, we can create a router instance and register it to app instance.
67
+
68
+ ```ts
69
+ import { createApp, createRouter } from 'h3'
70
+
71
+ const app = createApp()
72
+
73
+ const router = createRouter()
74
+ .get('/', () => 'Hello World!')
75
+ .get('/hello/:name', req => `Hello ${req.params.name}!`)
76
+
77
+ app.use(router)
78
+ ```
79
+
80
+ **Tip:** We can register same route more than once with different methods.
81
+
82
+ Routes are internally stored in a [Radix Tree](https://en.wikipedia.org/wiki/Radix_tree) and matched using [unjs/radix3](https://github.com/unjs/radix3).
83
+
84
+ ## More usage examples
61
85
 
62
86
  ```js
63
87
  // Handle can directly return object or Promise<object> for JSON response
@@ -92,11 +116,12 @@ Instead of adding helpers to `req` and `res`, h3 exposes them as composable util
92
116
  - `useCookies(req)`
93
117
  - `useCookie(req, name)`
94
118
  - `setCookie(res, name, value, opts?)`
119
+ - `deleteCookie(res, name, opts?)`
95
120
  - `useQuery(req)`
96
121
  - `send(res, data, type?)`
97
122
  - `sendRedirect(res, location, code=302)`
98
123
  - `appendHeader(res, name, value)`
99
- - `createError({ statusCode, statusMessage, data? }`
124
+ - `createError({ statusCode, statusMessage, data? })`
100
125
  - `sendError(res, error, debug?)`
101
126
  - `defineHandle(handle)`
102
127
  - `defineMiddleware(middlware)`
package/dist/index.cjs CHANGED
@@ -2,106 +2,14 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- const PLUS_RE = /\+/g;
6
- function decode$1(text = "") {
7
- try {
8
- return decodeURIComponent("" + text);
9
- } catch (_err) {
10
- return "" + text;
11
- }
12
- }
13
- function decodeQueryValue(text) {
14
- return decode$1(text.replace(PLUS_RE, " "));
15
- }
5
+ const ufo = require('ufo');
6
+ const radix3 = require('radix3');
7
+ const destr = require('destr');
8
+ const cookieEs = require('cookie-es');
16
9
 
17
- function parseQuery(paramsStr = "") {
18
- const obj = {};
19
- if (paramsStr[0] === "?") {
20
- paramsStr = paramsStr.substr(1);
21
- }
22
- for (const param of paramsStr.split("&")) {
23
- const s = param.match(/([^=]+)=?(.*)/) || [];
24
- if (s.length < 2) {
25
- continue;
26
- }
27
- const key = decode$1(s[1]);
28
- if (key === "__proto__" || key === "constructor") {
29
- continue;
30
- }
31
- const value = decodeQueryValue(s[2] || "");
32
- if (obj[key]) {
33
- if (Array.isArray(obj[key])) {
34
- obj[key].push(value);
35
- } else {
36
- obj[key] = [obj[key], value];
37
- }
38
- } else {
39
- obj[key] = value;
40
- }
41
- }
42
- return obj;
43
- }
44
- function hasProtocol(inputStr, acceptProtocolRelative = false) {
45
- return /^\w+:\/\/.+/.test(inputStr) || acceptProtocolRelative && /^\/\/[^/]+/.test(inputStr);
46
- }
47
- const TRAILING_SLASH_RE = /\/$|\/\?/;
48
- function hasTrailingSlash(input = "", queryParams = false) {
49
- if (!queryParams) {
50
- return input.endsWith("/");
51
- }
52
- return TRAILING_SLASH_RE.test(input);
53
- }
54
- function withoutTrailingSlash(input = "", queryParams = false) {
55
- if (!queryParams) {
56
- return (hasTrailingSlash(input) ? input.slice(0, -1) : input) || "/";
57
- }
58
- if (!hasTrailingSlash(input, true)) {
59
- return input || "/";
60
- }
61
- const [s0, ...s] = input.split("?");
62
- return (s0.slice(0, -1) || "/") + (s.length ? `?${s.join("?")}` : "");
63
- }
64
- function withoutBase(input, base) {
65
- if (isEmptyURL(base)) {
66
- return input;
67
- }
68
- const _base = withoutTrailingSlash(base);
69
- if (input.startsWith(_base)) {
70
- return input.substr(_base.length) || "/";
71
- }
72
- return input;
73
- }
74
- function getQuery(input) {
75
- return parseQuery(parseURL(input).search);
76
- }
77
- function isEmptyURL(url) {
78
- return !url || url === "/";
79
- }
10
+ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e["default"] : e; }
80
11
 
81
- function parseURL(input = "", defaultProto) {
82
- if (!hasProtocol(input, true)) {
83
- return defaultProto ? parseURL(defaultProto + input) : parsePath(input);
84
- }
85
- const [protocol = "", auth, hostAndPath] = (input.match(/([^:/]+:)?\/\/([^/@]+@)?(.*)/) || []).splice(1);
86
- const [host = "", path = ""] = (hostAndPath.match(/([^/?]*)(.*)?/) || []).splice(1);
87
- const { pathname, search, hash } = parsePath(path);
88
- return {
89
- protocol,
90
- auth: auth ? auth.substr(0, auth.length - 1) : "",
91
- host,
92
- pathname,
93
- search,
94
- hash
95
- };
96
- }
97
- function parsePath(input = "") {
98
- const [pathname = "", search = "", hash = ""] = (input.match(/([^#?]*)(\?[^#]*)?(#.*)?/) || []).splice(1);
99
- return {
100
- pathname,
101
- search,
102
- hash
103
- };
104
- }
12
+ const destr__default = /*#__PURE__*/_interopDefaultLegacy(destr);
105
13
 
106
14
  const defineHandle = (handler) => handler;
107
15
  const defineMiddleware = (middleware) => middleware;
@@ -139,64 +47,19 @@ function lazyHandle(handle, promisify) {
139
47
  };
140
48
  }
141
49
  function useBase(base, handle) {
142
- base = withoutTrailingSlash(base);
50
+ base = ufo.withoutTrailingSlash(base);
143
51
  if (!base) {
144
52
  return handle;
145
53
  }
146
54
  return function(req, res) {
147
55
  req.originalUrl = req.originalUrl || req.url || "/";
148
- req.url = withoutBase(req.url || "/", base);
56
+ req.url = ufo.withoutBase(req.url || "/", base);
149
57
  return handle(req, res);
150
58
  };
151
59
  }
152
60
 
153
- const suspectProtoRx = /"(?:_|\\u005[Ff])(?:_|\\u005[Ff])(?:p|\\u0070)(?:r|\\u0072)(?:o|\\u006[Ff])(?:t|\\u0074)(?:o|\\u006[Ff])(?:_|\\u005[Ff])(?:_|\\u005[Ff])"\s*:/;
154
- const suspectConstructorRx = /"(?:c|\\u0063)(?:o|\\u006[Ff])(?:n|\\u006[Ee])(?:s|\\u0073)(?:t|\\u0074)(?:r|\\u0072)(?:u|\\u0075)(?:c|\\u0063)(?:t|\\u0074)(?:o|\\u006[Ff])(?:r|\\u0072)"\s*:/;
155
- const JsonSigRx = /^["{[]|^-?[0-9][0-9.]{0,14}$/;
156
- function jsonParseTransform(key, value) {
157
- if (key === "__proto__" || key === "constructor") {
158
- return;
159
- }
160
- return value;
161
- }
162
- function destr(val) {
163
- if (typeof val !== "string") {
164
- return val;
165
- }
166
- const _lval = val.toLowerCase();
167
- if (_lval === "true") {
168
- return true;
169
- }
170
- if (_lval === "false") {
171
- return false;
172
- }
173
- if (_lval === "null") {
174
- return null;
175
- }
176
- if (_lval === "nan") {
177
- return NaN;
178
- }
179
- if (_lval === "infinity") {
180
- return Infinity;
181
- }
182
- if (_lval === "undefined") {
183
- return void 0;
184
- }
185
- if (!JsonSigRx.test(val)) {
186
- return val;
187
- }
188
- try {
189
- if (suspectProtoRx.test(val) || suspectConstructorRx.test(val)) {
190
- return JSON.parse(val, jsonParseTransform);
191
- }
192
- return JSON.parse(val);
193
- } catch (_e) {
194
- return val;
195
- }
196
- }
197
-
198
61
  function useQuery(req) {
199
- return getQuery(req.url || "");
62
+ return ufo.getQuery(req.url || "");
200
63
  }
201
64
  function useMethod(req, defaultMethod = "GET") {
202
65
  return (req.method || defaultMethod).toUpperCase();
@@ -253,7 +116,7 @@ async function useBody(req) {
253
116
  return req[ParsedBodySymbol];
254
117
  }
255
118
  const body = await useRawBody(req);
256
- const json = destr(body);
119
+ const json = destr__default(body);
257
120
  req[ParsedBodySymbol] = json;
258
121
  return json;
259
122
  }
@@ -263,203 +126,6 @@ const MIMES = {
263
126
  json: "application/json"
264
127
  };
265
128
 
266
- /*!
267
- * cookie
268
- * Copyright(c) 2012-2014 Roman Shtylman
269
- * Copyright(c) 2015 Douglas Christopher Wilson
270
- * MIT Licensed
271
- */
272
-
273
- /**
274
- * Module exports.
275
- * @public
276
- */
277
-
278
- var parse_1 = parse;
279
- var serialize_1 = serialize;
280
-
281
- /**
282
- * Module variables.
283
- * @private
284
- */
285
-
286
- var decode = decodeURIComponent;
287
- var encode = encodeURIComponent;
288
- var pairSplitRegExp = /; */;
289
-
290
- /**
291
- * RegExp to match field-content in RFC 7230 sec 3.2
292
- *
293
- * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
294
- * field-vchar = VCHAR / obs-text
295
- * obs-text = %x80-FF
296
- */
297
-
298
- var fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/;
299
-
300
- /**
301
- * Parse a cookie header.
302
- *
303
- * Parse the given cookie header string into an object
304
- * The object has the various cookies as keys(names) => values
305
- *
306
- * @param {string} str
307
- * @param {object} [options]
308
- * @return {object}
309
- * @public
310
- */
311
-
312
- function parse(str, options) {
313
- if (typeof str !== 'string') {
314
- throw new TypeError('argument str must be a string');
315
- }
316
-
317
- var obj = {};
318
- var opt = options || {};
319
- var pairs = str.split(pairSplitRegExp);
320
- var dec = opt.decode || decode;
321
-
322
- for (var i = 0; i < pairs.length; i++) {
323
- var pair = pairs[i];
324
- var eq_idx = pair.indexOf('=');
325
-
326
- // skip things that don't look like key=value
327
- if (eq_idx < 0) {
328
- continue;
329
- }
330
-
331
- var key = pair.substr(0, eq_idx).trim();
332
- var val = pair.substr(++eq_idx, pair.length).trim();
333
-
334
- // quoted values
335
- if ('"' == val[0]) {
336
- val = val.slice(1, -1);
337
- }
338
-
339
- // only assign once
340
- if (undefined == obj[key]) {
341
- obj[key] = tryDecode(val, dec);
342
- }
343
- }
344
-
345
- return obj;
346
- }
347
-
348
- /**
349
- * Serialize data into a cookie header.
350
- *
351
- * Serialize the a name value pair into a cookie string suitable for
352
- * http headers. An optional options object specified cookie parameters.
353
- *
354
- * serialize('foo', 'bar', { httpOnly: true })
355
- * => "foo=bar; httpOnly"
356
- *
357
- * @param {string} name
358
- * @param {string} val
359
- * @param {object} [options]
360
- * @return {string}
361
- * @public
362
- */
363
-
364
- function serialize(name, val, options) {
365
- var opt = options || {};
366
- var enc = opt.encode || encode;
367
-
368
- if (typeof enc !== 'function') {
369
- throw new TypeError('option encode is invalid');
370
- }
371
-
372
- if (!fieldContentRegExp.test(name)) {
373
- throw new TypeError('argument name is invalid');
374
- }
375
-
376
- var value = enc(val);
377
-
378
- if (value && !fieldContentRegExp.test(value)) {
379
- throw new TypeError('argument val is invalid');
380
- }
381
-
382
- var str = name + '=' + value;
383
-
384
- if (null != opt.maxAge) {
385
- var maxAge = opt.maxAge - 0;
386
- if (isNaN(maxAge)) throw new Error('maxAge should be a Number');
387
- str += '; Max-Age=' + Math.floor(maxAge);
388
- }
389
-
390
- if (opt.domain) {
391
- if (!fieldContentRegExp.test(opt.domain)) {
392
- throw new TypeError('option domain is invalid');
393
- }
394
-
395
- str += '; Domain=' + opt.domain;
396
- }
397
-
398
- if (opt.path) {
399
- if (!fieldContentRegExp.test(opt.path)) {
400
- throw new TypeError('option path is invalid');
401
- }
402
-
403
- str += '; Path=' + opt.path;
404
- }
405
-
406
- if (opt.expires) {
407
- if (typeof opt.expires.toUTCString !== 'function') {
408
- throw new TypeError('option expires is invalid');
409
- }
410
-
411
- str += '; Expires=' + opt.expires.toUTCString();
412
- }
413
-
414
- if (opt.httpOnly) {
415
- str += '; HttpOnly';
416
- }
417
-
418
- if (opt.secure) {
419
- str += '; Secure';
420
- }
421
-
422
- if (opt.sameSite) {
423
- var sameSite = typeof opt.sameSite === 'string'
424
- ? opt.sameSite.toLowerCase() : opt.sameSite;
425
-
426
- switch (sameSite) {
427
- case true:
428
- str += '; SameSite=Strict';
429
- break;
430
- case 'lax':
431
- str += '; SameSite=Lax';
432
- break;
433
- case 'strict':
434
- str += '; SameSite=Strict';
435
- break;
436
- case 'none':
437
- str += '; SameSite=None';
438
- break;
439
- default:
440
- throw new TypeError('option sameSite is invalid');
441
- }
442
- }
443
-
444
- return str;
445
- }
446
-
447
- /**
448
- * Try decoding a string using a decoding function.
449
- *
450
- * @param {string} str
451
- * @param {function} decode
452
- * @private
453
- */
454
-
455
- function tryDecode(str, decode) {
456
- try {
457
- return decode(str);
458
- } catch (e) {
459
- return str;
460
- }
461
- }
462
-
463
129
  const defer = typeof setImmediate !== "undefined" ? setImmediate : (fn) => fn();
464
130
  function send(res, data, type) {
465
131
  if (type) {
@@ -495,15 +161,21 @@ function appendHeader(res, name, value) {
495
161
  }
496
162
 
497
163
  function useCookies(req) {
498
- return parse_1(req.headers.cookie || "");
164
+ return cookieEs.parse(req.headers.cookie || "");
499
165
  }
500
166
  function useCookie(req, name) {
501
167
  return useCookies(req)[name];
502
168
  }
503
169
  function setCookie(res, name, value, serializeOptions) {
504
- const cookieStr = serialize_1(name, value, serializeOptions);
170
+ const cookieStr = cookieEs.serialize(name, value, serializeOptions);
505
171
  appendHeader(res, "Set-Cookie", cookieStr);
506
172
  }
173
+ function deleteCookie(res, name, serializeOptions) {
174
+ setCookie(res, name, "", {
175
+ ...serializeOptions,
176
+ maxAge: 0
177
+ });
178
+ }
507
179
 
508
180
  class H3Error extends Error {
509
181
  constructor() {
@@ -513,11 +185,10 @@ class H3Error extends Error {
513
185
  }
514
186
  }
515
187
  function createError(input) {
516
- var _a;
517
188
  if (input instanceof H3Error) {
518
189
  return input;
519
190
  }
520
- const err = new H3Error((_a = input.message) != null ? _a : input.statusMessage);
191
+ const err = new H3Error(input.message ?? input.statusMessage);
521
192
  if (input.statusCode) {
522
193
  err.statusCode = input.statusCode;
523
194
  }
@@ -595,7 +266,7 @@ function createHandle(stack, options) {
595
266
  if (!reqUrl.startsWith(layer.route)) {
596
267
  continue;
597
268
  }
598
- req.url = reqUrl.substr(layer.route.length) || "/";
269
+ req.url = reqUrl.slice(layer.route.length) || "/";
599
270
  } else {
600
271
  req.url = reqUrl;
601
272
  }
@@ -609,7 +280,7 @@ function createHandle(stack, options) {
609
280
  const type = typeof val;
610
281
  if (type === "string") {
611
282
  return send(res, val, MIMES.html);
612
- } else if (type === "object" && val !== void 0) {
283
+ } else if (type === "object" || type === "boolean" || type === "number") {
613
284
  if (val && val.buffer) {
614
285
  return send(res, val);
615
286
  } else if (val instanceof Error) {
@@ -629,12 +300,53 @@ function normalizeLayer(layer) {
629
300
  layer.promisify = layer.handle.length > 2;
630
301
  }
631
302
  return {
632
- route: withoutTrailingSlash(layer.route).toLocaleLowerCase(),
303
+ route: ufo.withoutTrailingSlash(layer.route),
633
304
  match: layer.match,
634
305
  handle: layer.lazy ? lazyHandle(layer.handle, layer.promisify) : layer.promisify ? promisifyHandle(layer.handle) : layer.handle
635
306
  };
636
307
  }
637
308
 
309
+ const RouterMethods = ["connect", "delete", "get", "head", "options", "post", "put", "trace"];
310
+ function createRouter() {
311
+ const _router = radix3.createRouter({});
312
+ const routes = {};
313
+ const router = {};
314
+ router.add = (path, handle, method = "all") => {
315
+ let route = routes[path];
316
+ if (!route) {
317
+ routes[path] = route = { handlers: {} };
318
+ _router.insert(path, route);
319
+ }
320
+ route.handlers[method] = handle;
321
+ return router;
322
+ };
323
+ for (const method of RouterMethods) {
324
+ router[method] = (path, handle) => router.add(path, handle, method);
325
+ }
326
+ router.handle = (req, res) => {
327
+ const matched = _router.lookup(req.url || "/");
328
+ if (!matched) {
329
+ throw createError({
330
+ statusCode: 404,
331
+ name: "Not Found",
332
+ statusMessage: `Cannot find any route matching ${req.url || "/"}.`
333
+ });
334
+ }
335
+ const method = (req.method || "get").toLowerCase();
336
+ const handler = matched.handlers[method] || matched.handlers.all;
337
+ if (!handler) {
338
+ throw createError({
339
+ statusCode: 405,
340
+ name: "Method Not Allowed",
341
+ statusMessage: `Method ${method} is not allowed on this route.`
342
+ });
343
+ }
344
+ req.params = matched.params || {};
345
+ return handler(req, res);
346
+ };
347
+ return router;
348
+ }
349
+
638
350
  exports.H3Error = H3Error;
639
351
  exports.MIMES = MIMES;
640
352
  exports.appendHeader = appendHeader;
@@ -643,9 +355,11 @@ exports.callHandle = callHandle;
643
355
  exports.createApp = createApp;
644
356
  exports.createError = createError;
645
357
  exports.createHandle = createHandle;
358
+ exports.createRouter = createRouter;
646
359
  exports.defaultContentType = defaultContentType;
647
360
  exports.defineHandle = defineHandle;
648
361
  exports.defineMiddleware = defineMiddleware;
362
+ exports.deleteCookie = deleteCookie;
649
363
  exports.isMethod = isMethod;
650
364
  exports.lazyHandle = lazyHandle;
651
365
  exports.promisifyHandle = promisifyHandle;
package/dist/index.d.ts CHANGED
@@ -3,11 +3,11 @@ import * as ufo from 'ufo';
3
3
 
4
4
  declare type Encoding = false | 'ascii' | 'utf8' | 'utf-8' | 'utf16le' | 'ucs2' | 'ucs-2' | 'base64' | 'latin1' | 'binary' | 'hex';
5
5
 
6
- declare type Handle<T = any> = (req: IncomingMessage, res: ServerResponse) => T;
6
+ declare type Handle<T = any, ReqT = {}> = (req: IncomingMessage & ReqT, 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>;
10
+ declare const defineHandle: <T>(handler: Handle<T, {}>) => Handle<T, {}>;
11
11
  declare const defineMiddleware: (middleware: Middleware) => Middleware;
12
12
  declare function promisifyHandle(handle: Handle | Middleware): PHandle;
13
13
  declare function callHandle(handle: Middleware, req: IncomingMessage, res: ServerResponse): Promise<unknown>;
@@ -73,8 +73,8 @@ declare class H3Error extends Error {
73
73
  */
74
74
  declare function createError(input: Partial<H3Error>): H3Error;
75
75
  /**
76
- * Recieve an error and return the corresponding response.<br>
77
- * H3 internally uses this fucntion to handle unhandled errors.<br>
76
+ * Receive an error and return the corresponding response.<br>
77
+ * H3 internally uses this function to handle unhandled errors.<br>
78
78
  * Note that calling this function will close the connection and no other data will be sent to client afterwards.
79
79
  *
80
80
  * @param res {ServerResponse} The ServerResponse object is passed as the second parameter in the handler function
@@ -232,9 +232,20 @@ declare function useCookie(req: IncomingMessage, name: string): string | undefin
232
232
  * ```
233
233
  */
234
234
  declare function setCookie(res: ServerResponse, name: string, value: string, serializeOptions?: CookieSerializeOptions): void;
235
+ /**
236
+ * Set a cookie value by name.
237
+ * @param res {ServerResponse} A ServerResponse object created by [http.Server](https://nodejs.org/api/http.html#http_class_http_server)
238
+ * @param name Name of the cookie to delete
239
+ * @param serializeOptions {CookieSerializeOptions} Cookie options
240
+ * ```ts
241
+ * deleteCookie(res, 'SessionId')
242
+ * ```
243
+ */
244
+ declare function deleteCookie(res: ServerResponse, name: string, serializeOptions?: CookieSerializeOptions): void;
235
245
 
236
- declare function useQuery(req: IncomingMessage): ufo.QueryObject;
237
246
  declare type HTTPMethod = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'CONNECT' | 'OPTIONS' | 'TRACE';
247
+
248
+ declare function useQuery(req: IncomingMessage): ufo.QueryObject;
238
249
  declare function useMethod(req: IncomingMessage, defaultMethod?: HTTPMethod): HTTPMethod;
239
250
  declare function isMethod(req: IncomingMessage, expected: HTTPMethod | HTTPMethod[], allowHead?: boolean): boolean;
240
251
  declare function assertMethod(req: IncomingMessage, expected: HTTPMethod | HTTPMethod[], allowHead?: boolean): void;
@@ -244,4 +255,16 @@ declare function defaultContentType(res: ServerResponse, type?: string): void;
244
255
  declare function sendRedirect(res: ServerResponse, location: string, code?: number): Promise<unknown>;
245
256
  declare function appendHeader(res: ServerResponse, name: string, value: string): void;
246
257
 
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 };
258
+ declare type RouterMethod = Lowercase<HTTPMethod>;
259
+ declare type HandleWithParams = Handle<any, {
260
+ params: Record<string, string>;
261
+ }>;
262
+ declare type AddWithMethod = (path: string, handle: HandleWithParams) => Router;
263
+ declare type AddRouteShortcuts = Record<Lowercase<HTTPMethod>, AddWithMethod>;
264
+ interface Router extends AddRouteShortcuts {
265
+ add: (path: string, handle: HandleWithParams, method?: RouterMethod | 'all') => Router;
266
+ handle: Handle;
267
+ }
268
+ declare function createRouter(): Router;
269
+
270
+ export { AddRouteShortcuts, AddWithMethod, App, AppOptions, AppUse, H3Error, Handle, HandleWithParams, InputLayer, InputStack, Layer, LazyHandle, MIMES, Matcher, Middleware, PHandle, Router, RouterMethod, Stack, appendHeader, assertMethod, callHandle, createApp, createError, createHandle, createRouter, defaultContentType, defineHandle, defineMiddleware, deleteCookie, isMethod, lazyHandle, promisifyHandle, send, sendError, sendRedirect, setCookie, use, useBase, useBody, useCookie, useCookies, useMethod, useQuery, useRawBody };
package/dist/index.mjs CHANGED
@@ -1,103 +1,7 @@
1
- const PLUS_RE = /\+/g;
2
- function decode$1(text = "") {
3
- try {
4
- return decodeURIComponent("" + text);
5
- } catch (_err) {
6
- return "" + text;
7
- }
8
- }
9
- function decodeQueryValue(text) {
10
- return decode$1(text.replace(PLUS_RE, " "));
11
- }
12
-
13
- function parseQuery(paramsStr = "") {
14
- const obj = {};
15
- if (paramsStr[0] === "?") {
16
- paramsStr = paramsStr.substr(1);
17
- }
18
- for (const param of paramsStr.split("&")) {
19
- const s = param.match(/([^=]+)=?(.*)/) || [];
20
- if (s.length < 2) {
21
- continue;
22
- }
23
- const key = decode$1(s[1]);
24
- if (key === "__proto__" || key === "constructor") {
25
- continue;
26
- }
27
- const value = decodeQueryValue(s[2] || "");
28
- if (obj[key]) {
29
- if (Array.isArray(obj[key])) {
30
- obj[key].push(value);
31
- } else {
32
- obj[key] = [obj[key], value];
33
- }
34
- } else {
35
- obj[key] = value;
36
- }
37
- }
38
- return obj;
39
- }
40
- function hasProtocol(inputStr, acceptProtocolRelative = false) {
41
- return /^\w+:\/\/.+/.test(inputStr) || acceptProtocolRelative && /^\/\/[^/]+/.test(inputStr);
42
- }
43
- const TRAILING_SLASH_RE = /\/$|\/\?/;
44
- function hasTrailingSlash(input = "", queryParams = false) {
45
- if (!queryParams) {
46
- return input.endsWith("/");
47
- }
48
- return TRAILING_SLASH_RE.test(input);
49
- }
50
- function withoutTrailingSlash(input = "", queryParams = false) {
51
- if (!queryParams) {
52
- return (hasTrailingSlash(input) ? input.slice(0, -1) : input) || "/";
53
- }
54
- if (!hasTrailingSlash(input, true)) {
55
- return input || "/";
56
- }
57
- const [s0, ...s] = input.split("?");
58
- return (s0.slice(0, -1) || "/") + (s.length ? `?${s.join("?")}` : "");
59
- }
60
- function withoutBase(input, base) {
61
- if (isEmptyURL(base)) {
62
- return input;
63
- }
64
- const _base = withoutTrailingSlash(base);
65
- if (input.startsWith(_base)) {
66
- return input.substr(_base.length) || "/";
67
- }
68
- return input;
69
- }
70
- function getQuery(input) {
71
- return parseQuery(parseURL(input).search);
72
- }
73
- function isEmptyURL(url) {
74
- return !url || url === "/";
75
- }
76
-
77
- function parseURL(input = "", defaultProto) {
78
- if (!hasProtocol(input, true)) {
79
- return defaultProto ? parseURL(defaultProto + input) : parsePath(input);
80
- }
81
- const [protocol = "", auth, hostAndPath] = (input.match(/([^:/]+:)?\/\/([^/@]+@)?(.*)/) || []).splice(1);
82
- const [host = "", path = ""] = (hostAndPath.match(/([^/?]*)(.*)?/) || []).splice(1);
83
- const { pathname, search, hash } = parsePath(path);
84
- return {
85
- protocol,
86
- auth: auth ? auth.substr(0, auth.length - 1) : "",
87
- host,
88
- pathname,
89
- search,
90
- hash
91
- };
92
- }
93
- function parsePath(input = "") {
94
- const [pathname = "", search = "", hash = ""] = (input.match(/([^#?]*)(\?[^#]*)?(#.*)?/) || []).splice(1);
95
- return {
96
- pathname,
97
- search,
98
- hash
99
- };
100
- }
1
+ import { withoutTrailingSlash, withoutBase, getQuery } from 'ufo';
2
+ import { createRouter as createRouter$1 } from 'radix3';
3
+ import destr from 'destr';
4
+ import { parse, serialize } from 'cookie-es';
101
5
 
102
6
  const defineHandle = (handler) => handler;
103
7
  const defineMiddleware = (middleware) => middleware;
@@ -146,51 +50,6 @@ function useBase(base, handle) {
146
50
  };
147
51
  }
148
52
 
149
- const suspectProtoRx = /"(?:_|\\u005[Ff])(?:_|\\u005[Ff])(?:p|\\u0070)(?:r|\\u0072)(?:o|\\u006[Ff])(?:t|\\u0074)(?:o|\\u006[Ff])(?:_|\\u005[Ff])(?:_|\\u005[Ff])"\s*:/;
150
- const suspectConstructorRx = /"(?:c|\\u0063)(?:o|\\u006[Ff])(?:n|\\u006[Ee])(?:s|\\u0073)(?:t|\\u0074)(?:r|\\u0072)(?:u|\\u0075)(?:c|\\u0063)(?:t|\\u0074)(?:o|\\u006[Ff])(?:r|\\u0072)"\s*:/;
151
- const JsonSigRx = /^["{[]|^-?[0-9][0-9.]{0,14}$/;
152
- function jsonParseTransform(key, value) {
153
- if (key === "__proto__" || key === "constructor") {
154
- return;
155
- }
156
- return value;
157
- }
158
- function destr(val) {
159
- if (typeof val !== "string") {
160
- return val;
161
- }
162
- const _lval = val.toLowerCase();
163
- if (_lval === "true") {
164
- return true;
165
- }
166
- if (_lval === "false") {
167
- return false;
168
- }
169
- if (_lval === "null") {
170
- return null;
171
- }
172
- if (_lval === "nan") {
173
- return NaN;
174
- }
175
- if (_lval === "infinity") {
176
- return Infinity;
177
- }
178
- if (_lval === "undefined") {
179
- return void 0;
180
- }
181
- if (!JsonSigRx.test(val)) {
182
- return val;
183
- }
184
- try {
185
- if (suspectProtoRx.test(val) || suspectConstructorRx.test(val)) {
186
- return JSON.parse(val, jsonParseTransform);
187
- }
188
- return JSON.parse(val);
189
- } catch (_e) {
190
- return val;
191
- }
192
- }
193
-
194
53
  function useQuery(req) {
195
54
  return getQuery(req.url || "");
196
55
  }
@@ -259,203 +118,6 @@ const MIMES = {
259
118
  json: "application/json"
260
119
  };
261
120
 
262
- /*!
263
- * cookie
264
- * Copyright(c) 2012-2014 Roman Shtylman
265
- * Copyright(c) 2015 Douglas Christopher Wilson
266
- * MIT Licensed
267
- */
268
-
269
- /**
270
- * Module exports.
271
- * @public
272
- */
273
-
274
- var parse_1 = parse;
275
- var serialize_1 = serialize;
276
-
277
- /**
278
- * Module variables.
279
- * @private
280
- */
281
-
282
- var decode = decodeURIComponent;
283
- var encode = encodeURIComponent;
284
- var pairSplitRegExp = /; */;
285
-
286
- /**
287
- * RegExp to match field-content in RFC 7230 sec 3.2
288
- *
289
- * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
290
- * field-vchar = VCHAR / obs-text
291
- * obs-text = %x80-FF
292
- */
293
-
294
- var fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/;
295
-
296
- /**
297
- * Parse a cookie header.
298
- *
299
- * Parse the given cookie header string into an object
300
- * The object has the various cookies as keys(names) => values
301
- *
302
- * @param {string} str
303
- * @param {object} [options]
304
- * @return {object}
305
- * @public
306
- */
307
-
308
- function parse(str, options) {
309
- if (typeof str !== 'string') {
310
- throw new TypeError('argument str must be a string');
311
- }
312
-
313
- var obj = {};
314
- var opt = options || {};
315
- var pairs = str.split(pairSplitRegExp);
316
- var dec = opt.decode || decode;
317
-
318
- for (var i = 0; i < pairs.length; i++) {
319
- var pair = pairs[i];
320
- var eq_idx = pair.indexOf('=');
321
-
322
- // skip things that don't look like key=value
323
- if (eq_idx < 0) {
324
- continue;
325
- }
326
-
327
- var key = pair.substr(0, eq_idx).trim();
328
- var val = pair.substr(++eq_idx, pair.length).trim();
329
-
330
- // quoted values
331
- if ('"' == val[0]) {
332
- val = val.slice(1, -1);
333
- }
334
-
335
- // only assign once
336
- if (undefined == obj[key]) {
337
- obj[key] = tryDecode(val, dec);
338
- }
339
- }
340
-
341
- return obj;
342
- }
343
-
344
- /**
345
- * Serialize data into a cookie header.
346
- *
347
- * Serialize the a name value pair into a cookie string suitable for
348
- * http headers. An optional options object specified cookie parameters.
349
- *
350
- * serialize('foo', 'bar', { httpOnly: true })
351
- * => "foo=bar; httpOnly"
352
- *
353
- * @param {string} name
354
- * @param {string} val
355
- * @param {object} [options]
356
- * @return {string}
357
- * @public
358
- */
359
-
360
- function serialize(name, val, options) {
361
- var opt = options || {};
362
- var enc = opt.encode || encode;
363
-
364
- if (typeof enc !== 'function') {
365
- throw new TypeError('option encode is invalid');
366
- }
367
-
368
- if (!fieldContentRegExp.test(name)) {
369
- throw new TypeError('argument name is invalid');
370
- }
371
-
372
- var value = enc(val);
373
-
374
- if (value && !fieldContentRegExp.test(value)) {
375
- throw new TypeError('argument val is invalid');
376
- }
377
-
378
- var str = name + '=' + value;
379
-
380
- if (null != opt.maxAge) {
381
- var maxAge = opt.maxAge - 0;
382
- if (isNaN(maxAge)) throw new Error('maxAge should be a Number');
383
- str += '; Max-Age=' + Math.floor(maxAge);
384
- }
385
-
386
- if (opt.domain) {
387
- if (!fieldContentRegExp.test(opt.domain)) {
388
- throw new TypeError('option domain is invalid');
389
- }
390
-
391
- str += '; Domain=' + opt.domain;
392
- }
393
-
394
- if (opt.path) {
395
- if (!fieldContentRegExp.test(opt.path)) {
396
- throw new TypeError('option path is invalid');
397
- }
398
-
399
- str += '; Path=' + opt.path;
400
- }
401
-
402
- if (opt.expires) {
403
- if (typeof opt.expires.toUTCString !== 'function') {
404
- throw new TypeError('option expires is invalid');
405
- }
406
-
407
- str += '; Expires=' + opt.expires.toUTCString();
408
- }
409
-
410
- if (opt.httpOnly) {
411
- str += '; HttpOnly';
412
- }
413
-
414
- if (opt.secure) {
415
- str += '; Secure';
416
- }
417
-
418
- if (opt.sameSite) {
419
- var sameSite = typeof opt.sameSite === 'string'
420
- ? opt.sameSite.toLowerCase() : opt.sameSite;
421
-
422
- switch (sameSite) {
423
- case true:
424
- str += '; SameSite=Strict';
425
- break;
426
- case 'lax':
427
- str += '; SameSite=Lax';
428
- break;
429
- case 'strict':
430
- str += '; SameSite=Strict';
431
- break;
432
- case 'none':
433
- str += '; SameSite=None';
434
- break;
435
- default:
436
- throw new TypeError('option sameSite is invalid');
437
- }
438
- }
439
-
440
- return str;
441
- }
442
-
443
- /**
444
- * Try decoding a string using a decoding function.
445
- *
446
- * @param {string} str
447
- * @param {function} decode
448
- * @private
449
- */
450
-
451
- function tryDecode(str, decode) {
452
- try {
453
- return decode(str);
454
- } catch (e) {
455
- return str;
456
- }
457
- }
458
-
459
121
  const defer = typeof setImmediate !== "undefined" ? setImmediate : (fn) => fn();
460
122
  function send(res, data, type) {
461
123
  if (type) {
@@ -491,15 +153,21 @@ function appendHeader(res, name, value) {
491
153
  }
492
154
 
493
155
  function useCookies(req) {
494
- return parse_1(req.headers.cookie || "");
156
+ return parse(req.headers.cookie || "");
495
157
  }
496
158
  function useCookie(req, name) {
497
159
  return useCookies(req)[name];
498
160
  }
499
161
  function setCookie(res, name, value, serializeOptions) {
500
- const cookieStr = serialize_1(name, value, serializeOptions);
162
+ const cookieStr = serialize(name, value, serializeOptions);
501
163
  appendHeader(res, "Set-Cookie", cookieStr);
502
164
  }
165
+ function deleteCookie(res, name, serializeOptions) {
166
+ setCookie(res, name, "", {
167
+ ...serializeOptions,
168
+ maxAge: 0
169
+ });
170
+ }
503
171
 
504
172
  class H3Error extends Error {
505
173
  constructor() {
@@ -509,11 +177,10 @@ class H3Error extends Error {
509
177
  }
510
178
  }
511
179
  function createError(input) {
512
- var _a;
513
180
  if (input instanceof H3Error) {
514
181
  return input;
515
182
  }
516
- const err = new H3Error((_a = input.message) != null ? _a : input.statusMessage);
183
+ const err = new H3Error(input.message ?? input.statusMessage);
517
184
  if (input.statusCode) {
518
185
  err.statusCode = input.statusCode;
519
186
  }
@@ -591,7 +258,7 @@ function createHandle(stack, options) {
591
258
  if (!reqUrl.startsWith(layer.route)) {
592
259
  continue;
593
260
  }
594
- req.url = reqUrl.substr(layer.route.length) || "/";
261
+ req.url = reqUrl.slice(layer.route.length) || "/";
595
262
  } else {
596
263
  req.url = reqUrl;
597
264
  }
@@ -605,7 +272,7 @@ function createHandle(stack, options) {
605
272
  const type = typeof val;
606
273
  if (type === "string") {
607
274
  return send(res, val, MIMES.html);
608
- } else if (type === "object" && val !== void 0) {
275
+ } else if (type === "object" || type === "boolean" || type === "number") {
609
276
  if (val && val.buffer) {
610
277
  return send(res, val);
611
278
  } else if (val instanceof Error) {
@@ -625,10 +292,51 @@ function normalizeLayer(layer) {
625
292
  layer.promisify = layer.handle.length > 2;
626
293
  }
627
294
  return {
628
- route: withoutTrailingSlash(layer.route).toLocaleLowerCase(),
295
+ route: withoutTrailingSlash(layer.route),
629
296
  match: layer.match,
630
297
  handle: layer.lazy ? lazyHandle(layer.handle, layer.promisify) : layer.promisify ? promisifyHandle(layer.handle) : layer.handle
631
298
  };
632
299
  }
633
300
 
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 };
301
+ const RouterMethods = ["connect", "delete", "get", "head", "options", "post", "put", "trace"];
302
+ function createRouter() {
303
+ const _router = createRouter$1({});
304
+ const routes = {};
305
+ const router = {};
306
+ router.add = (path, handle, method = "all") => {
307
+ let route = routes[path];
308
+ if (!route) {
309
+ routes[path] = route = { handlers: {} };
310
+ _router.insert(path, route);
311
+ }
312
+ route.handlers[method] = handle;
313
+ return router;
314
+ };
315
+ for (const method of RouterMethods) {
316
+ router[method] = (path, handle) => router.add(path, handle, method);
317
+ }
318
+ router.handle = (req, res) => {
319
+ const matched = _router.lookup(req.url || "/");
320
+ if (!matched) {
321
+ throw createError({
322
+ statusCode: 404,
323
+ name: "Not Found",
324
+ statusMessage: `Cannot find any route matching ${req.url || "/"}.`
325
+ });
326
+ }
327
+ const method = (req.method || "get").toLowerCase();
328
+ const handler = matched.handlers[method] || matched.handlers.all;
329
+ if (!handler) {
330
+ throw createError({
331
+ statusCode: 405,
332
+ name: "Method Not Allowed",
333
+ statusMessage: `Method ${method} is not allowed on this route.`
334
+ });
335
+ }
336
+ req.params = matched.params || {};
337
+ return handler(req, res);
338
+ };
339
+ return router;
340
+ }
341
+
342
+ export { H3Error, MIMES, appendHeader, assertMethod, callHandle, createApp, createError, createHandle, createRouter, defaultContentType, defineHandle, defineMiddleware, deleteCookie, 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.8",
3
+ "version": "0.4.1",
4
4
  "description": "Tiny JavaScript Server",
5
5
  "repository": "unjs/h3",
6
6
  "license": "MIT",
@@ -18,36 +18,38 @@
18
18
  "dist"
19
19
  ],
20
20
  "scripts": {
21
- "build": "siroc build",
22
- "dev": "jiti test/playground",
23
- "lint": "eslint --ext ts .",
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
- "test": "yarn lint && jest"
21
+ "build": "unbuild",
22
+ "dev": "jiti ./playground/index.ts",
23
+ "lint": "eslint --ext ts,mjs,cjs .",
24
+ "profile": "0x -o -D .profile -P 'autocannon -c 100 -p 10 -d 40 http://localhost:$PORT' ./playground/server.cjs",
25
+ "release": "yarn lint && yarn test && yarn build && standard-version && npm publish && git push --follow-tags",
26
+ "test": "vitest run"
27
+ },
28
+ "dependencies": {
29
+ "cookie-es": "^0.5.0",
30
+ "destr": "^1.1.0",
31
+ "radix3": "^0.1.1",
32
+ "ufo": "^0.7.11"
27
33
  },
28
34
  "devDependencies": {
29
35
  "0x": "latest",
30
36
  "@nuxtjs/eslint-config-typescript": "latest",
31
37
  "@types/cookie": "latest",
32
38
  "@types/express": "latest",
33
- "@types/jest": "latest",
34
39
  "@types/node": "latest",
35
40
  "@types/supertest": "latest",
36
41
  "autocannon": "latest",
42
+ "c8": "latest",
37
43
  "connect": "latest",
38
- "cookie-es": "latest",
39
- "destr": "latest",
40
44
  "eslint": "latest",
41
45
  "express": "latest",
42
46
  "get-port": "^5.0.0",
43
- "jest": "latest",
44
47
  "jiti": "latest",
45
48
  "listhen": "latest",
46
- "siroc": "latest",
47
49
  "standard-version": "latest",
48
50
  "supertest": "latest",
49
- "ts-jest": "latest",
50
51
  "typescript": "latest",
51
- "ufo": "latest"
52
+ "unbuild": "latest",
53
+ "vitest": "latest"
52
54
  }
53
55
  }