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 CHANGED
@@ -9,8 +9,13 @@
9
9
  ## 🚀 Get Started
10
10
 
11
11
  ```bash
12
+ # Create a new PrinceJS app
12
13
  bun create princejs my-app
14
+
15
+ # Move into the project
13
16
  cd my-app
17
+
18
+ # Run in development mode
14
19
  bun dev
15
20
  ```
16
21
 
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
- build() {
44
- return new Response(this._body, {
45
- status: this._status,
46
- headers: this._headers
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.includes("application/json")) {
148
- return await req.json();
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
- const text = await req.text();
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.includes("text/")) {
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 (let i = 0;i < parts.length; i++) {
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
- if (!node.children[part])
195
- node.children[part] = new TrieNode;
196
- node = node.children[part];
198
+ node = node.children[part] ??= new TrieNode;
197
199
  }
198
200
  }
199
- if (!node.handlers)
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, paramsGetter) {
206
- const mws = this.middlewares.slice();
207
- const hasMiddleware = mws.length > 0;
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 princeReq = req;
211
- princeReq.params = params;
212
- princeReq.query = query;
213
- if (req.method === "POST" || req.method === "PUT" || req.method === "PATCH") {
214
- princeReq.body = await this.parseBody(req);
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, { status: 200 });
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 princeReq = req;
228
- princeReq.params = params;
229
- princeReq.query = query;
230
- let idx = 0;
231
- const runNext = async () => {
232
- if (idx >= mws.length) {
233
- if (req.method === "POST" || req.method === "PUT" || req.method === "PATCH") {
234
- princeReq.body = await this.parseBody(req);
235
- }
236
- const res = await handler(princeReq);
237
- if (res instanceof Response)
238
- return res;
239
- if (typeof res === "string")
240
- return new Response(res, { status: 200 });
241
- if (res instanceof Uint8Array || res instanceof ArrayBuffer)
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
- const out = await runNext();
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 root = this.buildRouter();
263
- const handlerMap = new Map;
262
+ const self = this;
264
263
  Bun.serve({
265
264
  port,
266
- fetch: async (req) => {
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
- const { pathname, query } = this.parseUrl(req);
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 (this.errorHandler) {
331
- try {
332
- return this.errorHandler(err, req);
333
- } catch {}
334
- }
335
- return this.json({ error: String(err) }, 500);
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.4.2",
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",