princejs 1.4.2 → 1.5.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/Readme.md +5 -0
- package/bin/create.ts +16 -0
- package/dist/index.js +123 -169
- package/package.json +4 -1
package/Readme.md
CHANGED
package/bin/create.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
3
|
+
|
|
4
|
+
const name = Bun.argv[2] || "prince-app";
|
|
5
|
+
mkdirSync(name, { recursive: true });
|
|
6
|
+
writeFileSync(`${name}/index.ts`, `
|
|
7
|
+
import { prince } from "princejs";
|
|
8
|
+
const app = prince();
|
|
9
|
+
app.get("/", () => ({ message: "Hello from PrinceJS!" }));
|
|
10
|
+
app.listen(3000);
|
|
11
|
+
`);
|
|
12
|
+
console.log("✅ Created", name);
|
|
13
|
+
console.log("👉 To get started:");
|
|
14
|
+
console.log(` cd ${name}`);
|
|
15
|
+
console.log(" bun install princejs");
|
|
16
|
+
console.log(" bun run index.ts");
|
package/dist/index.js
CHANGED
|
@@ -40,11 +40,17 @@ class ResponseBuilder {
|
|
|
40
40
|
this._headers["Location"] = url;
|
|
41
41
|
return this.build();
|
|
42
42
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
stream(cb) {
|
|
44
|
+
const encoder = new TextEncoder;
|
|
45
|
+
const stream = new ReadableStream({
|
|
46
|
+
start(controller) {
|
|
47
|
+
cb((chunk) => controller.enqueue(encoder.encode(chunk)), () => controller.close());
|
|
48
|
+
}
|
|
47
49
|
});
|
|
50
|
+
return new Response(stream, { status: this._status, headers: this._headers });
|
|
51
|
+
}
|
|
52
|
+
build() {
|
|
53
|
+
return new Response(this._body, { status: this._status, headers: this._headers });
|
|
48
54
|
}
|
|
49
55
|
}
|
|
50
56
|
|
|
@@ -54,6 +60,8 @@ class Prince {
|
|
|
54
60
|
middlewares = [];
|
|
55
61
|
errorHandler;
|
|
56
62
|
prefix = "";
|
|
63
|
+
wsRoutes = {};
|
|
64
|
+
openapiData = null;
|
|
57
65
|
constructor(devMode = false) {
|
|
58
66
|
this.devMode = devMode;
|
|
59
67
|
}
|
|
@@ -74,6 +82,27 @@ class Prince {
|
|
|
74
82
|
response() {
|
|
75
83
|
return new ResponseBuilder;
|
|
76
84
|
}
|
|
85
|
+
ws(path, options) {
|
|
86
|
+
this.wsRoutes[path] = options;
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
openapi(path = "/docs") {
|
|
90
|
+
const paths = {};
|
|
91
|
+
for (const route of this.rawRoutes) {
|
|
92
|
+
paths[route.path] ??= {};
|
|
93
|
+
paths[route.path][route.method.toLowerCase()] = {
|
|
94
|
+
summary: "",
|
|
95
|
+
responses: { 200: { description: "OK" } }
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
this.openapiData = {
|
|
99
|
+
openapi: "3.1.0",
|
|
100
|
+
info: { title: "PrinceJS API", version: "1.0.0" },
|
|
101
|
+
paths
|
|
102
|
+
};
|
|
103
|
+
this.get(path, () => this.openapiData);
|
|
104
|
+
return this;
|
|
105
|
+
}
|
|
77
106
|
route(path) {
|
|
78
107
|
const group = new Prince(this.devMode);
|
|
79
108
|
group.prefix = path;
|
|
@@ -86,18 +115,6 @@ class Prince {
|
|
|
86
115
|
post: (subpath, handler) => {
|
|
87
116
|
this.post(path + subpath, handler);
|
|
88
117
|
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
118
|
}
|
|
102
119
|
};
|
|
103
120
|
}
|
|
@@ -116,12 +133,6 @@ class Prince {
|
|
|
116
133
|
patch(path, handler) {
|
|
117
134
|
return this.add("PATCH", path, handler);
|
|
118
135
|
}
|
|
119
|
-
options(path, handler) {
|
|
120
|
-
return this.add("OPTIONS", path, handler);
|
|
121
|
-
}
|
|
122
|
-
head(path, handler) {
|
|
123
|
-
return this.add("HEAD", path, handler);
|
|
124
|
-
}
|
|
125
136
|
add(method, path, handler) {
|
|
126
137
|
if (!path.startsWith("/"))
|
|
127
138
|
path = "/" + path;
|
|
@@ -134,32 +145,31 @@ class Prince {
|
|
|
134
145
|
parseUrl(req) {
|
|
135
146
|
const url = new URL(req.url);
|
|
136
147
|
const query = {};
|
|
137
|
-
for (const [key, value] of url.searchParams.entries())
|
|
148
|
+
for (const [key, value] of url.searchParams.entries())
|
|
138
149
|
query[key] = value;
|
|
139
|
-
}
|
|
140
|
-
return {
|
|
141
|
-
pathname: url.pathname,
|
|
142
|
-
query
|
|
143
|
-
};
|
|
150
|
+
return { pathname: url.pathname, query };
|
|
144
151
|
}
|
|
145
152
|
async parseBody(req) {
|
|
146
153
|
const ct = req.headers.get("content-type") || "";
|
|
147
|
-
if (ct.
|
|
148
|
-
|
|
154
|
+
if (ct.startsWith("multipart/form-data")) {
|
|
155
|
+
const form = await req.formData();
|
|
156
|
+
const files = {};
|
|
157
|
+
const fields = {};
|
|
158
|
+
for (const [k, v] of form.entries()) {
|
|
159
|
+
if (v instanceof File)
|
|
160
|
+
files[k] = v;
|
|
161
|
+
else
|
|
162
|
+
fields[k] = v;
|
|
163
|
+
}
|
|
164
|
+
return { files, fields };
|
|
149
165
|
}
|
|
166
|
+
if (ct.includes("application/json"))
|
|
167
|
+
return await req.json();
|
|
150
168
|
if (ct.includes("application/x-www-form-urlencoded")) {
|
|
151
|
-
|
|
152
|
-
const params = {};
|
|
153
|
-
const pairs = text.split("&");
|
|
154
|
-
for (const pair of pairs) {
|
|
155
|
-
const [key, val] = pair.split("=");
|
|
156
|
-
params[decodeURIComponent(key)] = decodeURIComponent(val || "");
|
|
157
|
-
}
|
|
158
|
-
return params;
|
|
169
|
+
return Object.fromEntries(new URLSearchParams(await req.text()).entries());
|
|
159
170
|
}
|
|
160
|
-
if (ct.
|
|
171
|
+
if (ct.startsWith("text/"))
|
|
161
172
|
return await req.text();
|
|
162
|
-
}
|
|
163
173
|
return null;
|
|
164
174
|
}
|
|
165
175
|
buildRouter() {
|
|
@@ -173,166 +183,110 @@ class Prince {
|
|
|
173
183
|
node.handlers[route.method] = route.handler;
|
|
174
184
|
continue;
|
|
175
185
|
}
|
|
176
|
-
for (
|
|
177
|
-
const part = parts[i];
|
|
186
|
+
for (const part of parts) {
|
|
178
187
|
if (part === "**") {
|
|
179
|
-
if (!node.catchAllChild)
|
|
188
|
+
if (!node.catchAllChild)
|
|
180
189
|
node.catchAllChild = { name: "**", node: new TrieNode };
|
|
181
|
-
}
|
|
182
190
|
node = node.catchAllChild.node;
|
|
183
191
|
break;
|
|
184
|
-
} else if (part === "*") {
|
|
185
|
-
if (!node.wildcardChild)
|
|
186
|
-
node.wildcardChild = new TrieNode;
|
|
187
|
-
node = node.wildcardChild;
|
|
188
192
|
} else if (part.startsWith(":")) {
|
|
189
193
|
const name = part.slice(1);
|
|
190
194
|
if (!node.paramChild)
|
|
191
195
|
node.paramChild = { name, node: new TrieNode };
|
|
192
196
|
node = node.paramChild.node;
|
|
193
197
|
} else {
|
|
194
|
-
|
|
195
|
-
node.children[part] = new TrieNode;
|
|
196
|
-
node = node.children[part];
|
|
198
|
+
node = node.children[part] ??= new TrieNode;
|
|
197
199
|
}
|
|
198
200
|
}
|
|
199
|
-
|
|
200
|
-
node.handlers = Object.create(null);
|
|
201
|
+
node.handlers ??= Object.create(null);
|
|
201
202
|
node.handlers[route.method] = route.handler;
|
|
202
203
|
}
|
|
203
204
|
return root;
|
|
204
205
|
}
|
|
205
|
-
compilePipeline(handler
|
|
206
|
-
const mws = this.middlewares
|
|
207
|
-
|
|
208
|
-
if (!hasMiddleware) {
|
|
206
|
+
compilePipeline(handler) {
|
|
207
|
+
const mws = this.middlewares;
|
|
208
|
+
if (mws.length === 0)
|
|
209
209
|
return async (req, params, query) => {
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const res = await handler(princeReq);
|
|
210
|
+
const r = req;
|
|
211
|
+
r.params = params;
|
|
212
|
+
r.query = query;
|
|
213
|
+
if (["POST", "PUT", "PATCH"].includes(req.method))
|
|
214
|
+
r.body = await this.parseBody(req);
|
|
215
|
+
const res = await handler(r);
|
|
217
216
|
if (res instanceof Response)
|
|
218
217
|
return res;
|
|
219
218
|
if (typeof res === "string")
|
|
220
|
-
return new Response(res
|
|
221
|
-
if (res instanceof Uint8Array || res instanceof ArrayBuffer)
|
|
222
|
-
return new Response(res, { status: 200 });
|
|
219
|
+
return new Response(res);
|
|
223
220
|
return this.json(res);
|
|
224
221
|
};
|
|
225
|
-
}
|
|
226
222
|
return async (req, params, query) => {
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
let
|
|
231
|
-
const
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
return new Response(res, { status: 200 });
|
|
243
|
-
return this.json(res);
|
|
244
|
-
}
|
|
245
|
-
const mw = mws[idx++];
|
|
246
|
-
return await mw(req, runNext);
|
|
223
|
+
const r = req;
|
|
224
|
+
r.params = params;
|
|
225
|
+
r.query = query;
|
|
226
|
+
let i = 0;
|
|
227
|
+
const next = async () => {
|
|
228
|
+
if (i < mws.length)
|
|
229
|
+
return await mws[i++](req, next) ?? new Response(null);
|
|
230
|
+
if (["POST", "PUT", "PATCH"].includes(req.method))
|
|
231
|
+
r.body = await this.parseBody(req);
|
|
232
|
+
const res = await handler(r);
|
|
233
|
+
if (res instanceof Response)
|
|
234
|
+
return res;
|
|
235
|
+
if (typeof res === "string")
|
|
236
|
+
return new Response(res);
|
|
237
|
+
return this.json(res);
|
|
247
238
|
};
|
|
248
|
-
|
|
249
|
-
if (out instanceof Response)
|
|
250
|
-
return out;
|
|
251
|
-
if (out !== undefined) {
|
|
252
|
-
if (typeof out === "string")
|
|
253
|
-
return new Response(out, { status: 200 });
|
|
254
|
-
if (out instanceof Uint8Array || out instanceof ArrayBuffer)
|
|
255
|
-
return new Response(out, { status: 200 });
|
|
256
|
-
return this.json(out);
|
|
257
|
-
}
|
|
258
|
-
return new Response(null, { status: 204 });
|
|
239
|
+
return next();
|
|
259
240
|
};
|
|
260
241
|
}
|
|
242
|
+
async handleFetch(req) {
|
|
243
|
+
const { pathname, query } = this.parseUrl(req);
|
|
244
|
+
const segments = pathname === "/" ? [] : pathname.slice(1).split("/");
|
|
245
|
+
let node = this.buildRouter(), params = {};
|
|
246
|
+
for (const seg of segments) {
|
|
247
|
+
if (node.children[seg])
|
|
248
|
+
node = node.children[seg];
|
|
249
|
+
else if (node.paramChild) {
|
|
250
|
+
params[node.paramChild.name] = seg;
|
|
251
|
+
node = node.paramChild.node;
|
|
252
|
+
} else
|
|
253
|
+
return this.json({ error: "Not Found" }, 404);
|
|
254
|
+
}
|
|
255
|
+
const handler = node.handlers?.[req.method];
|
|
256
|
+
if (!handler)
|
|
257
|
+
return this.json({ error: "Method Not Allowed" }, 405);
|
|
258
|
+
const pipeline = this.compilePipeline(handler);
|
|
259
|
+
return await pipeline(req, params, query);
|
|
260
|
+
}
|
|
261
261
|
listen(port = 3000) {
|
|
262
|
-
const
|
|
263
|
-
const handlerMap = new Map;
|
|
262
|
+
const self = this;
|
|
264
263
|
Bun.serve({
|
|
265
264
|
port,
|
|
266
|
-
fetch
|
|
265
|
+
fetch(req, server) {
|
|
266
|
+
const { pathname } = new URL(req.url);
|
|
267
|
+
const wsRoute = self.wsRoutes[pathname];
|
|
268
|
+
if (wsRoute) {
|
|
269
|
+
if (server.upgrade(req, { data: { route: wsRoute } }))
|
|
270
|
+
return;
|
|
271
|
+
return new Response("Upgrade failed", { status: 500 });
|
|
272
|
+
}
|
|
267
273
|
try {
|
|
268
|
-
|
|
269
|
-
const segments = pathname === "/" ? [] : pathname.slice(1).split("/");
|
|
270
|
-
let node = root;
|
|
271
|
-
const params = {};
|
|
272
|
-
let matched = true;
|
|
273
|
-
if (segments.length === 0) {
|
|
274
|
-
if (!node.handlers)
|
|
275
|
-
return this.json({ error: "Route not found" }, 404);
|
|
276
|
-
const handler2 = node.handlers[req.method];
|
|
277
|
-
if (!handler2)
|
|
278
|
-
return this.json({ error: "Method not allowed" }, 405);
|
|
279
|
-
let methodMap2 = handlerMap.get(node);
|
|
280
|
-
if (!methodMap2) {
|
|
281
|
-
methodMap2 = {};
|
|
282
|
-
handlerMap.set(node, methodMap2);
|
|
283
|
-
}
|
|
284
|
-
if (!methodMap2[req.method]) {
|
|
285
|
-
methodMap2[req.method] = this.compilePipeline(handler2, (_) => params);
|
|
286
|
-
}
|
|
287
|
-
return await methodMap2[req.method](req, params, query);
|
|
288
|
-
}
|
|
289
|
-
for (let i = 0;i < segments.length; i++) {
|
|
290
|
-
const seg = segments[i];
|
|
291
|
-
if (node.children[seg]) {
|
|
292
|
-
node = node.children[seg];
|
|
293
|
-
continue;
|
|
294
|
-
}
|
|
295
|
-
if (node.paramChild) {
|
|
296
|
-
params[node.paramChild.name] = seg;
|
|
297
|
-
node = node.paramChild.node;
|
|
298
|
-
continue;
|
|
299
|
-
}
|
|
300
|
-
if (node.wildcardChild) {
|
|
301
|
-
node = node.wildcardChild;
|
|
302
|
-
continue;
|
|
303
|
-
}
|
|
304
|
-
if (node.catchAllChild) {
|
|
305
|
-
const remaining = segments.slice(i).join("/");
|
|
306
|
-
if (node.catchAllChild.name)
|
|
307
|
-
params[node.catchAllChild.name] = remaining;
|
|
308
|
-
node = node.catchAllChild.node;
|
|
309
|
-
break;
|
|
310
|
-
}
|
|
311
|
-
matched = false;
|
|
312
|
-
break;
|
|
313
|
-
}
|
|
314
|
-
if (!matched || !node || !node.handlers) {
|
|
315
|
-
return this.json({ error: "Route not found" }, 404);
|
|
316
|
-
}
|
|
317
|
-
const handler = node.handlers[req.method];
|
|
318
|
-
if (!handler)
|
|
319
|
-
return this.json({ error: "Method not allowed" }, 405);
|
|
320
|
-
let methodMap = handlerMap.get(node);
|
|
321
|
-
if (!methodMap) {
|
|
322
|
-
methodMap = {};
|
|
323
|
-
handlerMap.set(node, methodMap);
|
|
324
|
-
}
|
|
325
|
-
if (!methodMap[req.method]) {
|
|
326
|
-
methodMap[req.method] = this.compilePipeline(handler, (_) => params);
|
|
327
|
-
}
|
|
328
|
-
return await methodMap[req.method](req, params, query);
|
|
274
|
+
return self.handleFetch(req);
|
|
329
275
|
} catch (err) {
|
|
330
|
-
if (
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
276
|
+
if (self.errorHandler)
|
|
277
|
+
return self.errorHandler(err, req);
|
|
278
|
+
return self.json({ error: String(err) }, 500);
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
websocket: {
|
|
282
|
+
open(ws) {
|
|
283
|
+
ws.data.route?.open?.(ws);
|
|
284
|
+
},
|
|
285
|
+
message(ws, msg) {
|
|
286
|
+
ws.data.route?.message?.(ws, msg);
|
|
287
|
+
},
|
|
288
|
+
close(ws) {
|
|
289
|
+
ws.data.route?.close?.(ws);
|
|
336
290
|
}
|
|
337
291
|
}
|
|
338
292
|
});
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "princejs",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
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",
|
|
7
|
+
"bin": {
|
|
8
|
+
"create-princejs": "bin/create.ts"
|
|
9
|
+
},
|
|
7
10
|
"exports": {
|
|
8
11
|
".": {
|
|
9
12
|
"import": "./dist/prince.js",
|