princejs 1.4.2 → 1.5.2

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/dist/create.js ADDED
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // bin/create.ts
5
+ import { existsSync, mkdirSync, writeFileSync } from "fs";
6
+ import { join } from "path";
7
+ var name = Bun.argv[2];
8
+ if (!name) {
9
+ console.error("\u274C Error: Please provide a project name");
10
+ console.log("Usage: bunx create-princejs <project-name>");
11
+ process.exit(1);
12
+ }
13
+ if (existsSync(name)) {
14
+ console.error(`\u274C Error: Directory "${name}" already exists`);
15
+ process.exit(1);
16
+ }
17
+ console.log(`\uD83C\uDFA8 Creating PrinceJS project: ${name}...`);
18
+ mkdirSync(name, { recursive: true });
19
+ mkdirSync(join(name, "src"), { recursive: true });
20
+ var packageJson = {
21
+ name,
22
+ version: "1.0.0",
23
+ type: "module",
24
+ scripts: {
25
+ dev: "bun --watch src/index.ts",
26
+ start: "bun src/index.ts"
27
+ },
28
+ dependencies: {
29
+ princejs: "latest"
30
+ },
31
+ devDependencies: {
32
+ "@types/bun": "latest",
33
+ "bun-types": "latest"
34
+ }
35
+ };
36
+ writeFileSync(join(name, "package.json"), JSON.stringify(packageJson, null, 2));
37
+ var indexContent = `import { prince } from "princejs";
38
+ import { cors, logger } from "princejs/middleware";
39
+
40
+ const app = prince(true); // dev mode enabled
41
+
42
+ // Middleware
43
+ app.use(cors());
44
+ app.use(logger({ format: "dev" }));
45
+
46
+ // Routes
47
+ app.get("/", () => {
48
+ return { message: "Welcome to PrinceJS! \uD83D\uDE80" };
49
+ });
50
+
51
+ app.get("/hello/:name", (req) => {
52
+ return { message: \`Hello, \${req.params.name}!\` };
53
+ });
54
+
55
+ app.post("/echo", (req) => {
56
+ return { echo: req.body };
57
+ });
58
+
59
+ // WebSocket example
60
+ app.ws("/ws", {
61
+ open: (ws) => {
62
+ console.log("Client connected");
63
+ ws.send("Welcome to WebSocket!");
64
+ },
65
+ message: (ws, msg) => {
66
+ console.log("Received:", msg);
67
+ ws.send(\`Echo: \${msg}\`);
68
+ },
69
+ close: (ws) => {
70
+ console.log("Client disconnected");
71
+ }
72
+ });
73
+
74
+ // Start server
75
+ const PORT = process.env.PORT || 3000;
76
+ app.listen(PORT);
77
+ `;
78
+ writeFileSync(join(name, "src", "index.ts"), indexContent);
79
+ var tsconfigContent = {
80
+ compilerOptions: {
81
+ lib: ["ESNext"],
82
+ target: "ESNext",
83
+ module: "ESNext",
84
+ moduleDetection: "force",
85
+ jsx: "react-jsx",
86
+ allowJs: true,
87
+ moduleResolution: "bundler",
88
+ allowImportingTsExtensions: true,
89
+ verbatimModuleSyntax: true,
90
+ noEmit: true,
91
+ strict: true,
92
+ skipLibCheck: true,
93
+ noFallthroughCasesInSwitch: true,
94
+ noUnusedLocals: false,
95
+ noUnusedParameters: false,
96
+ noPropertyAccessFromIndexSignature: false,
97
+ types: ["bun-types"]
98
+ }
99
+ };
100
+ writeFileSync(join(name, "tsconfig.json"), JSON.stringify(tsconfigContent, null, 2));
101
+ var gitignoreContent = `node_modules
102
+ .DS_Store
103
+ *.log
104
+ dist
105
+ .env
106
+ .env.local
107
+ `;
108
+ writeFileSync(join(name, ".gitignore"), gitignoreContent);
109
+ var readmeContent = `# ${name}
110
+
111
+ A PrinceJS application.
112
+
113
+ ## Getting Started
114
+
115
+ Install dependencies:
116
+ \`\`\`bash
117
+ bun install
118
+ \`\`\`
119
+
120
+ Run the development server:
121
+ \`\`\`bash
122
+ bun run dev
123
+ \`\`\`
124
+
125
+ Your server will be running at \`http://localhost:3000\`
126
+
127
+ ## Available Endpoints
128
+
129
+ - \`GET /\` - Welcome message
130
+ - \`GET /hello/:name\` - Personalized greeting
131
+ - \`POST /echo\` - Echo back request body
132
+ - \`WS /ws\` - WebSocket connection
133
+
134
+ ## Learn More
135
+
136
+ - [PrinceJS Documentation](https://github.com/MatthewTheCoder1218/princejs)
137
+ - [Bun Documentation](https://bun.sh/docs)
138
+ `;
139
+ writeFileSync(join(name, "README.md"), readmeContent);
140
+ var envContent = `PORT=3000
141
+ `;
142
+ writeFileSync(join(name, ".env.example"), envContent);
143
+ console.log(`
144
+ \u2705 Project created successfully!
145
+ `);
146
+ console.log("\uD83D\uDCC2 Next steps:");
147
+ console.log(` cd ${name}`);
148
+ console.log(" bun install");
149
+ console.log(` bun run dev
150
+ `);
151
+ console.log("\uD83D\uDE80 Your server will start at http://localhost:3000");
152
+ console.log(`\uD83D\uDCDA Check README.md for more information
153
+ `);
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
 
@@ -53,7 +59,8 @@ class Prince {
53
59
  rawRoutes = [];
54
60
  middlewares = [];
55
61
  errorHandler;
56
- prefix = "";
62
+ wsRoutes = {};
63
+ openapiData = null;
57
64
  constructor(devMode = false) {
58
65
  this.devMode = devMode;
59
66
  }
@@ -74,32 +81,26 @@ class Prince {
74
81
  response() {
75
82
  return new ResponseBuilder;
76
83
  }
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
- }
84
+ ws(path, options) {
85
+ this.wsRoutes[path] = options;
86
+ return this;
87
+ }
88
+ openapi(path = "/docs") {
89
+ const paths = {};
90
+ for (const route of this.rawRoutes) {
91
+ paths[route.path] ??= {};
92
+ paths[route.path][route.method.toLowerCase()] = {
93
+ summary: "",
94
+ responses: { 200: { description: "OK" } }
95
+ };
96
+ }
97
+ this.openapiData = {
98
+ openapi: "3.1.0",
99
+ info: { title: "PrinceJS API", version: "1.0.0" },
100
+ paths
102
101
  };
102
+ this.get(path, () => this.openapiData);
103
+ return this;
103
104
  }
104
105
  get(path, handler) {
105
106
  return this.add("GET", path, handler);
@@ -116,227 +117,140 @@ class Prince {
116
117
  patch(path, handler) {
117
118
  return this.add("PATCH", path, handler);
118
119
  }
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
120
  add(method, path, handler) {
126
121
  if (!path.startsWith("/"))
127
122
  path = "/" + path;
128
- if (path.length > 1 && path.endsWith("/"))
123
+ if (path !== "/" && path.endsWith("/"))
129
124
  path = path.slice(0, -1);
130
125
  const parts = path === "/" ? [""] : path.split("/").slice(1);
131
- this.rawRoutes.push({ method: method.toUpperCase(), path, parts, handler });
126
+ this.rawRoutes.push({ method, path, parts, handler });
132
127
  return this;
133
128
  }
134
129
  parseUrl(req) {
135
130
  const url = new URL(req.url);
136
131
  const query = {};
137
- for (const [key, value] of url.searchParams.entries()) {
138
- query[key] = value;
139
- }
140
- return {
141
- pathname: url.pathname,
142
- query
143
- };
132
+ for (const [k, v] of url.searchParams.entries())
133
+ query[k] = v;
134
+ return { pathname: url.pathname, query };
144
135
  }
145
136
  async parseBody(req) {
146
137
  const ct = req.headers.get("content-type") || "";
147
- if (ct.includes("application/json")) {
138
+ if (ct.includes("application/json"))
148
139
  return await req.json();
149
- }
150
- 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 || "");
140
+ if (ct.includes("application/x-www-form-urlencoded"))
141
+ return Object.fromEntries(new URLSearchParams(await req.text()).entries());
142
+ if (ct.startsWith("multipart/form-data")) {
143
+ const fd = await req.formData();
144
+ const files = {};
145
+ const fields = {};
146
+ for (const [k, v] of fd.entries()) {
147
+ if (v instanceof File)
148
+ files[k] = v;
149
+ else
150
+ fields[k] = v;
157
151
  }
158
- return params;
152
+ return { files, fields };
159
153
  }
160
- if (ct.includes("text/")) {
154
+ if (ct.startsWith("text/"))
161
155
  return await req.text();
162
- }
163
156
  return null;
164
157
  }
165
158
  buildRouter() {
166
159
  const root = new TrieNode;
167
- for (const route of this.rawRoutes) {
160
+ for (const r of this.rawRoutes) {
168
161
  let node = root;
169
- const parts = route.parts;
170
- if (parts.length === 1 && parts[0] === "") {
171
- if (!node.handlers)
172
- node.handlers = Object.create(null);
173
- node.handlers[route.method] = route.handler;
162
+ if (r.parts.length === 1 && r.parts[0] === "") {
163
+ node.handlers ??= {};
164
+ node.handlers[r.method] = r.handler;
174
165
  continue;
175
166
  }
176
- for (let i = 0;i < parts.length; i++) {
177
- const part = parts[i];
178
- if (part === "**") {
179
- if (!node.catchAllChild) {
180
- node.catchAllChild = { name: "**", node: new TrieNode };
181
- }
182
- node = node.catchAllChild.node;
183
- break;
184
- } else if (part === "*") {
185
- if (!node.wildcardChild)
186
- node.wildcardChild = new TrieNode;
187
- node = node.wildcardChild;
188
- } else if (part.startsWith(":")) {
167
+ for (const part of r.parts) {
168
+ if (part.startsWith(":")) {
189
169
  const name = part.slice(1);
190
- if (!node.paramChild)
191
- node.paramChild = { name, node: new TrieNode };
170
+ node.paramChild ??= { name, node: new TrieNode };
192
171
  node = node.paramChild.node;
193
172
  } else {
194
- if (!node.children[part])
195
- node.children[part] = new TrieNode;
173
+ node.children[part] ??= new TrieNode;
196
174
  node = node.children[part];
197
175
  }
198
176
  }
199
- if (!node.handlers)
200
- node.handlers = Object.create(null);
201
- node.handlers[route.method] = route.handler;
177
+ node.handlers ??= {};
178
+ node.handlers[r.method] = r.handler;
202
179
  }
203
180
  return root;
204
181
  }
205
- compilePipeline(handler, paramsGetter) {
206
- const mws = this.middlewares.slice();
207
- const hasMiddleware = mws.length > 0;
208
- if (!hasMiddleware) {
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);
182
+ compilePipeline(handler) {
183
+ return async (req, params, query) => {
184
+ req.params = params;
185
+ req.query = query;
186
+ let i = 0;
187
+ const next = async () => {
188
+ if (i < this.middlewares.length) {
189
+ return await this.middlewares[i++](req, next) ?? new Response("");
215
190
  }
216
- const res = await handler(princeReq);
191
+ if (["POST", "PUT", "PATCH"].includes(req.method))
192
+ req.body = await this.parseBody(req);
193
+ const res = await handler(req);
217
194
  if (res instanceof Response)
218
195
  return res;
219
196
  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 });
197
+ return new Response(res);
223
198
  return this.json(res);
224
199
  };
225
- }
226
- 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);
247
- };
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 });
200
+ return next();
259
201
  };
260
202
  }
203
+ async handleFetch(req) {
204
+ const { pathname, query } = this.parseUrl(req);
205
+ const r = req;
206
+ const segments = pathname === "/" ? [] : pathname.slice(1).split("/");
207
+ let node = this.buildRouter();
208
+ let params = {};
209
+ for (const seg of segments) {
210
+ if (node.children[seg])
211
+ node = node.children[seg];
212
+ else if (node.paramChild) {
213
+ params[node.paramChild.name] = seg;
214
+ node = node.paramChild.node;
215
+ } else
216
+ return this.json({ error: "Not Found" }, 404);
217
+ }
218
+ const handler = node.handlers?.[req.method];
219
+ if (!handler)
220
+ return this.json({ error: "Method Not Allowed" }, 405);
221
+ const pipeline = this.compilePipeline(handler);
222
+ return pipeline(r, params, query);
223
+ }
261
224
  listen(port = 3000) {
262
- const root = this.buildRouter();
263
- const handlerMap = new Map;
225
+ const self = this;
264
226
  Bun.serve({
265
227
  port,
266
- fetch: async (req) => {
267
- 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);
329
- } 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);
228
+ fetch(req, server) {
229
+ const { pathname } = new URL(req.url);
230
+ const ws = self.wsRoutes[pathname];
231
+ if (ws) {
232
+ server.upgrade(req, { data: { ws } });
233
+ return;
234
+ }
235
+ return self.handleFetch(req).catch((err) => {
236
+ if (self.errorHandler)
237
+ return self.errorHandler(err, req);
238
+ return self.json({ error: String(err) }, 500);
239
+ });
240
+ },
241
+ websocket: {
242
+ open(ws) {
243
+ ws.data.ws?.open?.(ws);
244
+ },
245
+ message(ws, msg) {
246
+ ws.data.ws?.message?.(ws, msg);
247
+ },
248
+ close(ws) {
249
+ ws.data.ws?.close?.(ws);
336
250
  }
337
251
  }
338
252
  });
339
- console.log(`\uD83D\uDE80 PrinceJS running at http://localhost:${port}`);
253
+ console.log(`\uD83D\uDE80 PrinceJS running http://localhost:${port}`);
340
254
  }
341
255
  }
342
256
  var prince = (dev = false) => new Prince(dev);
@@ -83,27 +83,76 @@ var logger = (options) => {
83
83
  };
84
84
  var rateLimit = (options) => {
85
85
  const store = new Map;
86
+ setInterval(() => {
87
+ const now = Date.now();
88
+ for (const [key, record] of store.entries()) {
89
+ if (now > record.resetAt) {
90
+ store.delete(key);
91
+ }
92
+ }
93
+ }, options.window * 1000);
86
94
  return async (req, next) => {
87
- const ip = req.headers.get("x-forwarded-for") || "unknown";
95
+ if (req[MIDDLEWARE_EXECUTED]?.rateLimit) {
96
+ return await next();
97
+ }
98
+ if (!req[MIDDLEWARE_EXECUTED]) {
99
+ req[MIDDLEWARE_EXECUTED] = {};
100
+ }
101
+ req[MIDDLEWARE_EXECUTED].rateLimit = true;
102
+ const key = options.keyGenerator ? options.keyGenerator(req) : req.headers.get("x-forwarded-for") || req.headers.get("x-real-ip") || "unknown";
88
103
  const now = Date.now();
89
104
  const windowMs = options.window * 1000;
90
- let record = store.get(ip);
105
+ let record = store.get(key);
91
106
  if (!record || now > record.resetAt) {
92
107
  record = { count: 1, resetAt: now + windowMs };
93
- store.set(ip, record);
108
+ store.set(key, record);
94
109
  return await next();
95
110
  }
96
111
  if (record.count >= options.max) {
97
- return new Response(JSON.stringify({ error: options.message || "Too many requests" }), {
112
+ const retryAfter = Math.ceil((record.resetAt - now) / 1000);
113
+ return new Response(JSON.stringify({
114
+ error: options.message || "Too many requests",
115
+ retryAfter
116
+ }), {
98
117
  status: 429,
99
- headers: { "Content-Type": "application/json" }
118
+ headers: {
119
+ "Content-Type": "application/json",
120
+ "Retry-After": String(retryAfter)
121
+ }
100
122
  });
101
123
  }
102
124
  record.count++;
103
125
  return await next();
104
126
  };
105
127
  };
128
+ var serve = (options) => {
129
+ const root = options.root || "./public";
130
+ const index = options.index || "index.html";
131
+ const dotfiles = options.dotfiles || "deny";
132
+ return async (req, next) => {
133
+ const url = new URL(req.url);
134
+ let filepath = url.pathname;
135
+ if (filepath.includes("..")) {
136
+ return new Response("Forbidden", { status: 403 });
137
+ }
138
+ if (dotfiles === "deny" && filepath.split("/").some((part) => part.startsWith("."))) {
139
+ return new Response("Forbidden", { status: 403 });
140
+ }
141
+ try {
142
+ const file = Bun.file(`${root}${filepath}`);
143
+ if (await file.exists()) {
144
+ return new Response(file);
145
+ }
146
+ const indexFile = Bun.file(`${root}${filepath}/${index}`);
147
+ if (await indexFile.exists()) {
148
+ return new Response(indexFile);
149
+ }
150
+ } catch (err) {}
151
+ return await next();
152
+ };
153
+ };
106
154
  export {
155
+ serve,
107
156
  rateLimit,
108
157
  logger,
109
158
  cors
package/dist/prince.js CHANGED
@@ -1,129 +1,4 @@
1
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
-
107
- // src/validation.ts
108
- var validate = (schema, source = "body") => {
109
- return async (req, next) => {
110
- try {
111
- const data = source === "body" ? req.body : source === "query" ? req.query : req.params;
112
- const validated = schema.parse(data);
113
- req[`validated${source.charAt(0).toUpperCase() + source.slice(1)}`] = validated;
114
- return await next();
115
- } catch (err) {
116
- return new Response(JSON.stringify({
117
- error: "Validation failed",
118
- details: err.errors || err.message
119
- }), {
120
- status: 400,
121
- headers: { "Content-Type": "application/json" }
122
- });
123
- }
124
- };
125
- };
126
-
127
2
  // src/prince.ts
128
3
  class TrieNode {
129
4
  children = Object.create(null);
@@ -165,11 +40,17 @@ class ResponseBuilder {
165
40
  this._headers["Location"] = url;
166
41
  return this.build();
167
42
  }
168
- build() {
169
- return new Response(this._body, {
170
- status: this._status,
171
- 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
+ }
172
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 });
173
54
  }
174
55
  }
175
56
 
@@ -178,25 +59,12 @@ class Prince {
178
59
  rawRoutes = [];
179
60
  middlewares = [];
180
61
  errorHandler;
181
- prefix = "";
62
+ wsRoutes = {};
63
+ openapiData = null;
64
+ router = null;
182
65
  constructor(devMode = false) {
183
66
  this.devMode = devMode;
184
67
  }
185
- useCors(options) {
186
- this.use(cors(options));
187
- return this;
188
- }
189
- useLogger(options) {
190
- this.use(logger(options));
191
- return this;
192
- }
193
- useRateLimit(options) {
194
- this.use(rateLimit(options));
195
- return this;
196
- }
197
- validate(schema, source = "body") {
198
- return validate(schema, source);
199
- }
200
68
  use(mw) {
201
69
  this.middlewares.push(mw);
202
70
  return this;
@@ -214,32 +82,26 @@ class Prince {
214
82
  response() {
215
83
  return new ResponseBuilder;
216
84
  }
217
- route(path) {
218
- const group = new Prince(this.devMode);
219
- group.prefix = path;
220
- group.middlewares = [...this.middlewares];
221
- return {
222
- get: (subpath, handler) => {
223
- this.get(path + subpath, handler);
224
- return group;
225
- },
226
- post: (subpath, handler) => {
227
- this.post(path + subpath, handler);
228
- return group;
229
- },
230
- put: (subpath, handler) => {
231
- this.put(path + subpath, handler);
232
- return group;
233
- },
234
- delete: (subpath, handler) => {
235
- this.delete(path + subpath, handler);
236
- return group;
237
- },
238
- patch: (subpath, handler) => {
239
- this.patch(path + subpath, handler);
240
- return group;
241
- }
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
242
102
  };
103
+ this.get(path, () => this.openapiData);
104
+ return this;
243
105
  }
244
106
  get(path, handler) {
245
107
  return this.add("GET", path, handler);
@@ -256,245 +118,161 @@ class Prince {
256
118
  patch(path, handler) {
257
119
  return this.add("PATCH", path, handler);
258
120
  }
259
- options(path, handler) {
260
- return this.add("OPTIONS", path, handler);
261
- }
262
- head(path, handler) {
263
- return this.add("HEAD", path, handler);
264
- }
265
121
  add(method, path, handler) {
266
122
  if (!path.startsWith("/"))
267
123
  path = "/" + path;
268
- if (path.length > 1 && path.endsWith("/"))
124
+ if (path !== "/" && path.endsWith("/"))
269
125
  path = path.slice(0, -1);
270
126
  const parts = path === "/" ? [""] : path.split("/").slice(1);
271
- this.rawRoutes.push({ method: method.toUpperCase(), path, parts, handler });
127
+ this.rawRoutes.push({ method, path, parts, handler });
128
+ this.router = null;
272
129
  return this;
273
130
  }
274
- fastPathname(req) {
275
- const u = req.url;
276
- const protoSep = u.indexOf("://");
277
- const start = protoSep !== -1 ? u.indexOf("/", protoSep + 3) : u.indexOf("/");
278
- if (start === -1)
279
- return "/";
280
- const q = u.indexOf("?", start);
281
- const h = u.indexOf("#", start);
282
- const end = q !== -1 ? q : h !== -1 ? h : u.length;
283
- return u.slice(start, end);
284
- }
285
- parseQuery(url) {
286
- const q = url.indexOf("?");
287
- if (q === -1)
288
- return {};
131
+ parseUrl(req) {
132
+ const url = new URL(req.url);
289
133
  const query = {};
290
- const search = url.slice(q + 1);
291
- const pairs = search.split("&");
292
- for (let i = 0;i < pairs.length; i++) {
293
- const pair = pairs[i];
294
- const eq = pair.indexOf("=");
295
- if (eq === -1) {
296
- query[decodeURIComponent(pair)] = "";
297
- } else {
298
- query[decodeURIComponent(pair.slice(0, eq))] = decodeURIComponent(pair.slice(eq + 1));
299
- }
300
- }
301
- return query;
134
+ for (const [k, v] of url.searchParams.entries())
135
+ query[k] = v;
136
+ return { pathname: url.pathname, query };
302
137
  }
303
138
  async parseBody(req) {
304
139
  const ct = req.headers.get("content-type") || "";
305
- if (ct.includes("application/json")) {
140
+ if (ct.includes("application/json"))
306
141
  return await req.json();
307
- }
308
- if (ct.includes("application/x-www-form-urlencoded")) {
309
- const text = await req.text();
310
- const params = {};
311
- const pairs = text.split("&");
312
- for (const pair of pairs) {
313
- const [key, val] = pair.split("=");
314
- params[decodeURIComponent(key)] = decodeURIComponent(val || "");
142
+ if (ct.includes("application/x-www-form-urlencoded"))
143
+ return Object.fromEntries(new URLSearchParams(await req.text()).entries());
144
+ if (ct.startsWith("multipart/form-data")) {
145
+ const fd = await req.formData();
146
+ const files = {};
147
+ const fields = {};
148
+ for (const [k, v] of fd.entries()) {
149
+ if (v instanceof File)
150
+ files[k] = v;
151
+ else
152
+ fields[k] = v;
315
153
  }
316
- return params;
154
+ return { files, fields };
317
155
  }
318
- if (ct.includes("text/")) {
156
+ if (ct.startsWith("text/"))
319
157
  return await req.text();
320
- }
321
158
  return null;
322
159
  }
323
160
  buildRouter() {
161
+ if (this.router)
162
+ return this.router;
324
163
  const root = new TrieNode;
325
- for (const route of this.rawRoutes) {
164
+ for (const r of this.rawRoutes) {
326
165
  let node = root;
327
- const parts = route.parts;
328
- if (parts.length === 1 && parts[0] === "") {
329
- if (!node.handlers)
330
- node.handlers = Object.create(null);
331
- node.handlers[route.method] = route.handler;
166
+ if (r.parts.length === 1 && r.parts[0] === "") {
167
+ node.handlers ??= {};
168
+ node.handlers[r.method] = r.handler;
332
169
  continue;
333
170
  }
334
- for (let i = 0;i < parts.length; i++) {
335
- const part = parts[i];
336
- if (part === "**") {
337
- if (!node.catchAllChild) {
338
- node.catchAllChild = { name: "**", node: new TrieNode };
339
- }
340
- node = node.catchAllChild.node;
341
- break;
342
- } else if (part === "*") {
343
- if (!node.wildcardChild)
344
- node.wildcardChild = new TrieNode;
345
- node = node.wildcardChild;
346
- } else if (part.startsWith(":")) {
171
+ for (const part of r.parts) {
172
+ if (part.startsWith(":")) {
347
173
  const name = part.slice(1);
348
- if (!node.paramChild)
349
- node.paramChild = { name, node: new TrieNode };
174
+ node.paramChild ??= { name, node: new TrieNode };
350
175
  node = node.paramChild.node;
351
176
  } else {
352
- if (!node.children[part])
353
- node.children[part] = new TrieNode;
177
+ node.children[part] ??= new TrieNode;
354
178
  node = node.children[part];
355
179
  }
356
180
  }
357
- if (!node.handlers)
358
- node.handlers = Object.create(null);
359
- node.handlers[route.method] = route.handler;
181
+ node.handlers ??= {};
182
+ node.handlers[r.method] = r.handler;
360
183
  }
184
+ this.router = root;
361
185
  return root;
362
186
  }
363
- compilePipeline(handler, paramsGetter) {
364
- const mws = this.middlewares.slice();
365
- const hasMiddleware = mws.length > 0;
366
- if (!hasMiddleware) {
367
- return async (req, params, query) => {
368
- const princeReq = req;
369
- princeReq.params = params;
370
- princeReq.query = query;
371
- if (req.method === "POST" || req.method === "PUT" || req.method === "PATCH") {
372
- princeReq.body = await this.parseBody(req);
187
+ compilePipeline(handler) {
188
+ return async (req, params, query) => {
189
+ Object.defineProperty(req, "params", { value: params, writable: true, configurable: true });
190
+ Object.defineProperty(req, "query", { value: query, writable: true, configurable: true });
191
+ let i = 0;
192
+ const next = async () => {
193
+ if (i < this.middlewares.length) {
194
+ return await this.middlewares[i++](req, next) ?? new Response("");
195
+ }
196
+ if (["POST", "PUT", "PATCH"].includes(req.method)) {
197
+ const parsed = await this.parseBody(req);
198
+ if (parsed && typeof parsed === "object" && "files" in parsed && "fields" in parsed) {
199
+ Object.defineProperty(req, "body", { value: parsed.fields, writable: true, configurable: true });
200
+ Object.defineProperty(req, "files", { value: parsed.files, writable: true, configurable: true });
201
+ } else {
202
+ Object.defineProperty(req, "body", { value: parsed, writable: true, configurable: true });
203
+ }
373
204
  }
374
- const res = await handler(princeReq);
205
+ const res = await handler(req);
375
206
  if (res instanceof Response)
376
207
  return res;
377
208
  if (typeof res === "string")
378
- return new Response(res, { status: 200 });
379
- if (res instanceof Uint8Array || res instanceof ArrayBuffer)
380
- return new Response(res, { status: 200 });
209
+ return new Response(res);
210
+ if (res instanceof Uint8Array)
211
+ return new Response(res);
381
212
  return this.json(res);
382
213
  };
383
- }
384
- return async (req, params, query) => {
385
- const princeReq = req;
386
- princeReq.params = params;
387
- princeReq.query = query;
388
- let idx = 0;
389
- const runNext = async () => {
390
- if (idx >= mws.length) {
391
- if (req.method === "POST" || req.method === "PUT" || req.method === "PATCH") {
392
- princeReq.body = await this.parseBody(req);
393
- }
394
- const res = await handler(princeReq);
395
- if (res instanceof Response)
396
- return res;
397
- if (typeof res === "string")
398
- return new Response(res, { status: 200 });
399
- if (res instanceof Uint8Array || res instanceof ArrayBuffer)
400
- return new Response(res, { status: 200 });
401
- return this.json(res);
402
- }
403
- const mw = mws[idx++];
404
- return await mw(req, runNext);
405
- };
406
- const out = await runNext();
407
- if (out instanceof Response)
408
- return out;
409
- if (out !== undefined) {
410
- if (typeof out === "string")
411
- return new Response(out, { status: 200 });
412
- if (out instanceof Uint8Array || out instanceof ArrayBuffer)
413
- return new Response(out, { status: 200 });
414
- return this.json(out);
415
- }
416
- return new Response(null, { status: 204 });
214
+ return next();
417
215
  };
418
216
  }
217
+ async handleFetch(req) {
218
+ const { pathname, query } = this.parseUrl(req);
219
+ const r = req;
220
+ const segments = pathname === "/" ? [] : pathname.slice(1).split("/");
221
+ const router = this.buildRouter();
222
+ let node = router;
223
+ let params = {};
224
+ for (const seg of segments) {
225
+ if (node.children[seg]) {
226
+ node = node.children[seg];
227
+ } else if (node.paramChild) {
228
+ params[node.paramChild.name] = seg;
229
+ node = node.paramChild.node;
230
+ } else {
231
+ return this.json({ error: "Not Found" }, 404);
232
+ }
233
+ }
234
+ const handler = node.handlers?.[req.method];
235
+ if (!handler)
236
+ return this.json({ error: "Method Not Allowed" }, 405);
237
+ const pipeline = this.compilePipeline(handler);
238
+ return pipeline(r, params, query);
239
+ }
419
240
  listen(port = 3000) {
420
- const root = this.buildRouter();
421
- const handlerMap = new Map;
241
+ const self = this;
422
242
  Bun.serve({
423
243
  port,
424
- fetch: async (req) => {
425
- try {
426
- const pathname = this.fastPathname(req);
427
- const query = this.parseQuery(req.url);
428
- const segments = pathname === "/" ? [] : pathname.slice(1).split("/");
429
- let node = root;
430
- const params = {};
431
- let matched = true;
432
- if (segments.length === 0) {
433
- if (!node.handlers)
434
- return this.json({ error: "Route not found" }, 404);
435
- const handler2 = node.handlers[req.method];
436
- if (!handler2)
437
- return this.json({ error: "Method not allowed" }, 405);
438
- let methodMap2 = handlerMap.get(node);
439
- if (!methodMap2) {
440
- methodMap2 = {};
441
- handlerMap.set(node, methodMap2);
442
- }
443
- if (!methodMap2[req.method]) {
444
- methodMap2[req.method] = this.compilePipeline(handler2, (_) => params);
445
- }
446
- return await methodMap2[req.method](req, params, query);
447
- }
448
- for (let i = 0;i < segments.length; i++) {
449
- const seg = segments[i];
450
- if (node.children[seg]) {
451
- node = node.children[seg];
452
- continue;
453
- }
454
- if (node.paramChild) {
455
- params[node.paramChild.name] = seg;
456
- node = node.paramChild.node;
457
- continue;
458
- }
459
- if (node.wildcardChild) {
460
- node = node.wildcardChild;
461
- continue;
462
- }
463
- if (node.catchAllChild) {
464
- const remaining = segments.slice(i).join("/");
465
- if (node.catchAllChild.name)
466
- params[node.catchAllChild.name] = remaining;
467
- node = node.catchAllChild.node;
468
- break;
469
- }
470
- matched = false;
471
- break;
472
- }
473
- if (!matched || !node || !node.handlers)
474
- return this.json({ error: "Route not found" }, 404);
475
- const handler = node.handlers[req.method];
476
- if (!handler)
477
- return this.json({ error: "Method not allowed" }, 405);
478
- let methodMap = handlerMap.get(node);
479
- if (!methodMap) {
480
- methodMap = {};
481
- handlerMap.set(node, methodMap);
482
- }
483
- if (!methodMap[req.method]) {
484
- methodMap[req.method] = this.compilePipeline(handler, (_) => params);
485
- }
486
- return await methodMap[req.method](req, params, query);
487
- } catch (err) {
488
- if (this.errorHandler) {
489
- try {
490
- return this.errorHandler(err, req);
491
- } catch {}
244
+ fetch(req, server) {
245
+ const { pathname } = new URL(req.url);
246
+ const ws = self.wsRoutes[pathname];
247
+ if (ws && server.upgrade(req, { data: { ws } })) {
248
+ return;
249
+ }
250
+ return self.handleFetch(req).catch((err) => {
251
+ if (self.errorHandler)
252
+ return self.errorHandler(err, req);
253
+ if (self.devMode) {
254
+ console.error("Error:", err);
255
+ return self.json({ error: String(err), stack: err.stack }, 500);
492
256
  }
493
- return this.json({ error: String(err) }, 500);
257
+ return self.json({ error: "Internal Server Error" }, 500);
258
+ });
259
+ },
260
+ websocket: {
261
+ open(ws) {
262
+ ws.data.ws?.open?.(ws);
263
+ },
264
+ message(ws, msg) {
265
+ ws.data.ws?.message?.(ws, msg);
266
+ },
267
+ close(ws, code, reason) {
268
+ ws.data.ws?.close?.(ws, code, reason);
269
+ },
270
+ drain(ws) {
271
+ ws.data.ws?.drain?.(ws);
494
272
  }
495
273
  }
496
274
  });
497
- console.log(`\uD83D\uDE80 PrinceJS running at http://localhost:${port}`);
275
+ console.log(`\uD83D\uDE80 PrinceJS running on http://localhost:${port}`);
498
276
  }
499
277
  }
500
278
  var prince = (dev = false) => new Prince(dev);
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "princejs",
3
- "version": "1.4.2",
3
+ "version": "1.5.2",
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
+ "type": "module",
8
+ "bin": {
9
+ "create-princejs": "./dist/create.js"
10
+ },
7
11
  "exports": {
8
12
  ".": {
9
13
  "import": "./dist/prince.js",
@@ -19,7 +23,9 @@
19
23
  }
20
24
  },
21
25
  "files": [
22
- "dist"
26
+ "dist",
27
+ "README.md",
28
+ "LICENSE"
23
29
  ],
24
30
  "keywords": [
25
31
  "backend",
@@ -30,21 +36,37 @@
30
36
  "princejs",
31
37
  "rest",
32
38
  "server",
33
- "typescript"
39
+ "typescript",
40
+ "websocket",
41
+ "middleware"
34
42
  ],
35
43
  "author": "Matthew Michael (MatthewTheCoder1218)",
36
44
  "license": "MIT",
37
- "repository": "https://github.com/MatthewTheCoder1218/princejs",
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "https://github.com/MatthewTheCoder1218/princejs"
48
+ },
49
+ "bugs": {
50
+ "url": "https://github.com/MatthewTheCoder1218/princejs/issues"
51
+ },
52
+ "homepage": "https://princejs.vercel.app",
38
53
  "publishConfig": {
39
54
  "access": "public"
40
55
  },
41
56
  "devDependencies": {
42
57
  "@types/bun": "^1.3.2",
43
58
  "bun-types": "latest",
44
- "typescript": "^5.9.3",
45
- "zod": "^4.1.12"
59
+ "typescript": "^5.9.3"
60
+ },
61
+ "peerDependencies": {
62
+ "zod": "^3.0.0"
63
+ },
64
+ "peerDependenciesMeta": {
65
+ "zod": {
66
+ "optional": true
67
+ }
46
68
  },
47
69
  "scripts": {
48
- "build": "bun build src/index.ts --outdir dist --target bun && bun build src/middleware.ts --outdir dist --target bun && bun build src/validation.ts --outdir dist --target bun"
70
+ "build": "bun build src/prince.ts --outdir dist --target bun && bun build src/middleware.ts --outdir dist --target bun && bun build src/validation.ts --outdir dist --target bun && bun build bin/create.ts --outdir dist --target bun --format esm"
49
71
  }
50
72
  }