@smuzi/http-server 0.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/build/config.d.ts +31 -0
- package/build/config.js +27 -0
- package/build/drivers/http1Server.d.ts +10 -0
- package/build/drivers/http1Server.js +187 -0
- package/build/drivers/http2Server.d.ts +11 -0
- package/build/drivers/http2Server.js +92 -0
- package/build/index.d.ts +5 -0
- package/build/index.js +5 -0
- package/build/router.d.ts +51 -0
- package/build/router.js +110 -0
- package/build/types.d.ts +12 -0
- package/build/types.js +1 -0
- package/package.json +42 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Option, HttpProtocol } from "@smuzi/std";
|
|
2
|
+
import { ActionErrorHandler, Http1Router } from "#lib/router.js";
|
|
3
|
+
import { ServerResponse } from "node:http";
|
|
4
|
+
type Cert = Option<{
|
|
5
|
+
key: string;
|
|
6
|
+
cert: string;
|
|
7
|
+
}>;
|
|
8
|
+
export type Http1ServerConfig = {
|
|
9
|
+
host: string;
|
|
10
|
+
port: number;
|
|
11
|
+
router: Http1Router;
|
|
12
|
+
cert: Cert;
|
|
13
|
+
protocol: HttpProtocol;
|
|
14
|
+
errorHandler: ActionErrorHandler<ServerResponse>;
|
|
15
|
+
};
|
|
16
|
+
type InputHttp1ServerConfig = Partial<Http1ServerConfig> & {
|
|
17
|
+
router: Http1Router;
|
|
18
|
+
};
|
|
19
|
+
export declare function buildHttp1ServerConfig({ host, port, router, cert, errorHandler }: InputHttp1ServerConfig): Http1ServerConfig;
|
|
20
|
+
type Http2BaseServerConfig = {
|
|
21
|
+
host: string;
|
|
22
|
+
port: number;
|
|
23
|
+
router: Http1Router;
|
|
24
|
+
cert?: Cert;
|
|
25
|
+
};
|
|
26
|
+
export type Http2ServerConfig = Http2BaseServerConfig & {
|
|
27
|
+
cert: Cert;
|
|
28
|
+
protocol: HttpProtocol;
|
|
29
|
+
};
|
|
30
|
+
export declare function buildHttp2ServerConfig({ host, port, router, cert }: Http2BaseServerConfig): Http2ServerConfig;
|
|
31
|
+
export {};
|
package/build/config.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { HttpProtocol, None, HttpResponse, dump } from "@smuzi/std";
|
|
2
|
+
function http1ErrorHandler(context, error) {
|
|
3
|
+
//TODO: write error to log and remove dump()
|
|
4
|
+
dump(error);
|
|
5
|
+
return HttpResponse.asJson({ error: "Internal Server Error" }, 500);
|
|
6
|
+
}
|
|
7
|
+
export function buildHttp1ServerConfig({ host = 'localhost', port = 8080, router, cert = None(), errorHandler = http1ErrorHandler }) {
|
|
8
|
+
return {
|
|
9
|
+
host,
|
|
10
|
+
port,
|
|
11
|
+
router,
|
|
12
|
+
cert,
|
|
13
|
+
protocol: cert.someOrNone(HttpProtocol.HTTPS, HttpProtocol.HTTP),
|
|
14
|
+
errorHandler,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
;
|
|
18
|
+
export function buildHttp2ServerConfig({ host, port, router, cert = None() }) {
|
|
19
|
+
return {
|
|
20
|
+
host,
|
|
21
|
+
port,
|
|
22
|
+
router,
|
|
23
|
+
cert,
|
|
24
|
+
protocol: cert.someOrNone(HttpProtocol.HTTPS, HttpProtocol.HTTP)
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Result, StdError } from '@smuzi/std';
|
|
2
|
+
import { HttpServer, HttpServerRunError, Http1ServerConfig } from "#lib/index.js";
|
|
3
|
+
type NativeServer = any;
|
|
4
|
+
export declare class StdHttp1Server implements HttpServer {
|
|
5
|
+
#private;
|
|
6
|
+
constructor(server: NativeServer);
|
|
7
|
+
close(): Promise<Result<boolean, StdError>>;
|
|
8
|
+
}
|
|
9
|
+
export declare function http1ServerRun(config: Http1ServerConfig): Promise<Result<StdHttp1Server, HttpServerRunError>>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import https from 'node:https';
|
|
3
|
+
import { TLSSocket } from 'node:tls';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import { methodFromString } from "#lib/router.js";
|
|
6
|
+
import { isArray, isObject, isString, matchUnknown, OptionFromNullable, Err, Ok, isNull, transformError, StdError, HttpResponse, HttpRequest, StdMap, isOption, isResult, RequestHttpHeaders, asList, asRecord, asMap, querystring, StdJson } from '@smuzi/std';
|
|
7
|
+
export class StdHttp1Server {
|
|
8
|
+
#server;
|
|
9
|
+
constructor(server) {
|
|
10
|
+
this.#server = server;
|
|
11
|
+
}
|
|
12
|
+
async close() {
|
|
13
|
+
return new Promise(resolve => {
|
|
14
|
+
this.#server.close((err) => {
|
|
15
|
+
return resolve(isNull(err) ? Ok(true) : Err(transformError(err)));
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function readRequestBodyAsBuffer(req) {
|
|
21
|
+
return async () => {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const chunks = [];
|
|
24
|
+
req.on("data", (chunk) => {
|
|
25
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
26
|
+
});
|
|
27
|
+
req.on("end", () => {
|
|
28
|
+
resolve(Ok(Buffer.concat(chunks)));
|
|
29
|
+
});
|
|
30
|
+
req.on("error", (err) => reject(Err(err)));
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function readRequestJson(req) {
|
|
35
|
+
return async (encoding = "utf-8") => {
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
let body = "";
|
|
38
|
+
req.setEncoding(encoding);
|
|
39
|
+
req.on("data", (chunk) => {
|
|
40
|
+
body += chunk;
|
|
41
|
+
});
|
|
42
|
+
req.on("end", () => {
|
|
43
|
+
resolve(StdJson.fromString(body));
|
|
44
|
+
});
|
|
45
|
+
req.on("error", (err) => resolve(Err(transformError(err))));
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function readRequestInput(req) {
|
|
50
|
+
return async (encoding = "utf-8") => {
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
let body = "";
|
|
53
|
+
req.setEncoding(encoding);
|
|
54
|
+
req.on("data", (chunk) => {
|
|
55
|
+
body += chunk;
|
|
56
|
+
});
|
|
57
|
+
req.on("end", () => {
|
|
58
|
+
resolve(querystring.fromString(body));
|
|
59
|
+
});
|
|
60
|
+
req.on("error", (err) => resolve(Err(transformError(err))));
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function readRawBody(req) {
|
|
65
|
+
return async (encoding = "utf-8") => {
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
let body = "";
|
|
68
|
+
req.setEncoding(encoding);
|
|
69
|
+
req.on("data", (chunk) => {
|
|
70
|
+
body += chunk;
|
|
71
|
+
});
|
|
72
|
+
req.on("end", () => {
|
|
73
|
+
resolve(Ok(body));
|
|
74
|
+
});
|
|
75
|
+
req.on("error", (err) => resolve(Err(transformError(err))));
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
let COUNTER = 0;
|
|
80
|
+
export async function http1ServerRun(config) {
|
|
81
|
+
return new Promise((resolve) => {
|
|
82
|
+
async function handler(nativeRequest, nativeResponse) {
|
|
83
|
+
const methodStr = OptionFromNullable(nativeRequest.method).unwrap();
|
|
84
|
+
const fullUrl = nativeRequest.url || "/";
|
|
85
|
+
const isHttps = nativeRequest.socket instanceof TLSSocket;
|
|
86
|
+
const urlObj = new URL(fullUrl, (isHttps ? "http" : "https") + `://${nativeRequest.headers.host}`);
|
|
87
|
+
const path = OptionFromNullable(urlObj.pathname).unwrap();
|
|
88
|
+
const request = {
|
|
89
|
+
path: path.replace(/^\//, '').replace(/\/$/, ''),
|
|
90
|
+
method: methodFromString(methodStr).unwrap(`Error: undefined http method '${methodStr}'`),
|
|
91
|
+
};
|
|
92
|
+
const routeMatched = config.router.match(request);
|
|
93
|
+
let context = {
|
|
94
|
+
request: new HttpRequest({
|
|
95
|
+
method: request.method,
|
|
96
|
+
path: request.path,
|
|
97
|
+
query: () => new StdMap(urlObj.searchParams),
|
|
98
|
+
headers: new RequestHttpHeaders(nativeRequest.headers),
|
|
99
|
+
buffer: readRequestBodyAsBuffer(nativeRequest),
|
|
100
|
+
body: readRawBody(nativeRequest),
|
|
101
|
+
json: readRequestJson(nativeRequest),
|
|
102
|
+
form: readRequestInput(nativeRequest),
|
|
103
|
+
}),
|
|
104
|
+
response: nativeResponse,
|
|
105
|
+
pathParams: routeMatched.pathParams,
|
|
106
|
+
};
|
|
107
|
+
let response;
|
|
108
|
+
try {
|
|
109
|
+
response = await routeMatched.action(context);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
response = await config.errorHandler(context, error);
|
|
113
|
+
}
|
|
114
|
+
if (isOption(response)) {
|
|
115
|
+
response = response.someOr("");
|
|
116
|
+
}
|
|
117
|
+
else if (isResult(response)) {
|
|
118
|
+
response = response.unsafeSource();
|
|
119
|
+
}
|
|
120
|
+
if (isNull(response)) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const handlers = new Map();
|
|
124
|
+
handlers.set(resp => isString(resp) || resp instanceof Buffer, (response) => {
|
|
125
|
+
nativeResponse.writeHead(200, {
|
|
126
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
127
|
+
});
|
|
128
|
+
nativeResponse.end(response);
|
|
129
|
+
});
|
|
130
|
+
handlers.set(resp => resp instanceof HttpResponse, (response) => {
|
|
131
|
+
nativeResponse.statusCode = response.status;
|
|
132
|
+
nativeResponse.statusMessage = response.statusText;
|
|
133
|
+
nativeResponse.setHeaders(response.headers.unsafeSource());
|
|
134
|
+
nativeResponse.end(response.body.someOr(""));
|
|
135
|
+
});
|
|
136
|
+
handlers.set(resp => resp instanceof StdError, (error) => {
|
|
137
|
+
nativeResponse.statusCode = 500;
|
|
138
|
+
nativeResponse.statusMessage = error.message;
|
|
139
|
+
nativeResponse.end(error.message);
|
|
140
|
+
});
|
|
141
|
+
handlers.set((response) => isObject(response) || isArray(response) || asList(response) || asRecord(response) || asMap(response), (response) => {
|
|
142
|
+
//TODO: return respons on top instead of changed nativeResponse inner
|
|
143
|
+
nativeResponse.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
144
|
+
try {
|
|
145
|
+
const resp = StdJson.toString(response).match({
|
|
146
|
+
Ok: (jsonStr) => ({ status: 200, body: jsonStr }),
|
|
147
|
+
Err: (err) => ({ status: 500, body: '{"error":"Internal Server Error"}' }),
|
|
148
|
+
});
|
|
149
|
+
nativeResponse.statusCode = resp.status;
|
|
150
|
+
nativeResponse.end(resp.body);
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
nativeResponse.statusCode = 500;
|
|
154
|
+
nativeResponse.end('{"error":"Internal Server Error"}');
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
matchUnknown(response, handlers, () => {
|
|
158
|
+
nativeResponse.writeHead(500, { "Content-Type": "text/html; charset=utf-8" });
|
|
159
|
+
nativeResponse.end("Internal Server Error");
|
|
160
|
+
}, false);
|
|
161
|
+
}
|
|
162
|
+
const server = config.cert.match({
|
|
163
|
+
Some: cert => {
|
|
164
|
+
return https.createServer({
|
|
165
|
+
key: fs.readFileSync(cert.key),
|
|
166
|
+
cert: fs.readFileSync(cert.cert),
|
|
167
|
+
}, handler);
|
|
168
|
+
},
|
|
169
|
+
None: () => {
|
|
170
|
+
return http.createServer(handler);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
server.once('error', (nativeError) => {
|
|
174
|
+
resolve(Err({
|
|
175
|
+
errno: OptionFromNullable(nativeError.errno),
|
|
176
|
+
code: OptionFromNullable(nativeError.code),
|
|
177
|
+
syscall: OptionFromNullable(nativeError.syscall),
|
|
178
|
+
path: OptionFromNullable(nativeError.path),
|
|
179
|
+
port: OptionFromNullable(nativeError.port),
|
|
180
|
+
address: OptionFromNullable(nativeError.address),
|
|
181
|
+
}));
|
|
182
|
+
});
|
|
183
|
+
server.listen(config.port, () => {
|
|
184
|
+
resolve(Ok(new StdHttp1Server(server)));
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Http2SecureServer, Http2Server } from 'node:http2';
|
|
2
|
+
import { Result, StdError } from '@smuzi/std';
|
|
3
|
+
import { HttpServer, HttpServerRunError, Http2ServerConfig } from "#lib/index.js";
|
|
4
|
+
type NativeServer = Http2SecureServer | Http2Server;
|
|
5
|
+
export declare class StdHttp2Server implements HttpServer {
|
|
6
|
+
#private;
|
|
7
|
+
constructor(server: NativeServer);
|
|
8
|
+
close(): Promise<Result<boolean, StdError>>;
|
|
9
|
+
}
|
|
10
|
+
export declare function http2ServerRun(config: Http2ServerConfig): Promise<Result<StdHttp2Server, HttpServerRunError>>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import http2 from 'node:http2';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { methodFromString } from "#lib/router.js";
|
|
4
|
+
import { isArray, isObject, isString, matchUnknown, OptionFromNullable, Err, Ok, isNull, transformError, HttpResponse, StdJson } from '@smuzi/std';
|
|
5
|
+
export class StdHttp2Server {
|
|
6
|
+
#server;
|
|
7
|
+
constructor(server) {
|
|
8
|
+
this.#server = server;
|
|
9
|
+
}
|
|
10
|
+
async close() {
|
|
11
|
+
return new Promise(resolve => {
|
|
12
|
+
this.#server.close((err) => {
|
|
13
|
+
return resolve(isNull(err) ? Ok(true) : Err(transformError(err)));
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function http2ServerRun(config) {
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
const server = config.cert.match({
|
|
21
|
+
Some: cert => {
|
|
22
|
+
return http2.createSecureServer({
|
|
23
|
+
key: fs.readFileSync(cert.key),
|
|
24
|
+
cert: fs.readFileSync(cert.cert),
|
|
25
|
+
});
|
|
26
|
+
},
|
|
27
|
+
None: () => {
|
|
28
|
+
return http2.createServer();
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
server.once('error', (nativeError) => {
|
|
32
|
+
resolve(Err({
|
|
33
|
+
errno: OptionFromNullable(nativeError.errno),
|
|
34
|
+
code: OptionFromNullable(nativeError.code),
|
|
35
|
+
syscall: OptionFromNullable(nativeError.syscall),
|
|
36
|
+
path: OptionFromNullable(nativeError.path),
|
|
37
|
+
port: OptionFromNullable(nativeError.port),
|
|
38
|
+
address: OptionFromNullable(nativeError.address),
|
|
39
|
+
}));
|
|
40
|
+
});
|
|
41
|
+
server.on('stream', (stream, headers) => {
|
|
42
|
+
const methodStr = OptionFromNullable(headers[':method']).unwrap();
|
|
43
|
+
const path = OptionFromNullable(headers[':path']).unwrap().replace(/^\//, '').replace(/\/$/, '');
|
|
44
|
+
const urlObj = new URL(path, `http://${headers[':authority']}`);
|
|
45
|
+
const request = {
|
|
46
|
+
path: path,
|
|
47
|
+
method: methodFromString(methodStr).unwrap(`Error: undefined http method '${methodStr}'`),
|
|
48
|
+
query: urlObj.searchParams,
|
|
49
|
+
};
|
|
50
|
+
const response = config.router.match(request);
|
|
51
|
+
const handlers = new Map();
|
|
52
|
+
handlers.set(isString, (response) => {
|
|
53
|
+
stream.respond({
|
|
54
|
+
'content-type': 'text/html; charset=utf-8',
|
|
55
|
+
':status': 200,
|
|
56
|
+
});
|
|
57
|
+
stream.end(response);
|
|
58
|
+
});
|
|
59
|
+
handlers.set(resp => resp instanceof HttpResponse, (response) => {
|
|
60
|
+
stream.respond({
|
|
61
|
+
'content-type': 'application/json; charset=utf-8',
|
|
62
|
+
':status': response.status,
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
handlers.set(response => isObject(response) || isArray(response), (response) => {
|
|
66
|
+
stream.respond({
|
|
67
|
+
'content-type': 'application/json; charset=utf-8',
|
|
68
|
+
':status': 200,
|
|
69
|
+
});
|
|
70
|
+
stream.end(StdJson.toString(response).match({
|
|
71
|
+
Ok: (json) => json,
|
|
72
|
+
Err: (err) => {
|
|
73
|
+
stream.respond({
|
|
74
|
+
'content-type': 'application/json; charset=utf-8',
|
|
75
|
+
':status': 500,
|
|
76
|
+
});
|
|
77
|
+
return `{"error":"Internal Server Error"}`;
|
|
78
|
+
}
|
|
79
|
+
}));
|
|
80
|
+
});
|
|
81
|
+
matchUnknown(response, handlers, _ => {
|
|
82
|
+
stream.respond({
|
|
83
|
+
':status': 500,
|
|
84
|
+
});
|
|
85
|
+
stream.end('Internal Server Error');
|
|
86
|
+
}, false);
|
|
87
|
+
});
|
|
88
|
+
server.listen(config.port, () => {
|
|
89
|
+
resolve(Ok(new StdHttp2Server(server)));
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
package/build/index.d.ts
ADDED
package/build/index.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { MatchedData, Option, HttpMethod, HttpRequest, Result, HttpResponse, StdMap, StdRecord, StdList } from "@smuzi/std";
|
|
2
|
+
import { ServerResponse } from "node:http";
|
|
3
|
+
import { ServerHttp2Stream } from "node:http2";
|
|
4
|
+
type Request = {
|
|
5
|
+
path: string;
|
|
6
|
+
method: HttpMethod;
|
|
7
|
+
};
|
|
8
|
+
type P = any;
|
|
9
|
+
type ActionPrimitiveResponse = P | StdMap<string> | StdRecord<any> | StdList | Record<PropertyKey, P> | Record<PropertyKey, P>[];
|
|
10
|
+
export type ActionResponse = void | ActionPrimitiveResponse | HttpResponse | Option<ActionPrimitiveResponse> | Result<ActionPrimitiveResponse, ActionPrimitiveResponse>;
|
|
11
|
+
export type Action<Resp extends THttpResponse> = (context: Context<Resp>) => ActionResponse | Promise<ActionResponse>;
|
|
12
|
+
export type PathParam = string | RegExp;
|
|
13
|
+
export type ActionErrorHandler<Resp extends THttpResponse> = (context: Context<Resp>, err: any) => ActionResponse | Promise<ActionResponse>;
|
|
14
|
+
type THttpResponse = ServerResponse | ServerHttp2Stream;
|
|
15
|
+
type Route = {
|
|
16
|
+
path: PathParam;
|
|
17
|
+
method: HttpMethod;
|
|
18
|
+
};
|
|
19
|
+
type GroupRoute = {
|
|
20
|
+
path: PathParam;
|
|
21
|
+
};
|
|
22
|
+
type RouteMatched = MatchedData<Request, Option<{
|
|
23
|
+
path: Record<string, string>;
|
|
24
|
+
}>>;
|
|
25
|
+
type RouteMatchResult<Resp extends THttpResponse> = {
|
|
26
|
+
action: Action<Resp>;
|
|
27
|
+
pathParams: Option<Record<string, string | number | boolean>>;
|
|
28
|
+
};
|
|
29
|
+
export type Router<Resp extends THttpResponse, A = Action<Resp>> = {
|
|
30
|
+
group: (groupRouter: Router<Resp>) => void;
|
|
31
|
+
getMapRoutes: () => Map<Route, (routeData: RouteMatched) => RouteMatchResult<Resp>>;
|
|
32
|
+
getGroupRoute(): GroupRoute;
|
|
33
|
+
get: (path: PathParam, action: A) => void;
|
|
34
|
+
post: (path: PathParam, action: A) => void;
|
|
35
|
+
put: (path: PathParam, action: A) => void;
|
|
36
|
+
delete: (path: PathParam, action: A) => void;
|
|
37
|
+
match: (request: Request) => RouteMatchResult<Resp>;
|
|
38
|
+
};
|
|
39
|
+
export type Http1Router = Router<ServerResponse>;
|
|
40
|
+
export type Http2Router = Router<ServerHttp2Stream>;
|
|
41
|
+
export type Context<Resp extends THttpResponse, Params = unknown> = {
|
|
42
|
+
request: HttpRequest;
|
|
43
|
+
response: Resp;
|
|
44
|
+
pathParams: Params;
|
|
45
|
+
};
|
|
46
|
+
export declare function processPath(path: PathParam): PathParam;
|
|
47
|
+
export declare function contactPaths(path1: PathParam, path2: PathParam): PathParam | never;
|
|
48
|
+
export declare function toStartWithPattern(input: PathParam): RegExp;
|
|
49
|
+
export declare function methodFromString(method: string): Option<HttpMethod>;
|
|
50
|
+
export declare function CreateHttpRouter<Resp extends THttpResponse, GR extends Router<Resp>>(groupRoute: GroupRoute, notFound: Action<Resp>): Router<Resp>;
|
|
51
|
+
export {};
|
package/build/router.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { asRegExp, asString, match, None, Some, HttpMethod, HttpResponse, } from "@smuzi/std";
|
|
2
|
+
export function processPath(path) {
|
|
3
|
+
if (!asString(path))
|
|
4
|
+
return path;
|
|
5
|
+
if (!/\{[a-zA-Z0-9_]+\}/g.test(path))
|
|
6
|
+
return path;
|
|
7
|
+
const pattern = `^${path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_, name) => `(?<${name}>[^/]+)`).replace(/\//g, '\\/')}$`;
|
|
8
|
+
return new RegExp(pattern);
|
|
9
|
+
}
|
|
10
|
+
export function contactPaths(path1, path2) {
|
|
11
|
+
const path1AsRegExp = asRegExp(path1);
|
|
12
|
+
const path2AsRegExp = asRegExp(path2);
|
|
13
|
+
if (path1AsRegExp || path2AsRegExp) {
|
|
14
|
+
return new RegExp((path1AsRegExp ? path1.source : path1) + (path2AsRegExp ? path2.source : path2));
|
|
15
|
+
}
|
|
16
|
+
return path1 + path2;
|
|
17
|
+
}
|
|
18
|
+
export function toStartWithPattern(input) {
|
|
19
|
+
if (asString(input)) {
|
|
20
|
+
const escaped = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
21
|
+
return new RegExp(`^${escaped}.*`);
|
|
22
|
+
}
|
|
23
|
+
let pattern = input.source;
|
|
24
|
+
if (pattern.endsWith('$')) {
|
|
25
|
+
pattern = pattern.slice(0, -1);
|
|
26
|
+
}
|
|
27
|
+
return new RegExp(`${pattern}.*`, input.flags);
|
|
28
|
+
}
|
|
29
|
+
export function methodFromString(method) {
|
|
30
|
+
const handers = new Map([
|
|
31
|
+
['GET', Some(HttpMethod.GET)],
|
|
32
|
+
['POST', Some(HttpMethod.POST)],
|
|
33
|
+
['PUT', Some(HttpMethod.PUT)],
|
|
34
|
+
['DELETE', Some(HttpMethod.DELETE)],
|
|
35
|
+
]);
|
|
36
|
+
return match(method, handers, None(), false);
|
|
37
|
+
}
|
|
38
|
+
function http1NotFoundHandler(context) {
|
|
39
|
+
return HttpResponse.asJson({ error: "Not Found" }, 404);
|
|
40
|
+
}
|
|
41
|
+
function http2NotFoundHandler(context) {
|
|
42
|
+
context.response.respond({
|
|
43
|
+
'content-type': 'application/json; charset=utf-8',
|
|
44
|
+
':status': 404,
|
|
45
|
+
});
|
|
46
|
+
context.response.end();
|
|
47
|
+
}
|
|
48
|
+
export function CreateHttpRouter(groupRoute, notFound) {
|
|
49
|
+
const routes = new Map();
|
|
50
|
+
const add = (route, action) => {
|
|
51
|
+
route.path = processPath(contactPaths(groupRoute.path, route.path));
|
|
52
|
+
routes.set(route, (routeData) => {
|
|
53
|
+
return {
|
|
54
|
+
action,
|
|
55
|
+
pathParams: routeData.params.flatByKey("path"),
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
const addGroup = (route, action) => {
|
|
60
|
+
route.path = processPath(contactPaths(groupRoute.path, route.path));
|
|
61
|
+
routes.set(route, action);
|
|
62
|
+
};
|
|
63
|
+
return {
|
|
64
|
+
get(path, action) {
|
|
65
|
+
add({ path, method: HttpMethod.GET }, action);
|
|
66
|
+
},
|
|
67
|
+
post(path, action) {
|
|
68
|
+
add({ path, method: HttpMethod.POST }, action);
|
|
69
|
+
},
|
|
70
|
+
put(path, action) {
|
|
71
|
+
add({ path, method: HttpMethod.PUT }, action);
|
|
72
|
+
},
|
|
73
|
+
delete(path, action) {
|
|
74
|
+
add({ path, method: HttpMethod.DELETE }, action);
|
|
75
|
+
},
|
|
76
|
+
group(groupRouter) {
|
|
77
|
+
const groupPath = groupRouter.getGroupRoute().path;
|
|
78
|
+
const startWithPattern = toStartWithPattern(groupPath);
|
|
79
|
+
addGroup({ path: startWithPattern }, (routeData) => {
|
|
80
|
+
return groupRouter.match(routeData.val);
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
getMapRoutes() {
|
|
84
|
+
return routes;
|
|
85
|
+
},
|
|
86
|
+
getGroupRoute() {
|
|
87
|
+
return groupRoute;
|
|
88
|
+
},
|
|
89
|
+
match(request) {
|
|
90
|
+
return match(request, this.getMapRoutes(), (routeData) => {
|
|
91
|
+
return {
|
|
92
|
+
action: notFound,
|
|
93
|
+
pathParams: routeData.params.flatByKey("path"),
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
// export function CreateHttp1Router(
|
|
100
|
+
// groupRoute: GroupRoute,
|
|
101
|
+
// notFound: Action<ServerResponse> = http1NotFoundHandler,
|
|
102
|
+
// ): Http1Router {
|
|
103
|
+
// return CreateHttpRouter<ServerResponse, Http1Router>(groupRoute, notFound);
|
|
104
|
+
// }
|
|
105
|
+
// export function CreateHttp2Router(
|
|
106
|
+
// groupRoute: GroupRoute,
|
|
107
|
+
// notFound: Action<ServerHttp2Stream> = http2NotFoundHandler
|
|
108
|
+
// ): Http2Router {
|
|
109
|
+
// return CreateHttpRouter<ServerHttp2Stream, Http2Router>(groupRoute, notFound);
|
|
110
|
+
// }
|
package/build/types.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Option, Result, StdError } from "@smuzi/std";
|
|
2
|
+
export interface HttpServer {
|
|
3
|
+
close(): Promise<Result<boolean, StdError>>;
|
|
4
|
+
}
|
|
5
|
+
export type HttpServerRunError = {
|
|
6
|
+
errno: Option<number>;
|
|
7
|
+
code: Option<string>;
|
|
8
|
+
syscall: Option<string>;
|
|
9
|
+
path: Option<string>;
|
|
10
|
+
port: Option<string>;
|
|
11
|
+
address: Option<string>;
|
|
12
|
+
};
|
package/build/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@smuzi/http-server",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "HTTP server and routing for JavaScript and TypeScript",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"test": "tsx tests/index.ts",
|
|
7
|
+
"build": "tsc --project tsconfig.build.json",
|
|
8
|
+
"prepublishOnly": "npm run build"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"keywords": [
|
|
12
|
+
"http",
|
|
13
|
+
"router",
|
|
14
|
+
"server",
|
|
15
|
+
"https",
|
|
16
|
+
"routing",
|
|
17
|
+
"middleware"
|
|
18
|
+
],
|
|
19
|
+
"author": "Denis Ratushniak <dinisimys2018@gmail.com>",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"./build"
|
|
26
|
+
],
|
|
27
|
+
"exports": {
|
|
28
|
+
".": "./src/index.js"
|
|
29
|
+
},
|
|
30
|
+
"imports": {
|
|
31
|
+
"#lib/*": "./src/*"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@smuzi/std": "workspace:*",
|
|
35
|
+
"@smuzi/faker": "workspace:*",
|
|
36
|
+
"@smuzi/schema": "workspace:*",
|
|
37
|
+
"@smuzi/tests": "workspace:*",
|
|
38
|
+
"@types/node": "^22.15.21",
|
|
39
|
+
"tsx": "^4.20.6",
|
|
40
|
+
"typescript": "^7.0.2"
|
|
41
|
+
}
|
|
42
|
+
}
|