hoa 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ v0.1.0 / 2025-09-25
2
+
3
+ - init
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 - present, Hoa contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,49 @@
1
+ ## Hoa
2
+
3
+ Hoa is a minimal Web framework inspired by [Koa](https://github.com/koajs/koa) and [Hono](https://github.com/honojs/hono), built entirely on Web Standards. It runs seamlessly on any modern JavaScript runtime: Cloudflare Workers, Fastly Compute, Deno, Bun, Vercel, AWS Lambda, Lambda@Edge, and Node.js.
4
+
5
+ ## Features
6
+
7
+ - ⚡ Minimal - Only ~4.4KB (gzipped).
8
+ - 🚫 Zero Dependencies - Built on modern Web Standards with no external dependencies.
9
+ - 🛠️ Highly Extensible - Features a flexible extension and middleware system.
10
+ - 😊 Standards-Based - Designed entirely around modern Web Standard APIs.
11
+ - 🌐 Multi-Runtime - The same code runs on Cloudflare Workers, Deno, Bun, Node.js, and more.
12
+ - ✅ 100% Tested – Backed by a full-coverage automated test suite.
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm i hoa --save
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ```js
23
+ import { Hoa } from 'hoa'
24
+ const app = new Hoa()
25
+
26
+ app.use(async (ctx, next) => {
27
+ ctx.res.body = 'Hello, Hoa!'
28
+ })
29
+
30
+ export default app
31
+ ```
32
+
33
+ ## Documentation
34
+
35
+ The documentation is available on [hoa-js.com](https://hoa-js.com)
36
+
37
+ ## Contributing
38
+
39
+ Contributions Welcome! You can contribute in the following ways.
40
+
41
+ - Create an Issue - Propose a new feature. Report a bug.
42
+ - Pull Request - Fix a bug and typo. Refactor the code.
43
+ - Create third-party middleware.
44
+ - Share your thoughts on the Blog, X, and others.
45
+ - Make your application.
46
+
47
+ ## License
48
+
49
+ Distributed under the MIT License.
@@ -0,0 +1,245 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
24
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
+ mod
26
+ ));
27
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
+ var application_exports = {};
29
+ __export(application_exports, {
30
+ Hoa: () => Application,
31
+ HoaContext: () => import_context.default,
32
+ HoaRequest: () => import_request.default,
33
+ HoaResponse: () => import_response.default,
34
+ HttpError: () => import_http_error.default,
35
+ compose: () => import_compose.default,
36
+ default: () => Application
37
+ });
38
+ module.exports = __toCommonJS(application_exports);
39
+ var import_compose = __toESM(require("./lib/compose.js"), 1);
40
+ var import_http_error = __toESM(require("./lib/http-error.js"), 1);
41
+ var import_utils = require("./lib/utils.js");
42
+ var import_context = __toESM(require("./context.js"), 1);
43
+ var import_request = __toESM(require("./request.js"), 1);
44
+ var import_response = __toESM(require("./response.js"), 1);
45
+ class Application {
46
+ /**
47
+ * Create an Application instance.
48
+ *
49
+ * @param {Object} [options={}] - Application options
50
+ * @param {string} [options.name='Hoa'] - Application name for identification
51
+ */
52
+ constructor(options = {}) {
53
+ this.name = options.name || "Hoa";
54
+ this.HoaContext = import_context.default;
55
+ this.HoaRequest = import_request.default;
56
+ this.HoaResponse = import_response.default;
57
+ this.middlewares = [];
58
+ this.fetch = this.fetch.bind(this);
59
+ }
60
+ /**
61
+ * Extend the application with a plugin initializer.
62
+ * @param {(app: Application) => void} fn - Plugin function that receives the app instance
63
+ * @returns {Application}
64
+ * @throws {TypeError} If fn is not a function
65
+ * @public
66
+ */
67
+ extend(fn) {
68
+ if (typeof fn !== "function") {
69
+ throw new TypeError("extend() must receive a function!");
70
+ }
71
+ fn(this);
72
+ return this;
73
+ }
74
+ /**
75
+ * Register a middleware. Executed in registration order.
76
+ * @param {(ctx: HoaContext, next: () => Promise<void>) => Promise<any | void>} fn - Middleware function
77
+ * @returns {Application}
78
+ * @throws {TypeError} If fn is not a function
79
+ * @public
80
+ */
81
+ use(fn) {
82
+ if (typeof fn !== "function") {
83
+ throw new TypeError("use() must receive a function!");
84
+ }
85
+ this.middlewares.push(fn);
86
+ this._composedMiddleware = null;
87
+ return this;
88
+ }
89
+ /**
90
+ * Web Standards fetch handler - main entry point for HTTP requests.
91
+ * Compatible with Cloudflare Workers, Deno, and other Web Standards environments.
92
+ *
93
+ * @param {Request} request - Web Standard Request object
94
+ * @param {any} [env] - Environment variables (platform-specific)
95
+ * @param {any} [executionCtx] - Execution context (platform-specific)
96
+ * @returns {Promise<Response>}
97
+ * @public
98
+ */
99
+ fetch(request, env, executionCtx) {
100
+ const ctx = this.createContext(request, env, executionCtx);
101
+ if (!this._composedMiddleware) this._composedMiddleware = (0, import_compose.default)(this.middlewares);
102
+ return this.handleRequest(ctx, this._composedMiddleware);
103
+ }
104
+ /**
105
+ * Handle incoming request through the middleware stack.
106
+ * Manages error handling and response building.
107
+ *
108
+ * @param {HoaContext} ctx - Request context
109
+ * @param {(ctx: HoaContext) => Promise<void>} middlewareFn - Composed middleware function
110
+ * @returns {Promise<Response>}
111
+ * @private
112
+ */
113
+ handleRequest(ctx, middlewareFn) {
114
+ const onerror = (err) => ctx.onerror(err);
115
+ const handleResponse = () => respond(ctx);
116
+ return middlewareFn(ctx).then(handleResponse).catch(onerror);
117
+ }
118
+ /**
119
+ * Create context for incoming request with linked request/response objects.
120
+ * Establishes the context chain: ctx ↔ req ↔ res ↔ app
121
+ *
122
+ * @param {Request} request - Web Standard Request object
123
+ * @param {any} [env] - Environment variables
124
+ * @param {any} [executionCtx] - Execution context
125
+ * @returns {HoaContext}
126
+ * @private
127
+ */
128
+ createContext(request, env, executionCtx) {
129
+ const ctx = new this.HoaContext({ request, env, executionCtx });
130
+ const req = ctx.req = new this.HoaRequest();
131
+ const res = ctx.res = new this.HoaResponse();
132
+ ctx.app = req.app = res.app = this;
133
+ req.ctx = res.ctx = ctx;
134
+ req.res = res;
135
+ res.req = req;
136
+ return ctx;
137
+ }
138
+ /**
139
+ * Default error handler for unhandled application errors.
140
+ * Logs errors to console unless they're client errors (4xx) or explicitly exposed.
141
+ *
142
+ * @param {Error} err - Error to handle
143
+ * @param {HoaContext} [ctx] - Request context (optional)
144
+ * @throws {TypeError} When err is not a proper Error object
145
+ * @private
146
+ */
147
+ onerror(err, ctx) {
148
+ const isNativeError = Object.prototype.toString.call(err) === "[object Error]" || err instanceof Error;
149
+ if (!isNativeError) {
150
+ throw new TypeError(`non-error thrown: ${JSON.stringify(err)}`);
151
+ }
152
+ if (err.status === 404 || err.expose) return;
153
+ if (this.silent) return;
154
+ const msg = err.stack || err.toString();
155
+ }
156
+ /**
157
+ * ESM/CJS interop helper for default exports.
158
+ *
159
+ * @returns {typeof Application}
160
+ * @static
161
+ */
162
+ static get default() {
163
+ return Application;
164
+ }
165
+ /**
166
+ * Return JSON representation of the app.
167
+ *
168
+ * @returns {AppJSON}
169
+ * @public
170
+ */
171
+ toJSON() {
172
+ return {
173
+ name: this.name
174
+ };
175
+ }
176
+ }
177
+ function respond(ctx) {
178
+ const { res, req } = ctx;
179
+ let body = res.body;
180
+ if (req.method === "HEAD") {
181
+ if (!res.has("Content-Length")) {
182
+ const contentLength = res.length;
183
+ if (Number.isInteger(contentLength)) {
184
+ res.length = contentLength;
185
+ }
186
+ }
187
+ return new Response(null, {
188
+ status: res.status,
189
+ statusText: res.statusText,
190
+ headers: res._headers
191
+ });
192
+ }
193
+ if (import_utils.statusEmptyMapping[res.status]) {
194
+ res.body = null;
195
+ return new Response(null, {
196
+ status: res.status,
197
+ statusText: res.statusText,
198
+ headers: res._headers
199
+ });
200
+ }
201
+ if (body == null) {
202
+ if (res._explicitNullBody) {
203
+ res.delete("Content-Type");
204
+ res.delete("Transfer-Encoding");
205
+ res.set("Content-Length", "0");
206
+ }
207
+ if (!res._explicitStatus) {
208
+ res.status = 404;
209
+ }
210
+ return new Response(null, {
211
+ status: res.status,
212
+ statusText: res.statusText,
213
+ headers: res._headers
214
+ });
215
+ }
216
+ if (typeof body === "string" || body instanceof Blob || body instanceof ArrayBuffer || ArrayBuffer.isView(body) || body instanceof ReadableStream || body instanceof FormData || body instanceof URLSearchParams) {
217
+ return new Response(body, {
218
+ status: res.status,
219
+ statusText: res.statusText,
220
+ headers: res._headers
221
+ });
222
+ }
223
+ if (body instanceof Response) {
224
+ return new Response(body.body, {
225
+ status: res.status,
226
+ statusText: res.statusText,
227
+ headers: res._headers
228
+ });
229
+ }
230
+ body = JSON.stringify(body);
231
+ return new Response(body, {
232
+ status: res.status,
233
+ statusText: res.statusText,
234
+ headers: res._headers
235
+ });
236
+ }
237
+ // Annotate the CommonJS export names for ESM import in node:
238
+ 0 && (module.exports = {
239
+ Hoa,
240
+ HoaContext,
241
+ HoaRequest,
242
+ HoaResponse,
243
+ HttpError,
244
+ compose
245
+ });
@@ -0,0 +1,111 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
24
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
+ mod
26
+ ));
27
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
+ var context_exports = {};
29
+ __export(context_exports, {
30
+ default: () => HoaContext
31
+ });
32
+ module.exports = __toCommonJS(context_exports);
33
+ var import_http_error = __toESM(require("./lib/http-error.js"), 1);
34
+ var import_utils = require("./lib/utils.js");
35
+ class HoaContext {
36
+ /**
37
+ * Create a context for a single HTTP request.
38
+ *
39
+ * @param {Object} [options={}]
40
+ * @param {Request} [options.request] - Web Standard Request
41
+ * @param {any} [options.env] - Environment (platform-specific)
42
+ * @param {any} [options.executionCtx] - Execution context (platform-specific)
43
+ * @public
44
+ */
45
+ constructor(options = {}) {
46
+ this.request = options.request;
47
+ this.env = options.env;
48
+ this.executionCtx = options.executionCtx;
49
+ this.state = /* @__PURE__ */ Object.create(null);
50
+ }
51
+ /**
52
+ * Throw an HttpError.
53
+ *
54
+ * @param {number} status
55
+ * @param {string|{message?: string, cause?: any, headers?: HeadersInit}} [messageOrOptions]
56
+ * @throws {HttpError}
57
+ * @public
58
+ */
59
+ throw(...args) {
60
+ throw new import_http_error.default(...args);
61
+ }
62
+ /**
63
+ * Assert condition or throw an HttpError.
64
+ *
65
+ * @param {any} value - Condition to assert
66
+ * @param {...any} args - Arguments passed to HttpError constructor
67
+ * @throws {HttpError}
68
+ * @public
69
+ */
70
+ assert(value, ...args) {
71
+ if (value) return;
72
+ throw new import_http_error.default(...args);
73
+ }
74
+ /**
75
+ * Default error handling and response builder.
76
+ * @param {Error} err
77
+ * @returns {Response}
78
+ * @private
79
+ */
80
+ onerror(err) {
81
+ const { res } = this;
82
+ const isNativeError = Object.prototype.toString.call(err) === "[object Error]" || err instanceof Error;
83
+ if (!isNativeError) err = new Error(`non-error thrown: ${JSON.stringify(err)}`);
84
+ this.app.onerror(err, this);
85
+ res.headers = new Headers();
86
+ res.set(err.headers);
87
+ res.type = "text";
88
+ let status = err.status || err.statusCode;
89
+ if (typeof status !== "number" || !import_utils.statusTextMapping[status]) status = 500;
90
+ const message = import_utils.statusTextMapping[status];
91
+ const msg = err.expose ? err.message : message;
92
+ res.status = status;
93
+ res.body = msg;
94
+ return new Response(res.body, {
95
+ status: res.status,
96
+ headers: res._headers
97
+ });
98
+ }
99
+ /**
100
+ * Return JSON representation of the context.
101
+ * @returns {CtxJSON}
102
+ * @public
103
+ */
104
+ toJSON() {
105
+ return {
106
+ app: this.app.toJSON(),
107
+ req: this.req.toJSON(),
108
+ res: this.res.toJSON()
109
+ };
110
+ }
111
+ }
@@ -0,0 +1,40 @@
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 compose_exports = {};
19
+ __export(compose_exports, {
20
+ default: () => compose
21
+ });
22
+ module.exports = __toCommonJS(compose_exports);
23
+ const composeSlim = (middlewares) => async (ctx, next) => {
24
+ const dispatch = (i) => async () => {
25
+ const fn = i === middlewares.length ? next : middlewares[i];
26
+ if (!fn) return;
27
+ return await fn(ctx, dispatch(i + 1));
28
+ };
29
+ return dispatch(0)();
30
+ };
31
+ function compose(middlewares) {
32
+ if (!Array.isArray(middlewares)) {
33
+ throw new TypeError("compose() must receive an array of middleware functions!");
34
+ }
35
+ middlewares = middlewares.flat();
36
+ for (const middleware of middlewares) {
37
+ if (typeof middleware !== "function") throw new TypeError("Middleware must be composed of functions!");
38
+ }
39
+ return composeSlim(middlewares);
40
+ }
@@ -0,0 +1,61 @@
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 http_error_exports = {};
19
+ __export(http_error_exports, {
20
+ default: () => HttpError
21
+ });
22
+ module.exports = __toCommonJS(http_error_exports);
23
+ var import_utils = require("./utils.js");
24
+ class HttpError extends Error {
25
+ /**
26
+ * Create a new HttpError instance.
27
+ *
28
+ * @param {number} status - HTTP status code (400-599, invalid codes become 500)
29
+ * @param {string|HttpErrorOptions} [message] - Error message or options object
30
+ * @param {HttpErrorOptions} [options] - Additional options when second param is string
31
+ * @throws {TypeError} When status is not an integer
32
+ */
33
+ constructor(status, message, options) {
34
+ if (!Number.isInteger(status)) {
35
+ throw new TypeError("status code must be an integer");
36
+ }
37
+ if (status < 400 || status >= 600) {
38
+ status = 500;
39
+ }
40
+ let finalOptions = {};
41
+ if (typeof message === "string") {
42
+ finalOptions.message = message;
43
+ if (options && typeof options === "object") {
44
+ finalOptions = { ...finalOptions, ...options };
45
+ }
46
+ } else if (message && typeof message === "object") {
47
+ finalOptions = message;
48
+ }
49
+ message = finalOptions.message ?? import_utils.statusTextMapping[status] ?? "Unknown error";
50
+ super(message, { cause: finalOptions.cause });
51
+ this.name = "HttpError";
52
+ this.status = this.statusCode = status;
53
+ this.expose = finalOptions.expose ?? status < 500;
54
+ if (finalOptions.headers) {
55
+ this.headers = Object.fromEntries(new Headers(finalOptions.headers).entries());
56
+ }
57
+ if (Error.captureStackTrace) {
58
+ Error.captureStackTrace(this, HttpError);
59
+ }
60
+ }
61
+ }