princejs 1.3.3 → 1.3.5
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/middleware.js +110 -0
- package/dist/prince.js +150 -21
- package/dist/validation.js +23 -0
- package/package.json +5 -2
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/middleware.ts
|
|
3
|
+
var MIDDLEWARE_EXECUTED = Symbol("middlewareExecuted");
|
|
4
|
+
var cors = (options) => {
|
|
5
|
+
const origin = options?.origin || "*";
|
|
6
|
+
const methods = options?.methods || "GET,POST,PUT,DELETE,PATCH,OPTIONS";
|
|
7
|
+
const headers = options?.headers || "Content-Type,Authorization";
|
|
8
|
+
const credentials = options?.credentials || false;
|
|
9
|
+
return async (req, next) => {
|
|
10
|
+
if (req[MIDDLEWARE_EXECUTED]?.cors) {
|
|
11
|
+
return await next();
|
|
12
|
+
}
|
|
13
|
+
if (!req[MIDDLEWARE_EXECUTED]) {
|
|
14
|
+
req[MIDDLEWARE_EXECUTED] = {};
|
|
15
|
+
}
|
|
16
|
+
req[MIDDLEWARE_EXECUTED].cors = true;
|
|
17
|
+
if (req.method === "OPTIONS") {
|
|
18
|
+
return new Response(null, {
|
|
19
|
+
status: 204,
|
|
20
|
+
headers: {
|
|
21
|
+
"Access-Control-Allow-Origin": origin,
|
|
22
|
+
"Access-Control-Allow-Methods": methods,
|
|
23
|
+
"Access-Control-Allow-Headers": headers,
|
|
24
|
+
...credentials ? { "Access-Control-Allow-Credentials": "true" } : {}
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const res = await next();
|
|
29
|
+
if (!res)
|
|
30
|
+
return res;
|
|
31
|
+
const newHeaders = new Headers(res.headers);
|
|
32
|
+
newHeaders.set("Access-Control-Allow-Origin", origin);
|
|
33
|
+
if (credentials)
|
|
34
|
+
newHeaders.set("Access-Control-Allow-Credentials", "true");
|
|
35
|
+
return new Response(res.body, {
|
|
36
|
+
status: res.status,
|
|
37
|
+
statusText: res.statusText,
|
|
38
|
+
headers: newHeaders
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
var logger = (options) => {
|
|
43
|
+
const format = options?.format || "dev";
|
|
44
|
+
const colors = options?.colors !== false;
|
|
45
|
+
const colorize = (code, text) => {
|
|
46
|
+
if (!colors)
|
|
47
|
+
return text;
|
|
48
|
+
if (code >= 500)
|
|
49
|
+
return `\x1B[31m${text}\x1B[0m`;
|
|
50
|
+
if (code >= 400)
|
|
51
|
+
return `\x1B[33m${text}\x1B[0m`;
|
|
52
|
+
if (code >= 300)
|
|
53
|
+
return `\x1B[36m${text}\x1B[0m`;
|
|
54
|
+
if (code >= 200)
|
|
55
|
+
return `\x1B[32m${text}\x1B[0m`;
|
|
56
|
+
return text;
|
|
57
|
+
};
|
|
58
|
+
return async (req, next) => {
|
|
59
|
+
if (req[MIDDLEWARE_EXECUTED]?.logger) {
|
|
60
|
+
return await next();
|
|
61
|
+
}
|
|
62
|
+
if (!req[MIDDLEWARE_EXECUTED]) {
|
|
63
|
+
req[MIDDLEWARE_EXECUTED] = {};
|
|
64
|
+
}
|
|
65
|
+
req[MIDDLEWARE_EXECUTED].logger = true;
|
|
66
|
+
const start = Date.now();
|
|
67
|
+
const pathname = new URL(req.url).pathname;
|
|
68
|
+
const res = await next();
|
|
69
|
+
if (!res)
|
|
70
|
+
return res;
|
|
71
|
+
const duration = Date.now() - start;
|
|
72
|
+
const status = res.status;
|
|
73
|
+
if (format === "dev") {
|
|
74
|
+
console.log(`${colorize(status, req.method)} ${pathname} ${colorize(status, String(status))} ${duration}ms`);
|
|
75
|
+
} else if (format === "tiny") {
|
|
76
|
+
console.log(`${req.method} ${pathname} ${status} - ${duration}ms`);
|
|
77
|
+
} else {
|
|
78
|
+
const date = new Date().toISOString();
|
|
79
|
+
console.log(`[${date}] ${req.method} ${pathname} ${status} ${duration}ms`);
|
|
80
|
+
}
|
|
81
|
+
return res;
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
var rateLimit = (options) => {
|
|
85
|
+
const store = new Map;
|
|
86
|
+
return async (req, next) => {
|
|
87
|
+
const ip = req.headers.get("x-forwarded-for") || "unknown";
|
|
88
|
+
const now = Date.now();
|
|
89
|
+
const windowMs = options.window * 1000;
|
|
90
|
+
let record = store.get(ip);
|
|
91
|
+
if (!record || now > record.resetAt) {
|
|
92
|
+
record = { count: 1, resetAt: now + windowMs };
|
|
93
|
+
store.set(ip, record);
|
|
94
|
+
return await next();
|
|
95
|
+
}
|
|
96
|
+
if (record.count >= options.max) {
|
|
97
|
+
return new Response(JSON.stringify({ error: options.message || "Too many requests" }), {
|
|
98
|
+
status: 429,
|
|
99
|
+
headers: { "Content-Type": "application/json" }
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
record.count++;
|
|
103
|
+
return await next();
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
export {
|
|
107
|
+
rateLimit,
|
|
108
|
+
logger,
|
|
109
|
+
cors
|
|
110
|
+
};
|
package/dist/prince.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// prince.ts
|
|
2
|
+
// src/prince.ts
|
|
3
3
|
class TrieNode {
|
|
4
4
|
children = Object.create(null);
|
|
5
5
|
paramChild;
|
|
@@ -8,11 +8,52 @@ class TrieNode {
|
|
|
8
8
|
handlers = null;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
class ResponseBuilder {
|
|
12
|
+
_status = 200;
|
|
13
|
+
_headers = {};
|
|
14
|
+
_body = null;
|
|
15
|
+
status(code) {
|
|
16
|
+
this._status = code;
|
|
17
|
+
return this;
|
|
18
|
+
}
|
|
19
|
+
header(key, value) {
|
|
20
|
+
this._headers[key] = value;
|
|
21
|
+
return this;
|
|
22
|
+
}
|
|
23
|
+
json(data) {
|
|
24
|
+
this._headers["Content-Type"] = "application/json";
|
|
25
|
+
this._body = JSON.stringify(data);
|
|
26
|
+
return this.build();
|
|
27
|
+
}
|
|
28
|
+
text(data) {
|
|
29
|
+
this._headers["Content-Type"] = "text/plain";
|
|
30
|
+
this._body = data;
|
|
31
|
+
return this.build();
|
|
32
|
+
}
|
|
33
|
+
html(data) {
|
|
34
|
+
this._headers["Content-Type"] = "text/html";
|
|
35
|
+
this._body = data;
|
|
36
|
+
return this.build();
|
|
37
|
+
}
|
|
38
|
+
redirect(url, status = 302) {
|
|
39
|
+
this._status = status;
|
|
40
|
+
this._headers["Location"] = url;
|
|
41
|
+
return this.build();
|
|
42
|
+
}
|
|
43
|
+
build() {
|
|
44
|
+
return new Response(this._body, {
|
|
45
|
+
status: this._status,
|
|
46
|
+
headers: this._headers
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
11
51
|
class Prince {
|
|
12
52
|
devMode;
|
|
13
53
|
rawRoutes = [];
|
|
14
54
|
middlewares = [];
|
|
15
55
|
errorHandler;
|
|
56
|
+
prefix = "";
|
|
16
57
|
constructor(devMode = false) {
|
|
17
58
|
this.devMode = devMode;
|
|
18
59
|
}
|
|
@@ -30,6 +71,36 @@ class Prince {
|
|
|
30
71
|
headers: { "Content-Type": "application/json" }
|
|
31
72
|
});
|
|
32
73
|
}
|
|
74
|
+
response() {
|
|
75
|
+
return new ResponseBuilder;
|
|
76
|
+
}
|
|
77
|
+
route(path) {
|
|
78
|
+
const group = new Prince(this.devMode);
|
|
79
|
+
group.prefix = path;
|
|
80
|
+
group.middlewares = [...this.middlewares];
|
|
81
|
+
return {
|
|
82
|
+
get: (subpath, handler) => {
|
|
83
|
+
this.get(path + subpath, handler);
|
|
84
|
+
return group;
|
|
85
|
+
},
|
|
86
|
+
post: (subpath, handler) => {
|
|
87
|
+
this.post(path + subpath, handler);
|
|
88
|
+
return group;
|
|
89
|
+
},
|
|
90
|
+
put: (subpath, handler) => {
|
|
91
|
+
this.put(path + subpath, handler);
|
|
92
|
+
return group;
|
|
93
|
+
},
|
|
94
|
+
delete: (subpath, handler) => {
|
|
95
|
+
this.delete(path + subpath, handler);
|
|
96
|
+
return group;
|
|
97
|
+
},
|
|
98
|
+
patch: (subpath, handler) => {
|
|
99
|
+
this.patch(path + subpath, handler);
|
|
100
|
+
return group;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
33
104
|
get(path, handler) {
|
|
34
105
|
return this.add("GET", path, handler);
|
|
35
106
|
}
|
|
@@ -71,6 +142,44 @@ class Prince {
|
|
|
71
142
|
const end = q !== -1 ? q : h !== -1 ? h : u.length;
|
|
72
143
|
return u.slice(start, end);
|
|
73
144
|
}
|
|
145
|
+
parseQuery(url) {
|
|
146
|
+
const q = url.indexOf("?");
|
|
147
|
+
if (q === -1)
|
|
148
|
+
return {};
|
|
149
|
+
const query = {};
|
|
150
|
+
const search = url.slice(q + 1);
|
|
151
|
+
const pairs = search.split("&");
|
|
152
|
+
for (let i = 0;i < pairs.length; i++) {
|
|
153
|
+
const pair = pairs[i];
|
|
154
|
+
const eq = pair.indexOf("=");
|
|
155
|
+
if (eq === -1) {
|
|
156
|
+
query[decodeURIComponent(pair)] = "";
|
|
157
|
+
} else {
|
|
158
|
+
query[decodeURIComponent(pair.slice(0, eq))] = decodeURIComponent(pair.slice(eq + 1));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return query;
|
|
162
|
+
}
|
|
163
|
+
async parseBody(req) {
|
|
164
|
+
const ct = req.headers.get("content-type") || "";
|
|
165
|
+
if (ct.includes("application/json")) {
|
|
166
|
+
return await req.json();
|
|
167
|
+
}
|
|
168
|
+
if (ct.includes("application/x-www-form-urlencoded")) {
|
|
169
|
+
const text = await req.text();
|
|
170
|
+
const params = {};
|
|
171
|
+
const pairs = text.split("&");
|
|
172
|
+
for (const pair of pairs) {
|
|
173
|
+
const [key, val] = pair.split("=");
|
|
174
|
+
params[decodeURIComponent(key)] = decodeURIComponent(val || "");
|
|
175
|
+
}
|
|
176
|
+
return params;
|
|
177
|
+
}
|
|
178
|
+
if (ct.includes("text/")) {
|
|
179
|
+
return await req.text();
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
74
183
|
buildRouter() {
|
|
75
184
|
const root = new TrieNode;
|
|
76
185
|
for (const route of this.rawRoutes) {
|
|
@@ -85,7 +194,9 @@ class Prince {
|
|
|
85
194
|
for (let i = 0;i < parts.length; i++) {
|
|
86
195
|
const part = parts[i];
|
|
87
196
|
if (part === "**") {
|
|
88
|
-
node.catchAllChild
|
|
197
|
+
if (!node.catchAllChild) {
|
|
198
|
+
node.catchAllChild = { name: "**", node: new TrieNode };
|
|
199
|
+
}
|
|
89
200
|
node = node.catchAllChild.node;
|
|
90
201
|
break;
|
|
91
202
|
} else if (part === "*") {
|
|
@@ -109,14 +220,38 @@ class Prince {
|
|
|
109
220
|
}
|
|
110
221
|
return root;
|
|
111
222
|
}
|
|
112
|
-
compilePipeline(handler,
|
|
223
|
+
compilePipeline(handler, paramsGetter) {
|
|
113
224
|
const mws = this.middlewares.slice();
|
|
114
|
-
|
|
225
|
+
const hasMiddleware = mws.length > 0;
|
|
226
|
+
if (!hasMiddleware) {
|
|
227
|
+
return async (req, params, query) => {
|
|
228
|
+
const princeReq = req;
|
|
229
|
+
princeReq.params = params;
|
|
230
|
+
princeReq.query = query;
|
|
231
|
+
if (req.method === "POST" || req.method === "PUT" || req.method === "PATCH") {
|
|
232
|
+
princeReq.body = await this.parseBody(req);
|
|
233
|
+
}
|
|
234
|
+
const res = await handler(princeReq);
|
|
235
|
+
if (res instanceof Response)
|
|
236
|
+
return res;
|
|
237
|
+
if (typeof res === "string")
|
|
238
|
+
return new Response(res, { status: 200 });
|
|
239
|
+
if (res instanceof Uint8Array || res instanceof ArrayBuffer)
|
|
240
|
+
return new Response(res, { status: 200 });
|
|
241
|
+
return this.json(res);
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
return async (req, params, query) => {
|
|
245
|
+
const princeReq = req;
|
|
246
|
+
princeReq.params = params;
|
|
247
|
+
princeReq.query = query;
|
|
115
248
|
let idx = 0;
|
|
116
249
|
const runNext = async () => {
|
|
117
250
|
if (idx >= mws.length) {
|
|
118
|
-
|
|
119
|
-
|
|
251
|
+
if (req.method === "POST" || req.method === "PUT" || req.method === "PATCH") {
|
|
252
|
+
princeReq.body = await this.parseBody(req);
|
|
253
|
+
}
|
|
254
|
+
const res = await handler(princeReq);
|
|
120
255
|
if (res instanceof Response)
|
|
121
256
|
return res;
|
|
122
257
|
if (typeof res === "string")
|
|
@@ -126,8 +261,7 @@ class Prince {
|
|
|
126
261
|
return this.json(res);
|
|
127
262
|
}
|
|
128
263
|
const mw = mws[idx++];
|
|
129
|
-
|
|
130
|
-
return maybe;
|
|
264
|
+
return await mw(req, runNext);
|
|
131
265
|
};
|
|
132
266
|
const out = await runNext();
|
|
133
267
|
if (out instanceof Response)
|
|
@@ -150,9 +284,10 @@ class Prince {
|
|
|
150
284
|
fetch: async (req) => {
|
|
151
285
|
try {
|
|
152
286
|
const pathname = this.fastPathname(req);
|
|
287
|
+
const query = this.parseQuery(req.url);
|
|
153
288
|
const segments = pathname === "/" ? [] : pathname.slice(1).split("/");
|
|
154
289
|
let node = root;
|
|
155
|
-
const params =
|
|
290
|
+
const params = {};
|
|
156
291
|
let matched = true;
|
|
157
292
|
if (segments.length === 0) {
|
|
158
293
|
if (!node.handlers)
|
|
@@ -162,20 +297,16 @@ class Prince {
|
|
|
162
297
|
return this.json({ error: "Method not allowed" }, 405);
|
|
163
298
|
let methodMap2 = handlerMap.get(node);
|
|
164
299
|
if (!methodMap2) {
|
|
165
|
-
methodMap2 =
|
|
300
|
+
methodMap2 = {};
|
|
166
301
|
handlerMap.set(node, methodMap2);
|
|
167
302
|
}
|
|
168
303
|
if (!methodMap2[req.method]) {
|
|
169
304
|
methodMap2[req.method] = this.compilePipeline(handler2, (_) => params);
|
|
170
305
|
}
|
|
171
|
-
return await methodMap2[req.method](req);
|
|
306
|
+
return await methodMap2[req.method](req, params, query);
|
|
172
307
|
}
|
|
173
308
|
for (let i = 0;i < segments.length; i++) {
|
|
174
309
|
const seg = segments[i];
|
|
175
|
-
if (!node) {
|
|
176
|
-
matched = false;
|
|
177
|
-
break;
|
|
178
|
-
}
|
|
179
310
|
if (node.children[seg]) {
|
|
180
311
|
node = node.children[seg];
|
|
181
312
|
continue;
|
|
@@ -194,7 +325,6 @@ class Prince {
|
|
|
194
325
|
if (node.catchAllChild.name)
|
|
195
326
|
params[node.catchAllChild.name] = remaining;
|
|
196
327
|
node = node.catchAllChild.node;
|
|
197
|
-
i = segments.length;
|
|
198
328
|
break;
|
|
199
329
|
}
|
|
200
330
|
matched = false;
|
|
@@ -202,19 +332,18 @@ class Prince {
|
|
|
202
332
|
}
|
|
203
333
|
if (!matched || !node || !node.handlers)
|
|
204
334
|
return this.json({ error: "Route not found" }, 404);
|
|
205
|
-
const
|
|
206
|
-
const handler = byMethod[req.method] ?? byMethod["GET"];
|
|
335
|
+
const handler = node.handlers[req.method];
|
|
207
336
|
if (!handler)
|
|
208
337
|
return this.json({ error: "Method not allowed" }, 405);
|
|
209
338
|
let methodMap = handlerMap.get(node);
|
|
210
339
|
if (!methodMap) {
|
|
211
|
-
methodMap =
|
|
340
|
+
methodMap = {};
|
|
212
341
|
handlerMap.set(node, methodMap);
|
|
213
342
|
}
|
|
214
343
|
if (!methodMap[req.method]) {
|
|
215
344
|
methodMap[req.method] = this.compilePipeline(handler, (_) => params);
|
|
216
345
|
}
|
|
217
|
-
return await methodMap[req.method](req);
|
|
346
|
+
return await methodMap[req.method](req, params, query);
|
|
218
347
|
} catch (err) {
|
|
219
348
|
if (this.errorHandler) {
|
|
220
349
|
try {
|
|
@@ -225,7 +354,7 @@ class Prince {
|
|
|
225
354
|
}
|
|
226
355
|
}
|
|
227
356
|
});
|
|
228
|
-
console.log(
|
|
357
|
+
console.log(`\uD83D\uDE80 PrinceJS running at http://localhost:${port}`);
|
|
229
358
|
}
|
|
230
359
|
}
|
|
231
360
|
var prince = (dev = false) => new Prince(dev);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/validation.ts
|
|
3
|
+
var validate = (schema, source = "body") => {
|
|
4
|
+
return async (req, next) => {
|
|
5
|
+
try {
|
|
6
|
+
const data = source === "body" ? req.body : source === "query" ? req.query : req.params;
|
|
7
|
+
const validated = schema.parse(data);
|
|
8
|
+
req[`validated${source.charAt(0).toUpperCase() + source.slice(1)}`] = validated;
|
|
9
|
+
return await next();
|
|
10
|
+
} catch (err) {
|
|
11
|
+
return new Response(JSON.stringify({
|
|
12
|
+
error: "Validation failed",
|
|
13
|
+
details: err.errors || err.message
|
|
14
|
+
}), {
|
|
15
|
+
status: 400,
|
|
16
|
+
headers: { "Content-Type": "application/json" }
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export {
|
|
22
|
+
validate
|
|
23
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "princejs",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.5",
|
|
4
4
|
"description": "An easy and fast backend framework — by a 13yo developer, for developers.",
|
|
5
5
|
"main": "dist/prince.js",
|
|
6
6
|
"types": "dist/prince.d.ts",
|
|
@@ -25,7 +25,10 @@
|
|
|
25
25
|
"@types/bun": "^1.3.2",
|
|
26
26
|
"bun-types": "latest"
|
|
27
27
|
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"zod": "^4.1.12"
|
|
30
|
+
},
|
|
28
31
|
"scripts": {
|
|
29
|
-
"build": "bun build prince.ts --outdir dist --target bun"
|
|
32
|
+
"build": "bun build ./src/prince.ts ./src/middleware.ts ./src/validation.ts --outdir dist --target bun"
|
|
30
33
|
}
|
|
31
34
|
}
|