hoa 0.3.2 → 0.3.4
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/CHANGELOG.md +9 -0
- package/dist/cjs/context.cjs +152 -0
- package/dist/cjs/hoa.cjs +158 -0
- package/dist/cjs/lib/compose.cjs +35 -0
- package/dist/cjs/lib/http-error.cjs +42 -0
- package/dist/cjs/lib/utils.cjs +214 -0
- package/dist/cjs/request.cjs +516 -0
- package/dist/cjs/response.cjs +335 -0
- package/dist/esm/context.mjs +152 -0
- package/dist/esm/hoa.mjs +148 -0
- package/dist/esm/lib/compose.mjs +34 -0
- package/dist/esm/lib/http-error.mjs +42 -0
- package/dist/esm/lib/utils.mjs +207 -0
- package/dist/esm/request.mjs +516 -0
- package/dist/esm/response.mjs +335 -0
- package/package.json +7 -7
- package/types/index.d.ts +52 -45
- package/dist/cjs/context.js +0 -177
- package/dist/cjs/hoa.js +0 -192
- package/dist/cjs/lib/compose.js +0 -40
- package/dist/cjs/lib/http-error.js +0 -61
- package/dist/cjs/lib/utils.js +0 -190
- package/dist/cjs/request.js +0 -546
- package/dist/cjs/response.js +0 -379
- package/dist/esm/context.js +0 -148
- package/dist/esm/hoa.js +0 -151
- package/dist/esm/lib/compose.js +0 -21
- package/dist/esm/lib/http-error.js +0 -42
- package/dist/esm/lib/utils.js +0 -161
- package/dist/esm/request.js +0 -527
- package/dist/esm/response.js +0 -360
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
## v0.3.4 / 2026-01-26
|
|
2
|
+
|
|
3
|
+
- feat: use tsdown instead of tsup
|
|
4
|
+
- fix: update devDependencies
|
|
5
|
+
|
|
6
|
+
## v0.3.3 / 2025-12-03
|
|
7
|
+
|
|
8
|
+
- fix: types improvements
|
|
9
|
+
|
|
1
10
|
## v0.3.2 / 2025-11-09
|
|
2
11
|
|
|
3
12
|
- fix: export statusTextMapping + statusRedirectMapping + statusEmptyMapping
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
const require_lib_utils = require('./lib/utils.cjs');
|
|
2
|
+
const require_lib_http_error = require('./lib/http-error.cjs');
|
|
3
|
+
|
|
4
|
+
//#region src/context.js
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {Object} CtxJSON
|
|
7
|
+
* @property {ReturnType<import('./hoa.js').default.prototype.toJSON>} app
|
|
8
|
+
* @property {ReturnType<import('./request.js').default.prototype.toJSON>} req
|
|
9
|
+
* @property {ReturnType<import('./response.js').default.prototype.toJSON>} res
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* @class HoaContext
|
|
13
|
+
*/
|
|
14
|
+
var HoaContext = class {
|
|
15
|
+
/**
|
|
16
|
+
* Create a context for a single HTTP request.
|
|
17
|
+
*
|
|
18
|
+
* @param {Object} [options={}]
|
|
19
|
+
* @param {Request} [options.request] - Web Standard Request
|
|
20
|
+
* @param {any} [options.env] - Environment (platform-specific)
|
|
21
|
+
* @param {any} [options.executionCtx] - Execution context (platform-specific)
|
|
22
|
+
* @public
|
|
23
|
+
*/
|
|
24
|
+
constructor(options = {}) {
|
|
25
|
+
this.request = options.request;
|
|
26
|
+
this.env = options.env;
|
|
27
|
+
this.executionCtx = options.executionCtx;
|
|
28
|
+
this.state = Object.create(null);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Throw an HttpError.
|
|
32
|
+
*
|
|
33
|
+
* @param {number} status - HTTP status code
|
|
34
|
+
* @param {string|{message?: string, cause?: any, headers?: HeadersInit}} [messageOrOptions] - Error message or options object
|
|
35
|
+
* @throws {HttpError}
|
|
36
|
+
* @public
|
|
37
|
+
*/
|
|
38
|
+
throw(...args) {
|
|
39
|
+
throw new require_lib_http_error(...args);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Assert condition or throw an HttpError.
|
|
43
|
+
*
|
|
44
|
+
* @param {any} value - Condition to assert
|
|
45
|
+
* @param {...any} args - Arguments passed to HttpError constructor
|
|
46
|
+
* @throws {HttpError}
|
|
47
|
+
* @public
|
|
48
|
+
*/
|
|
49
|
+
assert(value, ...args) {
|
|
50
|
+
if (value) return;
|
|
51
|
+
throw new require_lib_http_error(...args);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Default error handling and response builder.
|
|
55
|
+
*
|
|
56
|
+
* @param {Error} err - Error to handle
|
|
57
|
+
* @returns {Response} Web Standard Response object
|
|
58
|
+
* @private
|
|
59
|
+
*/
|
|
60
|
+
onerror(err) {
|
|
61
|
+
const { res } = this;
|
|
62
|
+
if (!(Object.prototype.toString.call(err) === "[object Error]" || err instanceof Error)) err = /* @__PURE__ */ new Error(`non-error thrown: ${JSON.stringify(err)}`);
|
|
63
|
+
this.app.onerror(err, this);
|
|
64
|
+
res.headers = new Headers();
|
|
65
|
+
res.set(err.headers);
|
|
66
|
+
res.type = "text";
|
|
67
|
+
let status = err.status || err.statusCode;
|
|
68
|
+
if (typeof status !== "number" || !require_lib_utils.statusTextMapping[status]) status = 500;
|
|
69
|
+
const message = require_lib_utils.statusTextMapping[status];
|
|
70
|
+
const msg = err.expose ? err.message : message;
|
|
71
|
+
res.status = status;
|
|
72
|
+
res.body = msg;
|
|
73
|
+
return new Response(res.body, {
|
|
74
|
+
status: res.status,
|
|
75
|
+
headers: res._headers
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Build Web Standard Response from current context state.
|
|
80
|
+
* Handles various body types, HEAD requests, and status-specific behaviors.
|
|
81
|
+
*
|
|
82
|
+
* @returns {Response} Web Standard Response object
|
|
83
|
+
* @public
|
|
84
|
+
*/
|
|
85
|
+
get response() {
|
|
86
|
+
const { res, req } = this;
|
|
87
|
+
let body = res.body;
|
|
88
|
+
if (req.method === "HEAD") {
|
|
89
|
+
if (!res.has("Content-Length")) {
|
|
90
|
+
const contentLength = res.length;
|
|
91
|
+
if (Number.isInteger(contentLength)) res.length = contentLength;
|
|
92
|
+
}
|
|
93
|
+
return new Response(null, {
|
|
94
|
+
status: res.status,
|
|
95
|
+
statusText: res.statusText,
|
|
96
|
+
headers: res._headers
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (require_lib_utils.statusEmptyMapping[res.status]) {
|
|
100
|
+
res.body = null;
|
|
101
|
+
return new Response(null, {
|
|
102
|
+
status: res.status,
|
|
103
|
+
statusText: res.statusText,
|
|
104
|
+
headers: res._headers
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
if (body == null) {
|
|
108
|
+
if (res._explicitNullBody) {
|
|
109
|
+
res.delete("Content-Type");
|
|
110
|
+
res.delete("Transfer-Encoding");
|
|
111
|
+
res.set("Content-Length", "0");
|
|
112
|
+
}
|
|
113
|
+
return new Response(null, {
|
|
114
|
+
status: res.status,
|
|
115
|
+
statusText: res.statusText,
|
|
116
|
+
headers: res._headers
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
if (typeof body === "string" || body instanceof Blob || body instanceof ArrayBuffer || ArrayBuffer.isView(body) || body instanceof ReadableStream || body instanceof FormData || body instanceof URLSearchParams) return new Response(body, {
|
|
120
|
+
status: res.status,
|
|
121
|
+
statusText: res.statusText,
|
|
122
|
+
headers: res._headers
|
|
123
|
+
});
|
|
124
|
+
if (body instanceof Response) return new Response(body.body, {
|
|
125
|
+
status: res.status,
|
|
126
|
+
statusText: res.statusText,
|
|
127
|
+
headers: res._headers
|
|
128
|
+
});
|
|
129
|
+
body = JSON.stringify(body);
|
|
130
|
+
return new Response(body, {
|
|
131
|
+
status: res.status,
|
|
132
|
+
statusText: res.statusText,
|
|
133
|
+
headers: res._headers
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Return JSON representation of the context.
|
|
138
|
+
*
|
|
139
|
+
* @returns {CtxJSON} JSON representation of context
|
|
140
|
+
* @public
|
|
141
|
+
*/
|
|
142
|
+
toJSON() {
|
|
143
|
+
return {
|
|
144
|
+
app: this.app.toJSON(),
|
|
145
|
+
req: this.req.toJSON(),
|
|
146
|
+
res: this.res.toJSON()
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
//#endregion
|
|
152
|
+
module.exports = HoaContext;
|
package/dist/cjs/hoa.cjs
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
2
|
+
const require_lib_utils = require('./lib/utils.cjs');
|
|
3
|
+
const require_lib_http_error = require('./lib/http-error.cjs');
|
|
4
|
+
const require_context = require('./context.cjs');
|
|
5
|
+
const require_lib_compose = require('./lib/compose.cjs');
|
|
6
|
+
const require_request = require('./request.cjs');
|
|
7
|
+
const require_response = require('./response.cjs');
|
|
8
|
+
|
|
9
|
+
//#region src/hoa.js
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Object} AppJSON
|
|
12
|
+
* @property {string} name - Application name
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* @class Hoa
|
|
16
|
+
* @property {string} name - Application name
|
|
17
|
+
* @property {boolean} [silent] - Suppress error console output when true
|
|
18
|
+
*/
|
|
19
|
+
var Hoa = class Hoa {
|
|
20
|
+
/**
|
|
21
|
+
* Create an Hoa instance.
|
|
22
|
+
*
|
|
23
|
+
* @param {Object} [options={}] - Application options
|
|
24
|
+
* @param {string} [options.name='Hoa'] - Application name for identification
|
|
25
|
+
*/
|
|
26
|
+
constructor(options = {}) {
|
|
27
|
+
this.name = options.name || "Hoa";
|
|
28
|
+
this.HoaContext = require_context;
|
|
29
|
+
this.HoaRequest = require_request;
|
|
30
|
+
this.HoaResponse = require_response;
|
|
31
|
+
this.middlewares = [];
|
|
32
|
+
this.fetch = this.fetch.bind(this);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Extend the application with a plugin initializer.
|
|
36
|
+
*
|
|
37
|
+
* @param {HoaExtension} fn - Plugin function that receives the app instance
|
|
38
|
+
* @returns {Hoa} The Hoa instance for method chaining
|
|
39
|
+
* @throws {TypeError}
|
|
40
|
+
* @public
|
|
41
|
+
*/
|
|
42
|
+
extend(fn) {
|
|
43
|
+
if (typeof fn !== "function") throw new TypeError("extend() must receive a function!");
|
|
44
|
+
fn(this);
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Register a middleware. Executed in registration order.
|
|
49
|
+
*
|
|
50
|
+
* @param {HoaMiddleware} fn - Middleware function
|
|
51
|
+
* @returns {Hoa} The Hoa instance for method chaining
|
|
52
|
+
* @throws {TypeError}
|
|
53
|
+
* @public
|
|
54
|
+
*/
|
|
55
|
+
use(fn) {
|
|
56
|
+
if (typeof fn !== "function") throw new TypeError("use() must receive a function!");
|
|
57
|
+
this.middlewares.push(fn);
|
|
58
|
+
this._composedMiddleware = null;
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Web Standards fetch handler - main entry point for HTTP requests.
|
|
63
|
+
* Compatible with Cloudflare Workers, Deno, and other Web Standards environments.
|
|
64
|
+
*
|
|
65
|
+
* @param {Request} request - Web Standard Request object
|
|
66
|
+
* @param {any} [env] - Environment variables (platform-specific)
|
|
67
|
+
* @param {any} [executionCtx] - Execution context (platform-specific)
|
|
68
|
+
* @returns {Promise<Response>} Web Standard Response object
|
|
69
|
+
* @public
|
|
70
|
+
*/
|
|
71
|
+
fetch(request, env, executionCtx) {
|
|
72
|
+
const ctx = this.createContext(request, env, executionCtx);
|
|
73
|
+
if (!this._composedMiddleware) this._composedMiddleware = require_lib_compose(this.middlewares);
|
|
74
|
+
return this.handleRequest(ctx, this._composedMiddleware);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Handle incoming request through the middleware stack.
|
|
78
|
+
* Manages error handling and response building.
|
|
79
|
+
*
|
|
80
|
+
* @param {HoaContext} ctx - Request context
|
|
81
|
+
* @param {HoaMiddleware} middlewareFn - Composed middleware function
|
|
82
|
+
* @returns {Promise<Response>} Web Standard Response object
|
|
83
|
+
* @private
|
|
84
|
+
*/
|
|
85
|
+
handleRequest(ctx, middlewareFn) {
|
|
86
|
+
return middlewareFn(ctx).then(() => ctx.response).catch((err) => ctx.onerror(err));
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Create context for incoming request with linked request/response objects.
|
|
90
|
+
* Establishes the context chain: ctx ↔ req ↔ res ↔ app
|
|
91
|
+
*
|
|
92
|
+
* @param {Request} request - Web Standard Request object
|
|
93
|
+
* @param {any} [env] - Environment variables
|
|
94
|
+
* @param {any} [executionCtx] - Execution context
|
|
95
|
+
* @returns {HoaContext} Created context instance
|
|
96
|
+
* @private
|
|
97
|
+
*/
|
|
98
|
+
createContext(request, env, executionCtx) {
|
|
99
|
+
const ctx = new this.HoaContext({
|
|
100
|
+
request,
|
|
101
|
+
env,
|
|
102
|
+
executionCtx
|
|
103
|
+
});
|
|
104
|
+
const req = ctx.req = new this.HoaRequest();
|
|
105
|
+
const res = ctx.res = new this.HoaResponse();
|
|
106
|
+
ctx.app = req.app = res.app = this;
|
|
107
|
+
req.ctx = res.ctx = ctx;
|
|
108
|
+
req.res = res;
|
|
109
|
+
res.req = req;
|
|
110
|
+
return ctx;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Default error handler for unhandled application errors.
|
|
114
|
+
* Logs errors to console unless they're client errors (4xx) or explicitly exposed.
|
|
115
|
+
*
|
|
116
|
+
* @param {Error} err - Error to handle
|
|
117
|
+
* @param {HoaContext} [ctx] - Request context (optional)
|
|
118
|
+
* @returns {void}
|
|
119
|
+
* @throws {TypeError}
|
|
120
|
+
* @private
|
|
121
|
+
*/
|
|
122
|
+
onerror(err, ctx) {
|
|
123
|
+
if (!(Object.prototype.toString.call(err) === "[object Error]" || err instanceof Error)) throw new TypeError(`non-error thrown: ${JSON.stringify(err)}`);
|
|
124
|
+
if (err.status === 404 || err.expose) return;
|
|
125
|
+
if (this.silent) return;
|
|
126
|
+
console.error(err);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* ESM/CJS interop helper for default exports.
|
|
130
|
+
*
|
|
131
|
+
* @returns {typeof Hoa} The Hoa class
|
|
132
|
+
* @static
|
|
133
|
+
*/
|
|
134
|
+
static get default() {
|
|
135
|
+
return Hoa;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Return JSON representation of the app.
|
|
139
|
+
*
|
|
140
|
+
* @returns {AppJSON} JSON representation of application
|
|
141
|
+
* @public
|
|
142
|
+
*/
|
|
143
|
+
toJSON() {
|
|
144
|
+
return { name: this.name };
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
//#endregion
|
|
149
|
+
exports.Hoa = Hoa;
|
|
150
|
+
exports.default = Hoa;
|
|
151
|
+
exports.HoaContext = require_context;
|
|
152
|
+
exports.HoaRequest = require_request;
|
|
153
|
+
exports.HoaResponse = require_response;
|
|
154
|
+
exports.HttpError = require_lib_http_error;
|
|
155
|
+
exports.compose = require_lib_compose;
|
|
156
|
+
exports.statusEmptyMapping = require_lib_utils.statusEmptyMapping;
|
|
157
|
+
exports.statusRedirectMapping = require_lib_utils.statusRedirectMapping;
|
|
158
|
+
exports.statusTextMapping = require_lib_utils.statusTextMapping;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/lib/compose.js
|
|
3
|
+
/**
|
|
4
|
+
* Compose middleware functions into a single dispatcher.
|
|
5
|
+
*
|
|
6
|
+
* @param {HoaMiddleware[]} middlewares - Array of middleware functions
|
|
7
|
+
* @returns {HoaMiddleware} Composed middleware function
|
|
8
|
+
* @private
|
|
9
|
+
*/
|
|
10
|
+
const composeSlim = (middlewares) => async (ctx, next) => {
|
|
11
|
+
const dispatch = (i) => async () => {
|
|
12
|
+
const fn = i === middlewares.length ? next : middlewares[i];
|
|
13
|
+
if (!fn) return;
|
|
14
|
+
return await fn(ctx, dispatch(i + 1));
|
|
15
|
+
};
|
|
16
|
+
return dispatch(0)();
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Compose multiple middleware functions into one.
|
|
20
|
+
* Validates input, flattens nested arrays, and returns a composed dispatcher.
|
|
21
|
+
*
|
|
22
|
+
* @param {HoaMiddleware[]|HoaMiddleware[][]} middlewares - Array of middleware functions or nested arrays
|
|
23
|
+
* @returns {HoaMiddleware} Composed middleware function
|
|
24
|
+
* @throws {TypeError}
|
|
25
|
+
* @public
|
|
26
|
+
*/
|
|
27
|
+
function compose(middlewares) {
|
|
28
|
+
if (!Array.isArray(middlewares)) throw new TypeError("compose() must receive an array of middleware functions!");
|
|
29
|
+
middlewares = middlewares.flat();
|
|
30
|
+
for (const middleware of middlewares) if (typeof middleware !== "function") throw new TypeError("Middleware must be composed of functions!");
|
|
31
|
+
return composeSlim(middlewares);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
//#endregion
|
|
35
|
+
module.exports = compose;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const require_lib_utils = require('./utils.cjs');
|
|
2
|
+
|
|
3
|
+
//#region src/lib/http-error.js
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {Object} HttpErrorOptions
|
|
6
|
+
* @property {string} [message] - Custom error message
|
|
7
|
+
* @property {Error} [cause] - The underlying cause of this error
|
|
8
|
+
* @property {boolean} [expose] - Whether to expose the error message to clients (defaults based on status)
|
|
9
|
+
* @property {HeadersInit} [headers] - Additional response headers
|
|
10
|
+
*/
|
|
11
|
+
var HttpError = class HttpError extends Error {
|
|
12
|
+
/**
|
|
13
|
+
* Create a new HttpError instance.
|
|
14
|
+
*
|
|
15
|
+
* @param {number} status - HTTP status code (400-599, invalid codes become 500)
|
|
16
|
+
* @param {string|HttpErrorOptions} [message] - Error message or options object
|
|
17
|
+
* @param {HttpErrorOptions} [options] - Additional options when second param is string
|
|
18
|
+
* @throws {TypeError}
|
|
19
|
+
*/
|
|
20
|
+
constructor(status, message, options) {
|
|
21
|
+
if (!Number.isInteger(status)) throw new TypeError("status code must be an integer");
|
|
22
|
+
if (status < 400 || status >= 600) status = 500;
|
|
23
|
+
let finalOptions = {};
|
|
24
|
+
if (typeof message === "string") {
|
|
25
|
+
finalOptions.message = message;
|
|
26
|
+
if (options && typeof options === "object") finalOptions = {
|
|
27
|
+
...finalOptions,
|
|
28
|
+
...options
|
|
29
|
+
};
|
|
30
|
+
} else if (message && typeof message === "object") finalOptions = message;
|
|
31
|
+
message = finalOptions.message ?? require_lib_utils.statusTextMapping[status] ?? "Unknown error";
|
|
32
|
+
super(message, { cause: finalOptions.cause });
|
|
33
|
+
this.name = "HttpError";
|
|
34
|
+
this.status = this.statusCode = status;
|
|
35
|
+
this.expose = finalOptions.expose ?? status < 500;
|
|
36
|
+
if (finalOptions.headers) this.headers = Object.fromEntries(new Headers(finalOptions.headers).entries());
|
|
37
|
+
if (Error.captureStackTrace) Error.captureStackTrace(this, HttpError);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
//#endregion
|
|
42
|
+
module.exports = HttpError;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/lib/utils.js
|
|
3
|
+
/**
|
|
4
|
+
* Parse URLSearchParams into a query object, handling multiple values for the same key.
|
|
5
|
+
* When a key appears multiple times, values are collected into an array.
|
|
6
|
+
*
|
|
7
|
+
* @param {URLSearchParams} searchParams - The URLSearchParams object to parse
|
|
8
|
+
* @returns {Record<string, string|string[]>} Query object with string values or arrays for multiple values
|
|
9
|
+
* @public
|
|
10
|
+
*/
|
|
11
|
+
function parseSearchParamsToQuery(searchParams) {
|
|
12
|
+
const query = {};
|
|
13
|
+
for (const [key, value] of searchParams) if (query[key] !== void 0) query[key] = [].concat(query[key], value);
|
|
14
|
+
else query[key] = value;
|
|
15
|
+
return query;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Convert a query object to a URL query string.
|
|
19
|
+
* Handles arrays by appending multiple parameters with the same key.
|
|
20
|
+
*
|
|
21
|
+
* @param {Record<string, string|string[]|undefined|null>} query - Query object to stringify
|
|
22
|
+
* @returns {string} URL-encoded query string (without leading '?')
|
|
23
|
+
* @public
|
|
24
|
+
*/
|
|
25
|
+
function stringifyQueryToString(query) {
|
|
26
|
+
if (!query) return "";
|
|
27
|
+
const params = new URLSearchParams();
|
|
28
|
+
for (const [key, value] of Object.entries(query)) if (Array.isArray(value)) value.forEach((v) => params.append(key, v ?? ""));
|
|
29
|
+
else params.append(key, value ?? "");
|
|
30
|
+
return params.toString();
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Mapping of HTTP status codes to their standard reason phrases.
|
|
34
|
+
*
|
|
35
|
+
* @type {Record<number, string>}
|
|
36
|
+
* @public
|
|
37
|
+
*/
|
|
38
|
+
const statusTextMapping = {
|
|
39
|
+
100: "Continue",
|
|
40
|
+
101: "Switching Protocols",
|
|
41
|
+
102: "Processing",
|
|
42
|
+
103: "Early Hints",
|
|
43
|
+
200: "OK",
|
|
44
|
+
201: "Created",
|
|
45
|
+
202: "Accepted",
|
|
46
|
+
203: "Non-Authoritative Information",
|
|
47
|
+
204: "No Content",
|
|
48
|
+
205: "Reset Content",
|
|
49
|
+
206: "Partial Content",
|
|
50
|
+
207: "Multi-Status",
|
|
51
|
+
208: "Already Reported",
|
|
52
|
+
226: "IM Used",
|
|
53
|
+
300: "Multiple Choices",
|
|
54
|
+
301: "Moved Permanently",
|
|
55
|
+
302: "Found",
|
|
56
|
+
303: "See Other",
|
|
57
|
+
304: "Not Modified",
|
|
58
|
+
305: "Use Proxy",
|
|
59
|
+
307: "Temporary Redirect",
|
|
60
|
+
308: "Permanent Redirect",
|
|
61
|
+
400: "Bad Request",
|
|
62
|
+
401: "Unauthorized",
|
|
63
|
+
402: "Payment Required",
|
|
64
|
+
403: "Forbidden",
|
|
65
|
+
404: "Not Found",
|
|
66
|
+
405: "Method Not Allowed",
|
|
67
|
+
406: "Not Acceptable",
|
|
68
|
+
407: "Proxy Authentication Required",
|
|
69
|
+
408: "Request Timeout",
|
|
70
|
+
409: "Conflict",
|
|
71
|
+
410: "Gone",
|
|
72
|
+
411: "Length Required",
|
|
73
|
+
412: "Precondition Failed",
|
|
74
|
+
413: "Payload Too Large",
|
|
75
|
+
414: "URI Too Long",
|
|
76
|
+
415: "Unsupported Media Type",
|
|
77
|
+
416: "Range Not Satisfiable",
|
|
78
|
+
417: "Expectation Failed",
|
|
79
|
+
418: "I'm a Teapot",
|
|
80
|
+
421: "Misdirected Request",
|
|
81
|
+
422: "Unprocessable Entity",
|
|
82
|
+
423: "Locked",
|
|
83
|
+
424: "Failed Dependency",
|
|
84
|
+
425: "Too Early",
|
|
85
|
+
426: "Upgrade Required",
|
|
86
|
+
428: "Precondition Required",
|
|
87
|
+
429: "Too Many Requests",
|
|
88
|
+
431: "Request Header Fields Too Large",
|
|
89
|
+
451: "Unavailable For Legal Reasons",
|
|
90
|
+
500: "Internal Server Error",
|
|
91
|
+
501: "Not Implemented",
|
|
92
|
+
502: "Bad Gateway",
|
|
93
|
+
503: "Service Unavailable",
|
|
94
|
+
504: "Gateway Timeout",
|
|
95
|
+
505: "HTTP Version Not Supported",
|
|
96
|
+
506: "Variant Also Negotiates",
|
|
97
|
+
507: "Insufficient Storage",
|
|
98
|
+
508: "Loop Detected",
|
|
99
|
+
509: "Bandwidth Limit Exceeded",
|
|
100
|
+
510: "Not Extended",
|
|
101
|
+
511: "Network Authentication Required"
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Mapping of HTTP status codes that indicate redirects.
|
|
105
|
+
* Used to determine if a response should trigger a redirect.
|
|
106
|
+
*
|
|
107
|
+
* @type {Record<number, boolean>}
|
|
108
|
+
* @readonly
|
|
109
|
+
* @public
|
|
110
|
+
*/
|
|
111
|
+
const statusRedirectMapping = {
|
|
112
|
+
300: true,
|
|
113
|
+
301: true,
|
|
114
|
+
302: true,
|
|
115
|
+
303: true,
|
|
116
|
+
305: true,
|
|
117
|
+
307: true,
|
|
118
|
+
308: true
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Mapping of HTTP status codes that should have empty response bodies.
|
|
122
|
+
* These status codes by specification should not include a message body.
|
|
123
|
+
*
|
|
124
|
+
* @type {Record<number, boolean>}
|
|
125
|
+
* @readonly
|
|
126
|
+
* @public
|
|
127
|
+
*/
|
|
128
|
+
const statusEmptyMapping = {
|
|
129
|
+
204: true,
|
|
130
|
+
205: true,
|
|
131
|
+
304: true
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* Mapping of common content type aliases to their full MIME types.
|
|
135
|
+
* Provides convenient shortcuts for setting response content types.
|
|
136
|
+
*
|
|
137
|
+
* @type {Record<string, string>}
|
|
138
|
+
* @readonly
|
|
139
|
+
* @public
|
|
140
|
+
*/
|
|
141
|
+
const commonTypeMapping = {
|
|
142
|
+
html: "text/html;charset=UTF-8",
|
|
143
|
+
text: "text/plain;charset=UTF-8",
|
|
144
|
+
xml: "text/xml;charset=UTF-8",
|
|
145
|
+
md: "text/markdown;charset=UTF-8",
|
|
146
|
+
json: "application/json",
|
|
147
|
+
form: "application/x-www-form-urlencoded;charset=UTF-8",
|
|
148
|
+
pdf: "application/pdf",
|
|
149
|
+
zip: "application/zip",
|
|
150
|
+
wasm: "application/wasm",
|
|
151
|
+
webmanifest: "application/manifest+json",
|
|
152
|
+
js: "application/javascript;charset=UTF-8",
|
|
153
|
+
ts: "application/typescript;charset=UTF-8",
|
|
154
|
+
png: "image/png",
|
|
155
|
+
jpg: "image/jpeg",
|
|
156
|
+
jpeg: "image/jpeg",
|
|
157
|
+
gif: "image/gif",
|
|
158
|
+
svg: "image/svg+xml",
|
|
159
|
+
webp: "image/webp",
|
|
160
|
+
avif: "image/avif",
|
|
161
|
+
ico: "image/x-icon",
|
|
162
|
+
mp3: "audio/mpeg",
|
|
163
|
+
wav: "audio/wav",
|
|
164
|
+
ogg: "audio/ogg",
|
|
165
|
+
mp4: "video/mp4",
|
|
166
|
+
webm: "video/webm",
|
|
167
|
+
avi: "video/x-msvideo",
|
|
168
|
+
mov: "video/quicktime",
|
|
169
|
+
woff: "font/woff",
|
|
170
|
+
woff2: "font/woff2",
|
|
171
|
+
ttf: "font/ttf",
|
|
172
|
+
otf: "font/otf",
|
|
173
|
+
bin: "application/octet-stream"
|
|
174
|
+
};
|
|
175
|
+
const ENCODE_CHARS_REGEXP = /(?:[^\x21\x23-\x3B\x3D\x3F-\x5F\x61-\x7A\x7C\x7E]|%(?:[^0-9A-Fa-f]|[0-9A-Fa-f][^0-9A-Fa-f]|$))+/g;
|
|
176
|
+
/**
|
|
177
|
+
* RegExp to match unmatched surrogate pair.
|
|
178
|
+
* @private
|
|
179
|
+
*/
|
|
180
|
+
const UNMATCHED_SURROGATE_PAIR_REGEXP = /(^|[^\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uD800-\uDBFF]([^\uDC00-\uDFFF]|$)/g;
|
|
181
|
+
/**
|
|
182
|
+
* String to replace unmatched surrogate pair with.
|
|
183
|
+
* @private
|
|
184
|
+
*/
|
|
185
|
+
const UNMATCHED_SURROGATE_PAIR_REPLACE = "$1�$2";
|
|
186
|
+
/**
|
|
187
|
+
* Encode a URL to a percent-encoded form, excluding already-encoded sequences.
|
|
188
|
+
*
|
|
189
|
+
* This function will take an already-encoded URL and encode all the non-URL
|
|
190
|
+
* code points. This function will not encode the "%" character unless it is
|
|
191
|
+
* not part of a valid sequence (`%20` will be left as-is, but `%foo` will
|
|
192
|
+
* be encoded as `%25foo`).
|
|
193
|
+
*
|
|
194
|
+
* This encode is meant to be "safe" and does not throw errors. It will try as
|
|
195
|
+
* hard as it can to properly encode the given URL, including replacing any raw,
|
|
196
|
+
* unpaired surrogate pairs with the Unicode replacement character prior to
|
|
197
|
+
* encoding.
|
|
198
|
+
*
|
|
199
|
+
* @param {string} url - URL string to encode
|
|
200
|
+
* @return {string} Encoded URL string
|
|
201
|
+
* @public
|
|
202
|
+
*/
|
|
203
|
+
function encodeUrl(url) {
|
|
204
|
+
return String(url).replace(UNMATCHED_SURROGATE_PAIR_REGEXP, UNMATCHED_SURROGATE_PAIR_REPLACE).replace(ENCODE_CHARS_REGEXP, encodeURI);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
//#endregion
|
|
208
|
+
exports.commonTypeMapping = commonTypeMapping;
|
|
209
|
+
exports.encodeUrl = encodeUrl;
|
|
210
|
+
exports.parseSearchParamsToQuery = parseSearchParamsToQuery;
|
|
211
|
+
exports.statusEmptyMapping = statusEmptyMapping;
|
|
212
|
+
exports.statusRedirectMapping = statusRedirectMapping;
|
|
213
|
+
exports.statusTextMapping = statusTextMapping;
|
|
214
|
+
exports.stringifyQueryToString = stringifyQueryToString;
|