@derivesome/server 1.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.
Files changed (139) hide show
  1. package/.package.json.~undo-tree~ +4 -0
  2. package/.tsconfig.json.~undo-tree~ +4 -0
  3. package/CLAUDE.md +51 -0
  4. package/dist/cjs/app.d.ts +25 -0
  5. package/dist/cjs/app.d.ts.map +1 -0
  6. package/dist/cjs/app.js +221 -0
  7. package/dist/cjs/app.js.map +1 -0
  8. package/dist/cjs/common/index.d.ts +3 -0
  9. package/dist/cjs/common/index.d.ts.map +1 -0
  10. package/dist/cjs/common/index.js +19 -0
  11. package/dist/cjs/common/index.js.map +1 -0
  12. package/dist/cjs/common/is.d.ts +2 -0
  13. package/dist/cjs/common/is.d.ts.map +1 -0
  14. package/dist/cjs/common/is.js +14 -0
  15. package/dist/cjs/common/is.js.map +1 -0
  16. package/dist/cjs/common/types.d.ts +7 -0
  17. package/dist/cjs/common/types.d.ts.map +1 -0
  18. package/dist/cjs/common/types.js +3 -0
  19. package/dist/cjs/common/types.js.map +1 -0
  20. package/dist/cjs/index.d.ts +9 -0
  21. package/dist/cjs/index.d.ts.map +1 -0
  22. package/dist/cjs/index.js +25 -0
  23. package/dist/cjs/index.js.map +1 -0
  24. package/dist/cjs/meta-provider.d.ts +5 -0
  25. package/dist/cjs/meta-provider.d.ts.map +1 -0
  26. package/dist/cjs/meta-provider.js +3 -0
  27. package/dist/cjs/meta-provider.js.map +1 -0
  28. package/dist/cjs/request-method.d.ts +2 -0
  29. package/dist/cjs/request-method.d.ts.map +1 -0
  30. package/dist/cjs/request-method.js +3 -0
  31. package/dist/cjs/request-method.js.map +1 -0
  32. package/dist/cjs/request-response.d.ts +13 -0
  33. package/dist/cjs/request-response.d.ts.map +1 -0
  34. package/dist/cjs/request-response.js +57 -0
  35. package/dist/cjs/request-response.js.map +1 -0
  36. package/dist/cjs/response.d.ts +40 -0
  37. package/dist/cjs/response.d.ts.map +1 -0
  38. package/dist/cjs/response.js +58 -0
  39. package/dist/cjs/response.js.map +1 -0
  40. package/dist/cjs/route-handler.d.ts +20 -0
  41. package/dist/cjs/route-handler.d.ts.map +1 -0
  42. package/dist/cjs/route-handler.js +3 -0
  43. package/dist/cjs/route-handler.js.map +1 -0
  44. package/dist/cjs/route.d.ts +17 -0
  45. package/dist/cjs/route.d.ts.map +1 -0
  46. package/dist/cjs/route.js +3 -0
  47. package/dist/cjs/route.js.map +1 -0
  48. package/dist/esm/app.d.ts +25 -0
  49. package/dist/esm/app.d.ts.map +1 -0
  50. package/dist/esm/app.js +221 -0
  51. package/dist/esm/app.js.map +1 -0
  52. package/dist/esm/common/index.d.ts +3 -0
  53. package/dist/esm/common/index.d.ts.map +1 -0
  54. package/dist/esm/common/index.js +19 -0
  55. package/dist/esm/common/index.js.map +1 -0
  56. package/dist/esm/common/is.d.ts +2 -0
  57. package/dist/esm/common/is.d.ts.map +1 -0
  58. package/dist/esm/common/is.js +14 -0
  59. package/dist/esm/common/is.js.map +1 -0
  60. package/dist/esm/common/types.d.ts +7 -0
  61. package/dist/esm/common/types.d.ts.map +1 -0
  62. package/dist/esm/common/types.js +3 -0
  63. package/dist/esm/common/types.js.map +1 -0
  64. package/dist/esm/index.d.ts +9 -0
  65. package/dist/esm/index.d.ts.map +1 -0
  66. package/dist/esm/index.js +25 -0
  67. package/dist/esm/index.js.map +1 -0
  68. package/dist/esm/meta-provider.d.ts +5 -0
  69. package/dist/esm/meta-provider.d.ts.map +1 -0
  70. package/dist/esm/meta-provider.js +3 -0
  71. package/dist/esm/meta-provider.js.map +1 -0
  72. package/dist/esm/request-method.d.ts +2 -0
  73. package/dist/esm/request-method.d.ts.map +1 -0
  74. package/dist/esm/request-method.js +3 -0
  75. package/dist/esm/request-method.js.map +1 -0
  76. package/dist/esm/request-response.d.ts +13 -0
  77. package/dist/esm/request-response.d.ts.map +1 -0
  78. package/dist/esm/request-response.js +57 -0
  79. package/dist/esm/request-response.js.map +1 -0
  80. package/dist/esm/response.d.ts +40 -0
  81. package/dist/esm/response.d.ts.map +1 -0
  82. package/dist/esm/response.js +58 -0
  83. package/dist/esm/response.js.map +1 -0
  84. package/dist/esm/route-handler.d.ts +20 -0
  85. package/dist/esm/route-handler.d.ts.map +1 -0
  86. package/dist/esm/route-handler.js +3 -0
  87. package/dist/esm/route-handler.js.map +1 -0
  88. package/dist/esm/route.d.ts +17 -0
  89. package/dist/esm/route.d.ts.map +1 -0
  90. package/dist/esm/route.js +3 -0
  91. package/dist/esm/route.js.map +1 -0
  92. package/package.json +48 -0
  93. package/package.json~ +54 -0
  94. package/src/.app.ts.~undo-tree~ +55 -0
  95. package/src/.index.ts.~undo-tree~ +9 -0
  96. package/src/.meta-provider.ts.~undo-tree~ +19 -0
  97. package/src/.method.ts.~undo-tree~ +6 -0
  98. package/src/.request-method.ts.~undo-tree~ +6 -0
  99. package/src/.request-response.ts.~undo-tree~ +233 -0
  100. package/src/.request.ts.~undo-tree~ +6 -0
  101. package/src/.response.ts.~undo-tree~ +168 -0
  102. package/src/.route-handler.ts.~undo-tree~ +53 -0
  103. package/src/.route-params.ts.~undo-tree~ +6 -0
  104. package/src/.route.ts.~undo-tree~ +5 -0
  105. package/src/app.test.ts +245 -0
  106. package/src/app.ts +272 -0
  107. package/src/app.ts~ +277 -0
  108. package/src/common/.index.ts.~undo-tree~ +9 -0
  109. package/src/common/.is.ts.~undo-tree~ +5 -0
  110. package/src/common/.types.ts.~undo-tree~ +31 -0
  111. package/src/common/index.ts +2 -0
  112. package/src/common/index.ts~ +1 -0
  113. package/src/common/is.ts +6 -0
  114. package/src/common/is.ts~ +0 -0
  115. package/src/common/types.ts +9 -0
  116. package/src/common/types.ts~ +13 -0
  117. package/src/index.ts +8 -0
  118. package/src/index.ts~ +8 -0
  119. package/src/meta-provider.ts +10 -0
  120. package/src/meta-provider.ts~ +5 -0
  121. package/src/method.ts~ +0 -0
  122. package/src/request-method.ts +1 -0
  123. package/src/request-method.ts~ +1 -0
  124. package/src/request-response.ts +63 -0
  125. package/src/request-response.ts~ +63 -0
  126. package/src/request.ts~ +0 -0
  127. package/src/response.ts +100 -0
  128. package/src/response.ts~ +49 -0
  129. package/src/route-handler.ts +45 -0
  130. package/src/route-handler.ts~ +37 -0
  131. package/src/route-params.ts~ +0 -0
  132. package/src/route.ts +37 -0
  133. package/src/route.ts~ +37 -0
  134. package/tsconfig.cjs.json +10 -0
  135. package/tsconfig.cjs.json~ +0 -0
  136. package/tsconfig.esm.json +10 -0
  137. package/tsconfig.esm.json~ +10 -0
  138. package/tsconfig.json +22 -0
  139. package/tsconfig.json~ +22 -0
@@ -0,0 +1,245 @@
1
+ import { describe, it, beforeAll, afterAll, assert } from "vitest";
2
+ import { shapes } from "shapedef";
3
+ import http from "node:http";
4
+ import { createApp } from "./app";
5
+
6
+ function getPort(server: http.Server): number {
7
+ const addr = server.address();
8
+ if (!addr || typeof addr === "string") throw new Error("No port");
9
+ return addr.port;
10
+ }
11
+
12
+ function closeServer(server: http.Server): Promise<void> {
13
+ return new Promise((resolve, reject) =>
14
+ server.close((err) => (err ? reject(err) : resolve())),
15
+ );
16
+ }
17
+
18
+ async function request(
19
+ port: number,
20
+ path: string,
21
+ options: RequestInit = {},
22
+ ): Promise<{ status: number; body: unknown; text: string }> {
23
+ const res = await fetch(`http://localhost:${port}${path}`, options);
24
+ const text = await res.text();
25
+ let body: unknown;
26
+ try {
27
+ body = JSON.parse(text);
28
+ } catch {
29
+ body = text;
30
+ }
31
+ return { status: res.status, body, text };
32
+ }
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Basic routes
36
+ // ---------------------------------------------------------------------------
37
+ describe("GET routes", () => {
38
+ let server: http.Server;
39
+ let port: number;
40
+
41
+ beforeAll(
42
+ () =>
43
+ new Promise<void>((resolve) => {
44
+ const app = createApp();
45
+ app.route({
46
+ method: "GET",
47
+ path: "/ping",
48
+ args: null,
49
+ handler: () => ({ pong: true }),
50
+ });
51
+ app.route({
52
+ method: "GET",
53
+ path: "/users/:id",
54
+ args: null,
55
+ handler: (ctx) => ({ id: ctx.params.id }),
56
+ });
57
+ server = app.listen(0, resolve);
58
+ port = getPort(server);
59
+ }),
60
+ );
61
+
62
+ afterAll(() => closeServer(server));
63
+
64
+ it("returns JSON for a plain-object handler return", async () => {
65
+ const { status, body } = await request(port, "/ping");
66
+ assert.equal(status, 200);
67
+ assert.deepEqual(body, { pong: true });
68
+ });
69
+
70
+ it("extracts URL params", async () => {
71
+ const { status, body } = await request(port, "/users/42");
72
+ assert.equal(status, 200);
73
+ assert.deepEqual(body, { id: "42" });
74
+ });
75
+
76
+ it("returns 404 for unknown routes", async () => {
77
+ const { status, body } = await request(port, "/does-not-exist");
78
+ assert.equal(status, 404);
79
+ assert.deepEqual(body, { error: "Not Found" });
80
+ });
81
+ });
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // POST routes with shapedef validation
85
+ // ---------------------------------------------------------------------------
86
+ describe("POST routes", () => {
87
+ let server: http.Server;
88
+ let port: number;
89
+
90
+ beforeAll(
91
+ () =>
92
+ new Promise<void>((resolve) => {
93
+ const app = createApp();
94
+ app.route({
95
+ method: "POST",
96
+ path: "/items",
97
+ args: shapes.mapping({ name: shapes.str(), qty: shapes.int() }),
98
+ handler: (ctx) => ({ received: ctx.args }),
99
+ });
100
+ server = app.listen(0, resolve);
101
+ port = getPort(server);
102
+ }),
103
+ );
104
+
105
+ afterAll(() => closeServer(server));
106
+
107
+ it("accepts a valid body and passes args to handler", async () => {
108
+ const { status, body } = await request(port, "/items", {
109
+ method: "POST",
110
+ headers: { "Content-Type": "application/json" },
111
+ body: JSON.stringify({ name: "widget", qty: 5 }),
112
+ });
113
+ assert.equal(status, 200);
114
+ assert.deepEqual(body, { received: { name: "widget", qty: 5 } });
115
+ });
116
+
117
+ it("rejects an invalid body with 400", async () => {
118
+ const { status } = await request(port, "/items", {
119
+ method: "POST",
120
+ headers: { "Content-Type": "application/json" },
121
+ body: JSON.stringify({ name: "widget", qty: "not-a-number" }),
122
+ });
123
+ assert.equal(status, 400);
124
+ });
125
+
126
+ it("rejects a missing required field with 400", async () => {
127
+ const { status } = await request(port, "/items", {
128
+ method: "POST",
129
+ headers: { "Content-Type": "application/json" },
130
+ body: JSON.stringify({ name: "widget" }),
131
+ });
132
+ assert.equal(status, 400);
133
+ });
134
+ });
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // ResponseBuilder — custom status codes and text responses
138
+ // ---------------------------------------------------------------------------
139
+ describe("ResponseBuilder", () => {
140
+ let server: http.Server;
141
+ let port: number;
142
+
143
+ beforeAll(
144
+ () =>
145
+ new Promise<void>((resolve) => {
146
+ const app = createApp();
147
+ app.route({
148
+ method: "GET",
149
+ path: "/not-found-custom",
150
+ args: null,
151
+ handler: (ctx) => ctx.R.json({ error: "custom" }).status(404),
152
+ });
153
+ app.route({
154
+ method: "GET",
155
+ path: "/html",
156
+ args: null,
157
+ handler: (ctx) => ctx.R.text("<p>hello</p>"),
158
+ });
159
+ server = app.listen(0, resolve);
160
+ port = getPort(server);
161
+ }),
162
+ );
163
+
164
+ afterAll(() => closeServer(server));
165
+
166
+ it("respects a custom 404 status from ResponseBuilder", async () => {
167
+ const { status, body } = await request(port, "/not-found-custom");
168
+ assert.equal(status, 404);
169
+ assert.deepEqual(body, { error: "custom" });
170
+ });
171
+
172
+ it("returns text/html content", async () => {
173
+ const { status, text } = await request(port, "/html");
174
+ assert.equal(status, 200);
175
+ assert.equal(text, "<p>hello</p>");
176
+ });
177
+ });
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // MetaProvider — metadata accumulates across providers
181
+ // ---------------------------------------------------------------------------
182
+ describe("metaProvider", () => {
183
+ let server: http.Server;
184
+ let port: number;
185
+
186
+ beforeAll(
187
+ () =>
188
+ new Promise<void>((resolve) => {
189
+ const app = createApp()
190
+ .metaProvider(() => ({ userId: 99 }))
191
+ .metaProvider((ctx) => ({
192
+ role: ctx.meta.userId === 99 ? "admin" : "user",
193
+ }));
194
+
195
+ app.route({
196
+ method: "GET",
197
+ path: "/me",
198
+ args: null,
199
+ handler: (ctx) => ({ userId: ctx.meta.userId, role: ctx.meta.role }),
200
+ });
201
+
202
+ server = app.listen(0, resolve);
203
+ port = getPort(server);
204
+ }),
205
+ );
206
+
207
+ afterAll(() => closeServer(server));
208
+
209
+ it("accumulates metadata from all providers", async () => {
210
+ const { status, body } = await request(port, "/me");
211
+ assert.equal(status, 200);
212
+ assert.deepEqual(body, { userId: 99, role: "admin" });
213
+ });
214
+ });
215
+
216
+ // ---------------------------------------------------------------------------
217
+ // Array return value
218
+ // ---------------------------------------------------------------------------
219
+ describe("array return", () => {
220
+ let server: http.Server;
221
+ let port: number;
222
+
223
+ beforeAll(
224
+ () =>
225
+ new Promise<void>((resolve) => {
226
+ const app = createApp();
227
+ app.route({
228
+ method: "GET",
229
+ path: "/list",
230
+ args: null,
231
+ handler: () => [1, 2, 3],
232
+ });
233
+ server = app.listen(0, resolve);
234
+ port = getPort(server);
235
+ }),
236
+ );
237
+
238
+ afterAll(() => closeServer(server));
239
+
240
+ it("serialises an array return as JSON", async () => {
241
+ const { status, body } = await request(port, "/list");
242
+ assert.equal(status, 200);
243
+ assert.deepEqual(body, [1, 2, 3]);
244
+ });
245
+ });
package/src/app.ts ADDED
@@ -0,0 +1,272 @@
1
+ import { ShapeMapping, shapes, validate } from "shapedef";
2
+ import { isPlainObject, LooseRecord, UnknownRecord } from "./common";
3
+ import { MetaProvider } from "./meta-provider";
4
+ import { RequestMethod } from "./request-method";
5
+ import { Request, Response } from "./request-response";
6
+ import { Route, RouteWithoutArgs } from "./route";
7
+ import {
8
+ RouteHandler,
9
+ RouteHandlerContext,
10
+ RouteHandlerReturn,
11
+ } from "./route-handler";
12
+ import http from "node:http";
13
+ import fs from "node:fs";
14
+ import path from "node:path";
15
+ import { ResponseBuilder, ResponseMaker, ResponseFile } from "./response";
16
+
17
+ const MIME_TYPES: Record<string, string> = {
18
+ ".html": "text/html",
19
+ ".htm": "text/html",
20
+ ".css": "text/css",
21
+ ".js": "application/javascript",
22
+ ".mjs": "application/javascript",
23
+ ".json": "application/json",
24
+ ".png": "image/png",
25
+ ".jpg": "image/jpeg",
26
+ ".jpeg": "image/jpeg",
27
+ ".gif": "image/gif",
28
+ ".svg": "image/svg+xml",
29
+ ".ico": "image/x-icon",
30
+ ".webp": "image/webp",
31
+ ".txt": "text/plain",
32
+ ".pdf": "application/pdf",
33
+ ".woff": "font/woff",
34
+ ".woff2": "font/woff2",
35
+ };
36
+
37
+ function mimeForFile(filePath: string): string {
38
+ const ext = path.extname(filePath).toLowerCase();
39
+ return MIME_TYPES[ext] ?? "application/octet-stream";
40
+ }
41
+
42
+ function sendFile(res: Response, resp: ResponseFile): Promise<void> {
43
+ return new Promise((resolve, reject) => {
44
+ fs.stat(resp.filePath, (err, stat) => {
45
+ if (err) {
46
+ res.writeHead(404, { "Content-Type": "application/json" });
47
+ res.end(JSON.stringify({ error: "Not Found" }));
48
+ return resolve();
49
+ }
50
+ const contentType = resp.contentType ?? mimeForFile(resp.filePath);
51
+ res.writeHead(resp.statusCode, {
52
+ "Content-Type": contentType,
53
+ "Content-Length": stat.size,
54
+ });
55
+ const stream = fs.createReadStream(resp.filePath);
56
+ stream.on("error", reject);
57
+ stream.on("end", resolve);
58
+ stream.pipe(res);
59
+ });
60
+ });
61
+ }
62
+
63
+ type CompiledRoute = {
64
+ pattern: RegExp;
65
+ paramNames: string[];
66
+ route: Route<any, any, any, any, any>;
67
+ };
68
+
69
+ function compilePath(path: string): { pattern: RegExp; paramNames: string[] } {
70
+ const paramNames: string[] = [];
71
+ const regexStr = path.replace(/:([^/]+)/g, (_, name: string) => {
72
+ paramNames.push(name);
73
+ return "([^/]+)";
74
+ });
75
+ return { pattern: new RegExp(`^${regexStr}$`), paramNames };
76
+ }
77
+
78
+ export class App<Meta extends UnknownRecord = UnknownRecord> {
79
+ private _meta: Meta;
80
+ private _metaProviders: Array<MetaProvider<LooseRecord, LooseRecord>> = [];
81
+ private _routes: Map<RequestMethod, CompiledRoute[]> = new Map();
82
+ private _responseMaker: ResponseMaker = new ResponseMaker();
83
+
84
+ constructor(meta: Meta) {
85
+ this._meta = meta;
86
+ }
87
+
88
+ metaProvider<NextMeta extends UnknownRecord>(
89
+ provider: MetaProvider<NextMeta, Meta>,
90
+ ): App<Meta & NextMeta> {
91
+ const app = new App<Meta & NextMeta>(this._meta as Meta & NextMeta);
92
+ app._metaProviders.push(...this._metaProviders);
93
+ app._metaProviders.push(provider as MetaProvider<LooseRecord, LooseRecord>);
94
+ return app;
95
+ }
96
+
97
+ route<Method extends RequestMethod, Path extends string>(
98
+ route: RouteWithoutArgs<Meta, Method, Path>,
99
+ ): this;
100
+ route<
101
+ Args extends ShapeMapping<any>,
102
+ Method extends RequestMethod,
103
+ Path extends string,
104
+ >(route: Route<Meta, Args, Method, Path>): this;
105
+ route(route: Route<any, any, any, any, any>): this {
106
+ if (!this._routes.has(route.method)) {
107
+ this._routes.set(route.method, []);
108
+ }
109
+ const { pattern, paramNames } = compilePath(route.path);
110
+ this._routes.get(route.method)!.push({ pattern, paramNames, route });
111
+ return this;
112
+ }
113
+
114
+ get<
115
+ Path extends string,
116
+ Args extends ShapeMapping<any>,
117
+ Handler extends RouteHandler<Meta, Args>,
118
+ >(path: Path, args: Args | undefined | null, handler: Handler): this {
119
+ return this.route({
120
+ method: "GET",
121
+ path: path,
122
+ handler,
123
+ args: args || null,
124
+ });
125
+ }
126
+
127
+ private resolve(
128
+ method: RequestMethod,
129
+ url: string,
130
+ ): { route: Route; params: Record<string, string> } | null {
131
+ const routes = this._routes.get(method);
132
+ if (!routes) return null;
133
+ for (const { pattern, paramNames, route } of routes) {
134
+ const match = pattern.exec(url);
135
+ if (match) {
136
+ const params: Record<string, string> = {};
137
+ for (let i = 0; i < paramNames.length; i++) {
138
+ params[paramNames[i]!] = match[i + 1]!;
139
+ }
140
+ return { route, params };
141
+ }
142
+ }
143
+ return null;
144
+ }
145
+
146
+ private async sendReturn(ctx: RouteHandlerContext, ret: RouteHandlerReturn) {
147
+ if (ret instanceof ResponseBuilder) {
148
+ const resp = ret.resp;
149
+ switch (resp.type) {
150
+ case "json":
151
+ {
152
+ ctx.res.writeHead(resp.statusCode, {
153
+ "Content-Type": resp.contentType,
154
+ });
155
+ ctx.res.end(JSON.stringify(resp.payload));
156
+ }
157
+ break;
158
+ case "text":
159
+ {
160
+ ctx.res.writeHead(resp.statusCode, {
161
+ "Content-Type": resp.contentType,
162
+ });
163
+ ctx.res.end(resp.payload);
164
+ }
165
+ break;
166
+ case "stream":
167
+ {
168
+ await new Promise<void>((resolve, reject) => {
169
+ ctx.res.writeHead(resp.statusCode, {
170
+ "Content-Type": resp.contentType,
171
+ });
172
+ resp.stream.on("error", reject);
173
+ resp.stream.on("end", resolve);
174
+ resp.stream.pipe(ctx.res);
175
+ });
176
+ }
177
+ break;
178
+ case "binary":
179
+ {
180
+ ctx.res.writeHead(resp.statusCode, {
181
+ "Content-Type": resp.contentType,
182
+ "Content-Length": resp.payload.byteLength,
183
+ });
184
+ ctx.res.end(resp.payload);
185
+ }
186
+ break;
187
+ case "file":
188
+ {
189
+ await sendFile(ctx.res, resp);
190
+ }
191
+ break;
192
+ }
193
+ } else if (Array.isArray(ret) || isPlainObject(ret)) {
194
+ ctx.res.writeHead(200, { "Content-Type": "application/json" });
195
+ ctx.res.end(JSON.stringify(ret));
196
+ }
197
+ }
198
+
199
+ private async runRoute(
200
+ ctx: RouteHandlerContext<Request, Response, any, any, any>,
201
+ route: Route,
202
+ ) {
203
+ if (route.mergeParams !== false && isPlainObject(ctx.req.body)) {
204
+ ctx.args = {};
205
+ ctx.args = { ...ctx.args, ...ctx.req.body };
206
+ }
207
+ if (["POST", "PUT", "PATCH"].includes(route.method)) {
208
+ if (route.args) {
209
+ const validation = validate(ctx.req.body, route.args);
210
+ if (!validation.ok) {
211
+ return await this.sendReturn(ctx, ctx.R.json(validation).status(400));
212
+ }
213
+ ctx.args = ctx.req.body;
214
+ }
215
+ }
216
+
217
+ await this.sendReturn(ctx, await route.handler(ctx));
218
+ }
219
+
220
+ private async runPipeline(
221
+ req: Request,
222
+ res: Response,
223
+ method: RequestMethod,
224
+ path: string,
225
+ ) {
226
+ console.log(method, path);
227
+ const ctx: RouteHandlerContext<Request, Response, any, any, any> = {
228
+ req,
229
+ res,
230
+ meta: {},
231
+ args: {},
232
+ params: {},
233
+ R: this._responseMaker,
234
+ };
235
+
236
+ for (const metaProvider of this._metaProviders) {
237
+ const output = await metaProvider(ctx);
238
+ Object.assign(ctx.meta, output);
239
+ }
240
+
241
+ const pathname = path.split("?")[0]!;
242
+ const resolved = this.resolve(method, pathname);
243
+
244
+ if (!resolved) {
245
+ res.writeHead(404, { "Content-Type": "application/json" });
246
+ res.end(JSON.stringify({ error: "Not Found" }));
247
+ return;
248
+ }
249
+
250
+ ctx.params = resolved.params;
251
+ await this.runRoute(ctx, resolved.route);
252
+ }
253
+
254
+ listen(port: number, cb?: () => void): http.Server {
255
+ const server = http.createServer(async (req, res) => {
256
+ const method = (req.method || "GET").toUpperCase() as RequestMethod;
257
+ const path = req.url || "/";
258
+
259
+ const wrappedRequest = new Request(req);
260
+ await wrappedRequest.run();
261
+
262
+ await this.runPipeline(wrappedRequest, res, method, path);
263
+ });
264
+
265
+ server.listen(port, cb);
266
+ return server;
267
+ }
268
+ }
269
+
270
+ export function createApp(): App<UnknownRecord> {
271
+ return new App({});
272
+ }