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.
@@ -1,379 +0,0 @@
1
- var __defProp = Object.defineProperty;
2
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
- var __getOwnPropNames = Object.getOwnPropertyNames;
4
- var __hasOwnProp = Object.prototype.hasOwnProperty;
5
- var __export = (target, all) => {
6
- for (var name in all)
7
- __defProp(target, name, { get: all[name], enumerable: true });
8
- };
9
- var __copyProps = (to, from, except, desc) => {
10
- if (from && typeof from === "object" || typeof from === "function") {
11
- for (let key of __getOwnPropNames(from))
12
- if (!__hasOwnProp.call(to, key) && key !== except)
13
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
- }
15
- return to;
16
- };
17
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
- var response_exports = {};
19
- __export(response_exports, {
20
- default: () => HoaResponse
21
- });
22
- module.exports = __toCommonJS(response_exports);
23
- var import_utils = require("./lib/utils.js");
24
- class HoaResponse {
25
- /**
26
- * Expose a plain object snapshot of response headers.
27
- * Uses the Web Standard Headers API internally for proper header handling.
28
- * Returns a plain object representation where header names are normalized.
29
- *
30
- * @returns {Record<string, string>} Object with all response headers
31
- * @public
32
- */
33
- get headers() {
34
- this._headers = this._headers || new Headers();
35
- return Object.fromEntries(this._headers.entries());
36
- }
37
- /**
38
- * Set the response headers.
39
- * Accepts either a Headers object or a plain object/array of header entries.
40
- * This replaces all existing headers with the new ones.
41
- *
42
- * @param {Headers|Record<string, string>|Array<[string, string]>} val - Headers to set
43
- * @public
44
- */
45
- set headers(val) {
46
- if (val instanceof Headers) {
47
- this._headers = val;
48
- } else {
49
- this._headers = new Headers(val);
50
- }
51
- }
52
- /**
53
- * Get a response header value by name (case-insensitive).
54
- * Uses the Web Standard Headers API for proper header retrieval.
55
- * Special-cases "referer/referrer" for compatibility.
56
- * Returns null for empty or whitespace-only field names.
57
- *
58
- * @param {string} field - The header name
59
- * @returns {string|null} The header value or null if not found
60
- * @public
61
- */
62
- get(field) {
63
- if (!field) return null;
64
- this._headers = this._headers || new Headers();
65
- switch (field = field.toLowerCase()) {
66
- case "referer":
67
- case "referrer": {
68
- return this._headers.get("referrer") ?? this._headers.get("referer");
69
- }
70
- default:
71
- return this._headers.get(field);
72
- }
73
- }
74
- /**
75
- * Get all Set-Cookie header values as an array.
76
- * Returns all Set-Cookie headers that will be sent with the response.
77
- *
78
- * @returns {string[]} Array of Set-Cookie header values
79
- * @public
80
- */
81
- getSetCookie() {
82
- this._headers = this._headers || new Headers();
83
- return this._headers.getSetCookie();
84
- }
85
- /**
86
- * Check if a response header is present.
87
- * Uses the Web Standard Headers API for case-insensitive header checking.
88
- *
89
- * @param {string} field - The header name to check
90
- * @returns {boolean} True if the header exists
91
- * @public
92
- */
93
- has(field) {
94
- if (!field) return false;
95
- this._headers = this._headers || new Headers();
96
- return this._headers.has(field);
97
- }
98
- /**
99
- * Set response header(s) using the Web Standard Headers API.
100
- * Accepts various input formats:
101
- * - Single key/value pair
102
- * - Plain object with multiple headers
103
- * Replaces existing header values.
104
- *
105
- * @param {string|Record<string,string>} field - Header name or headers object
106
- * @param {string} [val] - Header value when field is a string
107
- * @public
108
- */
109
- set(field, val) {
110
- if (!field) return;
111
- this._headers = this._headers || new Headers();
112
- if (typeof field === "string") {
113
- this._headers.set(field, val);
114
- } else {
115
- Object.keys(field).forEach((header) => this._headers.set(header, field[header]));
116
- }
117
- }
118
- /**
119
- * Append a response header value using the Web Standard Headers API.
120
- * Does not replace existing values, but appends to them.
121
- * This is useful for headers that can have multiple values like Set-Cookie.
122
- *
123
- * @param {string|Record<string,string>} field - The header name or headers object
124
- * @param {string} [val] - The value to append when field is a string
125
- * @public
126
- */
127
- append(field, val) {
128
- if (!field) return;
129
- this._headers = this._headers || new Headers();
130
- if (typeof field === "string") {
131
- this._headers.append(field, val);
132
- } else {
133
- Object.keys(field).forEach((header) => this._headers.append(header, field[header]));
134
- }
135
- }
136
- /**
137
- * Delete a response header by name using the Web Standard Headers API.
138
- * Header deletion is case-insensitive.
139
- *
140
- * @param {string} field - The header name to delete
141
- * @public
142
- */
143
- delete(field) {
144
- if (!field) return;
145
- this._headers = this._headers || new Headers();
146
- this._headers.delete(field);
147
- }
148
- /**
149
- * Get the response status code.
150
- * Defaults to 404 if not explicitly set.
151
- *
152
- * @returns {number} The HTTP status code
153
- * @public
154
- */
155
- get status() {
156
- return this._status || 404;
157
- }
158
- /**
159
- * Set the response status code.
160
- * Automatically clears body for status codes that should not have content.
161
- *
162
- * @param {number} val - The HTTP status code (100-599)
163
- * @throws {TypeError}
164
- * @public
165
- */
166
- set status(val) {
167
- if (!Number.isInteger(val)) {
168
- throw new TypeError("status code must be an integer");
169
- }
170
- if (val < 100 || val > 1e3) {
171
- throw new TypeError(`invalid status code: ${val}`);
172
- }
173
- this._status = val;
174
- this._explicitStatus = true;
175
- if (!this._explicitStatusText) {
176
- this._statusText = import_utils.statusTextMapping[val];
177
- }
178
- if (this.body && import_utils.statusEmptyMapping[val]) this.body = null;
179
- }
180
- /**
181
- * Get the response status text.
182
- *
183
- * @returns {string} The statusText (e.g., 'OK', 'Not Found')
184
- * @public
185
- */
186
- get statusText() {
187
- if (this._explicitStatusText) {
188
- return this._statusText;
189
- }
190
- return this._statusText || (this._statusText = import_utils.statusTextMapping[this.status]);
191
- }
192
- /**
193
- * Set a custom response status text.
194
- *
195
- * @param {string} val - The custom status text
196
- * @public
197
- */
198
- set statusText(val) {
199
- this._statusText = val;
200
- this._explicitStatusText = true;
201
- }
202
- /**
203
- * Get the response body.
204
- *
205
- * @returns {any} The response body content
206
- * @public
207
- */
208
- get body() {
209
- return this._body;
210
- }
211
- /**
212
- * Set response body with automatic content-type detection.
213
- * Supports various body types and automatically sets appropriate headers.
214
- * When set to null/undefined, if current Content-Type is application/json,
215
- * body becomes the literal string 'null'; otherwise status is set to 204 and
216
- * Content-Type/Transfer-Encoding are removed.
217
- *
218
- * @param {string|Object|ReadableStream|Blob|Response|ArrayBuffer|TypedArray|FormData|URLSearchParams|null} val - The response body
219
- * @public
220
- */
221
- set body(val) {
222
- this._body = val;
223
- if (val == null) {
224
- if (!import_utils.statusEmptyMapping[this.status]) {
225
- if (this.type === "application/json") {
226
- this._body = "null";
227
- return;
228
- }
229
- this.status = 204;
230
- }
231
- if (val === null) this._explicitNullBody = true;
232
- this.delete("Content-Type");
233
- this.delete("Content-Length");
234
- this.delete("Transfer-Encoding");
235
- return;
236
- }
237
- if (!this._explicitStatus) this.status = 200;
238
- const noType = !this.has("Content-Type");
239
- if (typeof val === "string") {
240
- if (noType) this.type = /^\s*</.test(val) ? "html" : "text";
241
- return;
242
- }
243
- if (val instanceof Blob || val instanceof ArrayBuffer || ArrayBuffer.isView(val) || val instanceof ReadableStream) {
244
- if (noType) {
245
- if (val instanceof Blob && val.type) {
246
- this.set("Content-Type", val.type);
247
- } else {
248
- this.type = "bin";
249
- }
250
- }
251
- return;
252
- }
253
- if (val instanceof FormData) {
254
- return;
255
- }
256
- if (val instanceof URLSearchParams) {
257
- if (noType) this.type = "form";
258
- return;
259
- }
260
- if (val instanceof Response) {
261
- if (noType) this.type = "bin";
262
- this.status = val.status;
263
- for (const [k, v] of val.headers) this.set(k, v);
264
- return;
265
- }
266
- if (!this.type || !/\bjson\b/i.test(this.type)) this.type = "json";
267
- }
268
- /**
269
- * Perform an HTTP redirect to the specified URL.
270
- * Automatically sets the Location header and appropriate status code.
271
- * Absolute URLs are normalized and Location value is URL-encoded.
272
- *
273
- * @param {string} url - The URL to redirect to (absolute or relative)
274
- * @public
275
- */
276
- redirect(url) {
277
- if (/^https?:\/\//i.test(url)) {
278
- url = new URL(url).toString();
279
- }
280
- this.set("Location", (0, import_utils.encodeUrl)(url));
281
- if (!import_utils.statusRedirectMapping[this.status]) this.status = 302;
282
- this.type = "text";
283
- this.body = `Redirecting to ${url}.`;
284
- }
285
- /**
286
- * Perform a special-cased "back" redirect using the Referrer header.
287
- * When Referrer is not present or unsafe, falls back to the provided alternative or "/".
288
- * Only redirects to same-origin referrers for security.
289
- * If referrer is an absolute URL with different origin or an invalid URL, falls back.
290
- *
291
- * @param {string} [alt='/'] - Alternative URL when referrer is unavailable or unsafe
292
- * @public
293
- */
294
- back(alt) {
295
- const referrer = this.req.get("Referrer");
296
- if (referrer) {
297
- if (referrer.startsWith("/")) {
298
- this.redirect(referrer);
299
- return;
300
- }
301
- const url = new URL(referrer, this.req.href);
302
- if (url.origin === this.req.origin) {
303
- this.redirect(referrer);
304
- return;
305
- }
306
- }
307
- this.redirect(alt || "/");
308
- }
309
- /**
310
- * Get the response Content-Type without parameters.
311
- * Returns only the media type without parameters (e.g., 'application/json').
312
- *
313
- * @returns {string|null} The content type, or null if not set
314
- * @public
315
- */
316
- get type() {
317
- const type = this.get("Content-Type");
318
- if (!type) return null;
319
- return type.split(";", 1)[0];
320
- }
321
- /**
322
- * Set the response Content-Type.
323
- * Supports both full MIME types and shorthand aliases.
324
- *
325
- * @param {string} type - The content type or alias (e.g., 'json', 'html', 'application/json')
326
- * @public
327
- */
328
- set type(type) {
329
- if (!type) return;
330
- type = import_utils.commonTypeMapping[type] || type;
331
- this.set("Content-Type", type);
332
- }
333
- /**
334
- * Get the response Content-Length as a number.
335
- * Returns the parsed Content-Length header value, or calculates it from the body if available.
336
- *
337
- * @returns {number|null} The content length in bytes, or null if not determinable
338
- * @public
339
- */
340
- get length() {
341
- if (this.has("Content-Length")) {
342
- return Number.parseInt(this.get("Content-Length"), 10) || 0;
343
- }
344
- if (this.body == null || this.body instanceof ReadableStream || this.body instanceof FormData || this.body instanceof Response) {
345
- return null;
346
- }
347
- if (typeof this.body === "string") return new TextEncoder().encode(this.body).length;
348
- if (this.body instanceof Blob) return this.body.size;
349
- if (this.body instanceof ArrayBuffer) return this.body.byteLength;
350
- if (ArrayBuffer.isView(this.body)) return this.body.byteLength;
351
- if (this.body instanceof URLSearchParams) return new TextEncoder().encode(this.body.toString()).length;
352
- return new TextEncoder().encode(JSON.stringify(this.body)).length;
353
- }
354
- /**
355
- * Set the response Content-Length header.
356
- * Only sets the header if Transfer-Encoding is not present.
357
- *
358
- * @param {number|string} val - The content length in bytes
359
- * @public
360
- */
361
- set length(val) {
362
- if (!this.has("Transfer-Encoding")) {
363
- this.set("Content-Length", val);
364
- }
365
- }
366
- /**
367
- * Return JSON representation of the response.
368
- *
369
- * @returns {ResJSON} JSON representation of response
370
- * @public
371
- */
372
- toJSON() {
373
- return {
374
- status: this.status,
375
- statusText: this.statusText,
376
- headers: this.headers
377
- };
378
- }
379
- }
@@ -1,148 +0,0 @@
1
- import HttpError from "./lib/http-error.js";
2
- import { statusTextMapping, statusEmptyMapping } from "./lib/utils.js";
3
- class HoaContext {
4
- /**
5
- * Create a context for a single HTTP request.
6
- *
7
- * @param {Object} [options={}]
8
- * @param {Request} [options.request] - Web Standard Request
9
- * @param {any} [options.env] - Environment (platform-specific)
10
- * @param {any} [options.executionCtx] - Execution context (platform-specific)
11
- * @public
12
- */
13
- constructor(options = {}) {
14
- this.request = options.request;
15
- this.env = options.env;
16
- this.executionCtx = options.executionCtx;
17
- this.state = /* @__PURE__ */ Object.create(null);
18
- }
19
- /**
20
- * Throw an HttpError.
21
- *
22
- * @param {number} status - HTTP status code
23
- * @param {string|{message?: string, cause?: any, headers?: HeadersInit}} [messageOrOptions] - Error message or options object
24
- * @throws {HttpError}
25
- * @public
26
- */
27
- throw(...args) {
28
- throw new HttpError(...args);
29
- }
30
- /**
31
- * Assert condition or throw an HttpError.
32
- *
33
- * @param {any} value - Condition to assert
34
- * @param {...any} args - Arguments passed to HttpError constructor
35
- * @throws {HttpError}
36
- * @public
37
- */
38
- assert(value, ...args) {
39
- if (value) return;
40
- throw new HttpError(...args);
41
- }
42
- /**
43
- * Default error handling and response builder.
44
- *
45
- * @param {Error} err - Error to handle
46
- * @returns {Response} Web Standard Response object
47
- * @private
48
- */
49
- onerror(err) {
50
- const { res } = this;
51
- const isNativeError = Object.prototype.toString.call(err) === "[object Error]" || err instanceof Error;
52
- if (!isNativeError) err = new Error(`non-error thrown: ${JSON.stringify(err)}`);
53
- this.app.onerror(err, this);
54
- res.headers = new Headers();
55
- res.set(err.headers);
56
- res.type = "text";
57
- let status = err.status || err.statusCode;
58
- if (typeof status !== "number" || !statusTextMapping[status]) status = 500;
59
- const message = statusTextMapping[status];
60
- const msg = err.expose ? err.message : message;
61
- res.status = status;
62
- res.body = msg;
63
- return new Response(res.body, {
64
- status: res.status,
65
- headers: res._headers
66
- });
67
- }
68
- /**
69
- * Build Web Standard Response from current context state.
70
- * Handles various body types, HEAD requests, and status-specific behaviors.
71
- *
72
- * @returns {Response} Web Standard Response object
73
- * @public
74
- */
75
- get response() {
76
- const { res, req } = this;
77
- let body = res.body;
78
- if (req.method === "HEAD") {
79
- if (!res.has("Content-Length")) {
80
- const contentLength = res.length;
81
- if (Number.isInteger(contentLength)) {
82
- res.length = contentLength;
83
- }
84
- }
85
- return new Response(null, {
86
- status: res.status,
87
- statusText: res.statusText,
88
- headers: res._headers
89
- });
90
- }
91
- if (statusEmptyMapping[res.status]) {
92
- res.body = null;
93
- return new Response(null, {
94
- status: res.status,
95
- statusText: res.statusText,
96
- headers: res._headers
97
- });
98
- }
99
- if (body == null) {
100
- if (res._explicitNullBody) {
101
- res.delete("Content-Type");
102
- res.delete("Transfer-Encoding");
103
- res.set("Content-Length", "0");
104
- }
105
- return new Response(null, {
106
- status: res.status,
107
- statusText: res.statusText,
108
- headers: res._headers
109
- });
110
- }
111
- if (typeof body === "string" || body instanceof Blob || body instanceof ArrayBuffer || ArrayBuffer.isView(body) || body instanceof ReadableStream || body instanceof FormData || body instanceof URLSearchParams) {
112
- return new Response(body, {
113
- status: res.status,
114
- statusText: res.statusText,
115
- headers: res._headers
116
- });
117
- }
118
- if (body instanceof Response) {
119
- return new Response(body.body, {
120
- status: res.status,
121
- statusText: res.statusText,
122
- headers: res._headers
123
- });
124
- }
125
- body = JSON.stringify(body);
126
- return new Response(body, {
127
- status: res.status,
128
- statusText: res.statusText,
129
- headers: res._headers
130
- });
131
- }
132
- /**
133
- * Return JSON representation of the context.
134
- *
135
- * @returns {CtxJSON} JSON representation of context
136
- * @public
137
- */
138
- toJSON() {
139
- return {
140
- app: this.app.toJSON(),
141
- req: this.req.toJSON(),
142
- res: this.res.toJSON()
143
- };
144
- }
145
- }
146
- export {
147
- HoaContext as default
148
- };
package/dist/esm/hoa.js DELETED
@@ -1,151 +0,0 @@
1
- import compose from "./lib/compose.js";
2
- import HttpError from "./lib/http-error.js";
3
- import { statusTextMapping, statusRedirectMapping, statusEmptyMapping } from "./lib/utils.js";
4
- import HoaContext from "./context.js";
5
- import HoaRequest from "./request.js";
6
- import HoaResponse from "./response.js";
7
- class Hoa {
8
- /**
9
- * Create an Hoa instance.
10
- *
11
- * @param {Object} [options={}] - Application options
12
- * @param {string} [options.name='Hoa'] - Application name for identification
13
- */
14
- constructor(options = {}) {
15
- this.name = options.name || "Hoa";
16
- this.HoaContext = HoaContext;
17
- this.HoaRequest = HoaRequest;
18
- this.HoaResponse = HoaResponse;
19
- this.middlewares = [];
20
- this.fetch = this.fetch.bind(this);
21
- }
22
- /**
23
- * Extend the application with a plugin initializer.
24
- *
25
- * @param {HoaExtension} fn - Plugin function that receives the app instance
26
- * @returns {Hoa} The Hoa instance for method chaining
27
- * @throws {TypeError}
28
- * @public
29
- */
30
- extend(fn) {
31
- if (typeof fn !== "function") {
32
- throw new TypeError("extend() must receive a function!");
33
- }
34
- fn(this);
35
- return this;
36
- }
37
- /**
38
- * Register a middleware. Executed in registration order.
39
- *
40
- * @param {HoaMiddleware} fn - Middleware function
41
- * @returns {Hoa} The Hoa instance for method chaining
42
- * @throws {TypeError}
43
- * @public
44
- */
45
- use(fn) {
46
- if (typeof fn !== "function") {
47
- throw new TypeError("use() must receive a function!");
48
- }
49
- this.middlewares.push(fn);
50
- this._composedMiddleware = null;
51
- return this;
52
- }
53
- /**
54
- * Web Standards fetch handler - main entry point for HTTP requests.
55
- * Compatible with Cloudflare Workers, Deno, and other Web Standards environments.
56
- *
57
- * @param {Request} request - Web Standard Request object
58
- * @param {any} [env] - Environment variables (platform-specific)
59
- * @param {any} [executionCtx] - Execution context (platform-specific)
60
- * @returns {Promise<Response>} Web Standard Response object
61
- * @public
62
- */
63
- fetch(request, env, executionCtx) {
64
- const ctx = this.createContext(request, env, executionCtx);
65
- if (!this._composedMiddleware) this._composedMiddleware = compose(this.middlewares);
66
- return this.handleRequest(ctx, this._composedMiddleware);
67
- }
68
- /**
69
- * Handle incoming request through the middleware stack.
70
- * Manages error handling and response building.
71
- *
72
- * @param {HoaContext} ctx - Request context
73
- * @param {HoaMiddleware} middlewareFn - Composed middleware function
74
- * @returns {Promise<Response>} Web Standard Response object
75
- * @private
76
- */
77
- handleRequest(ctx, middlewareFn) {
78
- return middlewareFn(ctx).then(() => ctx.response).catch((err) => ctx.onerror(err));
79
- }
80
- /**
81
- * Create context for incoming request with linked request/response objects.
82
- * Establishes the context chain: ctx ↔ req ↔ res ↔ app
83
- *
84
- * @param {Request} request - Web Standard Request object
85
- * @param {any} [env] - Environment variables
86
- * @param {any} [executionCtx] - Execution context
87
- * @returns {HoaContext} Created context instance
88
- * @private
89
- */
90
- createContext(request, env, executionCtx) {
91
- const ctx = new this.HoaContext({ request, env, executionCtx });
92
- const req = ctx.req = new this.HoaRequest();
93
- const res = ctx.res = new this.HoaResponse();
94
- ctx.app = req.app = res.app = this;
95
- req.ctx = res.ctx = ctx;
96
- req.res = res;
97
- res.req = req;
98
- return ctx;
99
- }
100
- /**
101
- * Default error handler for unhandled application errors.
102
- * Logs errors to console unless they're client errors (4xx) or explicitly exposed.
103
- *
104
- * @param {Error} err - Error to handle
105
- * @param {HoaContext} [ctx] - Request context (optional)
106
- * @returns {void}
107
- * @throws {TypeError}
108
- * @private
109
- */
110
- onerror(err, ctx) {
111
- const isNativeError = Object.prototype.toString.call(err) === "[object Error]" || err instanceof Error;
112
- if (!isNativeError) {
113
- throw new TypeError(`non-error thrown: ${JSON.stringify(err)}`);
114
- }
115
- if (err.status === 404 || err.expose) return;
116
- if (this.silent) return;
117
- console.error(err);
118
- }
119
- /**
120
- * ESM/CJS interop helper for default exports.
121
- *
122
- * @returns {typeof Hoa} The Hoa class
123
- * @static
124
- */
125
- static get default() {
126
- return Hoa;
127
- }
128
- /**
129
- * Return JSON representation of the app.
130
- *
131
- * @returns {AppJSON} JSON representation of application
132
- * @public
133
- */
134
- toJSON() {
135
- return {
136
- name: this.name
137
- };
138
- }
139
- }
140
- export {
141
- Hoa,
142
- HoaContext,
143
- HoaRequest,
144
- HoaResponse,
145
- HttpError,
146
- compose,
147
- Hoa as default,
148
- statusEmptyMapping,
149
- statusRedirectMapping,
150
- statusTextMapping
151
- };
@@ -1,21 +0,0 @@
1
- const composeSlim = (middlewares) => async (ctx, next) => {
2
- const dispatch = (i) => async () => {
3
- const fn = i === middlewares.length ? next : middlewares[i];
4
- if (!fn) return;
5
- return await fn(ctx, dispatch(i + 1));
6
- };
7
- return dispatch(0)();
8
- };
9
- function compose(middlewares) {
10
- if (!Array.isArray(middlewares)) {
11
- throw new TypeError("compose() must receive an array of middleware functions!");
12
- }
13
- middlewares = middlewares.flat();
14
- for (const middleware of middlewares) {
15
- if (typeof middleware !== "function") throw new TypeError("Middleware must be composed of functions!");
16
- }
17
- return composeSlim(middlewares);
18
- }
19
- export {
20
- compose as default
21
- };