@kaito-http/core 3.0.0-beta.7 → 3.0.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/dist/index.js CHANGED
@@ -21,205 +21,265 @@ var KaitoError = class extends Error {
21
21
  }
22
22
  };
23
23
 
24
- // src/req.ts
25
- import { TLSSocket } from "node:tls";
26
-
27
- // src/util.ts
28
- import { parse as parseContentType } from "content-type";
29
- import { Readable } from "node:stream";
30
- import { json } from "node:stream/consumers";
31
- import getRawBody from "raw-body";
24
+ // src/handler.ts
25
+ function createKaitoHandler(config) {
26
+ const handle = config.router.freeze(config);
27
+ return async (request) => {
28
+ if (config.before) {
29
+ const result = await config.before(request);
30
+ if (result instanceof Response) {
31
+ if (config.transform) {
32
+ const result2 = await config.transform(request, result);
33
+ if (result2 instanceof Response) return result;
34
+ }
35
+ return result;
36
+ }
37
+ }
38
+ const response = await handle(request);
39
+ if (config.transform) {
40
+ const result = await config.transform(request, response);
41
+ if (result instanceof Response) return result;
42
+ }
43
+ return response;
44
+ };
45
+ }
32
46
 
33
- // src/router.ts
34
- import fmw from "find-my-way";
47
+ // src/request.ts
48
+ var KaitoRequest = class {
49
+ url;
50
+ _request;
51
+ constructor(url, request) {
52
+ this._request = request;
53
+ this.url = url;
54
+ }
55
+ get headers() {
56
+ return this._request.headers;
57
+ }
58
+ get method() {
59
+ return this._request.method;
60
+ }
61
+ async arrayBuffer() {
62
+ return this._request.arrayBuffer();
63
+ }
64
+ async blob() {
65
+ return this._request.blob();
66
+ }
67
+ async formData() {
68
+ return this._request.formData();
69
+ }
70
+ async bytes() {
71
+ const buffer = await this.arrayBuffer();
72
+ return new Uint8Array(buffer);
73
+ }
74
+ async json() {
75
+ return this._request.json();
76
+ }
77
+ async text() {
78
+ return this._request.text();
79
+ }
80
+ get request() {
81
+ return this._request;
82
+ }
83
+ };
35
84
 
36
- // src/res.ts
37
- import { serialize } from "cookie";
38
- var KaitoResponse = class {
39
- constructor(raw) {
40
- this.raw = raw;
85
+ // src/head.ts
86
+ var KaitoHead = class {
87
+ _headers;
88
+ _status;
89
+ constructor() {
90
+ this._headers = null;
91
+ this._status = 200;
41
92
  }
42
- /**
43
- * Send a response
44
- * @param key The key of the header
45
- * @param value The value of the header
46
- * @returns The response object
47
- */
48
- header(key, value) {
49
- this.raw.setHeader(key, value);
50
- return this;
93
+ get headers() {
94
+ if (this._headers === null) {
95
+ this._headers = new Headers();
96
+ }
97
+ return this._headers;
51
98
  }
52
- /**
53
- * Set the status code of the response
54
- * @param code The status code
55
- * @returns The response object
56
- */
57
- status(code) {
58
- this.raw.statusCode = code;
99
+ status(status) {
100
+ if (status === void 0) {
101
+ return this._status;
102
+ }
103
+ this._status = status;
59
104
  return this;
60
105
  }
61
106
  /**
62
- * Set a cookie
63
- * @param name The name of the cookie
64
- * @param value The value of the cookie
65
- * @param options The options for the cookie
66
- * @returns The response object
107
+ * Turn this KaitoHead instance into a Response instance
108
+ * @param body The Kaito JSON format to be sent as the response body
109
+ * @returns A Response instance, ready to be sent
67
110
  */
68
- cookie(name, value, options) {
69
- this.raw.setHeader("Set-Cookie", serialize(name, value, options));
70
- return this;
111
+ toResponse(body) {
112
+ const init = {
113
+ status: this._status
114
+ };
115
+ if (this._headers) {
116
+ init.headers = this._headers;
117
+ }
118
+ return Response.json(body, init);
71
119
  }
72
120
  /**
73
- * Send a JSON APIResponse body
74
- * @param data The data to send
75
- * @returns The response object
121
+ * Whether this KaitoHead instance has been touched/modified
76
122
  */
77
- json(data) {
78
- const json2 = JSON.stringify(data);
79
- this.raw.setHeader("Content-Type", "application/json");
80
- this.raw.setHeader("Content-Length", Buffer.byteLength(json2));
81
- this.raw.end(json2);
82
- return this;
123
+ get touched() {
124
+ return this._status !== 200 || this._headers !== null;
83
125
  }
84
126
  };
85
127
 
86
- // src/router.ts
87
- var getSend = (res) => (status, response) => {
88
- if (res.raw.headersSent) {
89
- return;
90
- }
91
- res.status(status).json(response);
92
- };
128
+ // src/util.ts
129
+ var isNodeLikeDev = typeof process !== "undefined" && typeof process.env !== "undefined" && process.env.NODE_ENV === "development";
130
+ function createUtilities(getContext) {
131
+ return {
132
+ getContext,
133
+ router: () => Router.create()
134
+ };
135
+ }
136
+ function parsable(parse) {
137
+ return {
138
+ parse
139
+ };
140
+ }
141
+
142
+ // src/router/router.ts
93
143
  var Router = class _Router {
94
- routerOptions;
95
- routes;
96
- static create = () => new _Router([], {
97
- through: async (context) => context
144
+ state;
145
+ static create = () => new _Router({
146
+ through: async (context) => context,
147
+ routes: /* @__PURE__ */ new Set()
98
148
  });
99
149
  static parseQuery(schema, url) {
100
150
  if (!schema) {
101
151
  return {};
102
152
  }
103
153
  const result = {};
104
- for (const [key, parsable2] of Object.entries(schema)) {
154
+ for (const key in schema) {
155
+ if (!schema.hasOwnProperty(key)) continue;
105
156
  const value = url.searchParams.get(key);
106
- result[key] = parsable2.parse(value);
157
+ result[key] = schema[key].parse(value);
107
158
  }
108
159
  return result;
109
160
  }
110
- static async handle(server, route, options) {
111
- const send = getSend(options.res);
112
- try {
113
- const rootCtx = await server.getContext(options.req, options.res);
114
- const ctx = await route.through(rootCtx);
115
- const body = await route.body?.parse(await getBody(options.req)) ?? void 0;
116
- const query = _Router.parseQuery(route.query, options.req.url);
117
- const result = await route.run({
118
- ctx,
119
- body,
120
- query,
121
- params: options.params
122
- });
123
- if (options.res.raw.headersSent) {
124
- return {
125
- success: true,
126
- data: result
127
- };
128
- }
129
- send(200, {
130
- success: true,
131
- data: result,
132
- message: "OK"
133
- });
134
- return {
135
- success: true,
136
- data: result
137
- };
138
- } catch (e) {
139
- const error = WrappedError.maybe(e);
140
- if (error instanceof KaitoError) {
141
- send(error.status, {
142
- success: false,
143
- data: null,
144
- message: error.message
145
- });
146
- return;
147
- }
148
- const { status, message } = await server.onError({ error, req: options.req, res: options.res }).catch(() => ({ status: 500, message: "Internal Server Error" }));
149
- send(status, {
150
- success: false,
151
- data: null,
152
- message
153
- });
154
- return {
155
- success: false,
156
- data: { status, message }
157
- };
158
- }
161
+ constructor(options) {
162
+ this.state = options;
159
163
  }
160
- constructor(routes, options) {
161
- this.routerOptions = options;
162
- this.routes = new Set(routes);
164
+ get routes() {
165
+ return this.state.routes;
163
166
  }
164
- /**
165
- * Adds a new route to the router
166
- * @deprecated Use the method-specific methods instead
167
- */
168
167
  add = (method, path, route) => {
169
168
  const merged = {
169
+ // TODO: Ideally fix the typing here, but this will be replaced in Kaito v4 where all routes must return a Response (which we can type)
170
170
  ...typeof route === "object" ? route : { run: route },
171
171
  method,
172
172
  path,
173
- through: this.routerOptions.through
173
+ through: this.state.through
174
174
  };
175
- return new _Router([...this.routes, merged], this.routerOptions);
175
+ return new _Router({
176
+ ...this.state,
177
+ routes: /* @__PURE__ */ new Set([...this.state.routes, merged])
178
+ });
176
179
  };
177
180
  merge = (pathPrefix, other) => {
178
- const newRoutes = [...other.routes].map((route) => ({
181
+ const newRoutes = [...other.state.routes].map((route) => ({
179
182
  ...route,
180
183
  path: `${pathPrefix}${route.path}`
181
184
  }));
182
- return new _Router(
183
- [...this.routes, ...newRoutes],
184
- this.routerOptions
185
- );
185
+ return new _Router({
186
+ ...this.state,
187
+ routes: /* @__PURE__ */ new Set([...this.state.routes, ...newRoutes])
188
+ });
186
189
  };
187
- // Allow for any server context to be passed
188
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
189
190
  freeze = (server) => {
190
- const instance = fmw({
191
- ignoreTrailingSlash: true,
192
- async defaultRoute(req, serverResponse) {
193
- const res = new KaitoResponse(serverResponse);
194
- const message = `Cannot ${req.method} ${req.url ?? "/"}`;
195
- getSend(res)(404, {
191
+ const routes = /* @__PURE__ */ new Map();
192
+ for (const route of this.state.routes) {
193
+ if (!routes.has(route.path)) {
194
+ routes.set(route.path, /* @__PURE__ */ new Map());
195
+ }
196
+ routes.get(route.path).set(route.method, route);
197
+ }
198
+ const findRoute = (method, path) => {
199
+ const params = {};
200
+ const pathParts = path.split("/").filter(Boolean);
201
+ for (const [routePath, methodHandlers] of routes) {
202
+ const routeParts = routePath.split("/").filter(Boolean);
203
+ if (routeParts.length !== pathParts.length) continue;
204
+ let matches = true;
205
+ for (let i = 0; i < routeParts.length; i++) {
206
+ const routePart = routeParts[i];
207
+ const pathPart = pathParts[i];
208
+ if (routePart && pathPart && routePart.startsWith(":")) {
209
+ params[routePart.slice(1)] = pathPart;
210
+ } else if (routePart !== pathPart) {
211
+ matches = false;
212
+ break;
213
+ }
214
+ }
215
+ if (matches) {
216
+ const route = methodHandlers.get(method);
217
+ if (route) return { route, params };
218
+ }
219
+ }
220
+ return { params };
221
+ };
222
+ return async (req) => {
223
+ const url = new URL(req.url);
224
+ const method = req.method;
225
+ const { route, params } = findRoute(method, url.pathname);
226
+ if (!route) {
227
+ const body = {
196
228
  success: false,
197
229
  data: null,
198
- message
199
- });
200
- return {
201
- success: false,
202
- data: { status: 404, message }
230
+ message: `Cannot ${method} ${url.pathname}`
203
231
  };
232
+ return Response.json(body, { status: 404 });
204
233
  }
205
- });
206
- for (const route of this.routes) {
207
- const handler = async (incomingMessage, serverResponse, params) => {
208
- const req = new KaitoRequest(incomingMessage);
209
- const res = new KaitoResponse(serverResponse);
210
- return _Router.handle(server, route, {
211
- params,
212
- req,
213
- res
234
+ const request = new KaitoRequest(url, req);
235
+ const head = new KaitoHead();
236
+ try {
237
+ const body = route.body ? await route.body.parse(await req.json()) : void 0;
238
+ const query = _Router.parseQuery(route.query, url);
239
+ const rootCtx = await server.getContext(request, head);
240
+ const ctx = await route.through(rootCtx);
241
+ const result = await route.run({
242
+ ctx,
243
+ body,
244
+ query,
245
+ params
246
+ });
247
+ if (result instanceof Response) {
248
+ if (isNodeLikeDev) {
249
+ if (head.touched) {
250
+ const msg = [
251
+ "Kaito detected that you used the KaitoHead object to modify the headers or status, but then returned a Response in the route",
252
+ "This is usually a mistake, as your Response object will override any changes you made to the headers or status code.",
253
+ "",
254
+ "This warning was shown because `process.env.NODE_ENV=development`"
255
+ ].join("\n");
256
+ console.warn(msg);
257
+ }
258
+ }
259
+ return result;
260
+ }
261
+ return head.toResponse({
262
+ success: true,
263
+ data: result,
264
+ message: "OK"
265
+ });
266
+ } catch (e) {
267
+ const error = WrappedError.maybe(e);
268
+ if (error instanceof KaitoError) {
269
+ return head.status(error.status).toResponse({
270
+ success: false,
271
+ data: null,
272
+ message: error.message
273
+ });
274
+ }
275
+ const { status, message } = await server.onError({ error, req: request }).catch(() => ({ status: 500, message: "Internal Server Error" }));
276
+ return head.status(status).toResponse({
277
+ success: false,
278
+ data: null,
279
+ message
214
280
  });
215
- };
216
- if (route.method === "*") {
217
- instance.all(route.path, handler);
218
- continue;
219
281
  }
220
- instance.on(route.method, route.path, handler);
221
- }
222
- return instance;
282
+ };
223
283
  };
224
284
  method = (method) => (path, route) => {
225
285
  return this.add(method, path, route);
@@ -231,161 +291,20 @@ var Router = class _Router {
231
291
  delete = this.method("DELETE");
232
292
  head = this.method("HEAD");
233
293
  options = this.method("OPTIONS");
234
- through = (transform) => new _Router(this.routes, {
235
- through: async (context) => {
236
- const fromCurrentRouter = await this.routerOptions.through(context);
237
- return transform(fromCurrentRouter);
238
- }
239
- });
240
- };
241
-
242
- // src/util.ts
243
- function createGetContext(callback) {
244
- return callback;
245
- }
246
- function createUtilities(getContext) {
247
- return {
248
- getContext,
249
- router: () => Router.create()
250
- };
251
- }
252
- function getLastEntryInMultiHeaderValue(headerValue) {
253
- const normalized = Array.isArray(headerValue) ? headerValue.join(",") : headerValue;
254
- const lastIndex = normalized.lastIndexOf(",");
255
- return lastIndex === -1 ? normalized.trim() : normalized.slice(lastIndex + 1).trim();
256
- }
257
- function parsable(parse) {
258
- return {
259
- parse
294
+ through = (through) => {
295
+ return new _Router({
296
+ ...this.state,
297
+ through: async (context) => through(await this.state.through(context))
298
+ });
260
299
  };
261
- }
262
- async function getBody(req) {
263
- if (!req.headers["content-type"]) {
264
- return null;
265
- }
266
- const buffer = await getRawBody(req.raw);
267
- const { type } = parseContentType(req.headers["content-type"]);
268
- switch (type) {
269
- case "application/json": {
270
- return json(Readable.from(buffer));
271
- }
272
- default: {
273
- if (process.env.NODE_ENV === "development") {
274
- console.warn("[kaito] Unsupported content type:", type);
275
- console.warn("[kaito] This message is only shown in development mode.");
276
- }
277
- return null;
278
- }
279
- }
280
- }
281
-
282
- // src/req.ts
283
- var KaitoRequest = class {
284
- constructor(raw) {
285
- this.raw = raw;
286
- }
287
- _url = null;
288
- /**
289
- * The full URL of the request, including the protocol, hostname, and path.
290
- * Note: does not include the query string or hash
291
- */
292
- get fullURL() {
293
- return `${this.protocol}://${this.hostname}${this.raw.url ?? ""}`;
294
- }
295
- /**
296
- * A new URL instance for the full URL of the request.
297
- */
298
- get url() {
299
- if (this._url) {
300
- return this._url;
301
- }
302
- this._url = new URL(this.fullURL);
303
- return this._url;
304
- }
305
- /**
306
- * The HTTP method of the request.
307
- */
308
- get method() {
309
- if (!this.raw.method) {
310
- throw new Error("Request method is not defined, somehow...");
311
- }
312
- return this.raw.method;
313
- }
314
- /**
315
- * The protocol of the request, either `http` or `https`.
316
- */
317
- get protocol() {
318
- if (this.raw.socket instanceof TLSSocket) {
319
- return this.raw.socket.encrypted ? "https" : "http";
320
- }
321
- return "http";
322
- }
323
- /**
324
- * The request headers
325
- */
326
- get headers() {
327
- return this.raw.headers;
328
- }
329
- /**
330
- * The hostname of the request.
331
- */
332
- get hostname() {
333
- return this.raw.headers.host ?? getLastEntryInMultiHeaderValue(this.raw.headers[":authority"] ?? []);
334
- }
335
300
  };
336
-
337
- // src/server.ts
338
- import * as http from "node:http";
339
- function createFMWServer(config) {
340
- const router = config.router.freeze(config);
341
- const rawRoutes = config.rawRoutes ?? {};
342
- for (const method in rawRoutes) {
343
- if (!Object.prototype.hasOwnProperty.call(rawRoutes, method)) {
344
- continue;
345
- }
346
- const routes = rawRoutes[method];
347
- if (!routes || routes.length === 0) {
348
- continue;
349
- }
350
- for (const route of routes) {
351
- if (method === "*") {
352
- router.all(route.path, route.handler);
353
- continue;
354
- }
355
- router[method.toLowerCase()](route.path, route.handler);
356
- }
357
- }
358
- const server = http.createServer(async (req, res) => {
359
- let before;
360
- if (config.before) {
361
- before = await config.before(req, res);
362
- } else {
363
- before = void 0;
364
- }
365
- if (res.headersSent) {
366
- return;
367
- }
368
- const result = await router.lookup(req, res);
369
- if ("after" in config && config.after) {
370
- await config.after(before, result);
371
- }
372
- });
373
- return { server, fmw: router };
374
- }
375
- function createServer2(config) {
376
- return createFMWServer(config).server;
377
- }
378
301
  export {
379
302
  KaitoError,
380
303
  KaitoRequest,
381
- KaitoResponse,
382
304
  Router,
383
305
  WrappedError,
384
- createFMWServer,
385
- createGetContext,
386
- createServer2 as createServer,
306
+ createKaitoHandler,
387
307
  createUtilities,
388
- getBody,
389
- getLastEntryInMultiHeaderValue,
308
+ isNodeLikeDev,
390
309
  parsable
391
310
  };