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 +5 -0
- package/dist/create.js +153 -0
- package/dist/index.js +118 -204
- package/dist/middleware.js +54 -5
- package/dist/prince.js +141 -363
- package/package.json +29 -7
package/Readme.md
CHANGED
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
|
-
|
|
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
|
|
|
@@ -53,7 +59,8 @@ class Prince {
|
|
|
53
59
|
rawRoutes = [];
|
|
54
60
|
middlewares = [];
|
|
55
61
|
errorHandler;
|
|
56
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
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
|
|
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 [
|
|
138
|
-
query[
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
|
152
|
+
return { files, fields };
|
|
159
153
|
}
|
|
160
|
-
if (ct.
|
|
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
|
|
160
|
+
for (const r of this.rawRoutes) {
|
|
168
161
|
let node = root;
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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 (
|
|
177
|
-
|
|
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
|
-
|
|
191
|
-
node.paramChild = { name, node: new TrieNode };
|
|
170
|
+
node.paramChild ??= { name, node: new TrieNode };
|
|
192
171
|
node = node.paramChild.node;
|
|
193
172
|
} else {
|
|
194
|
-
|
|
195
|
-
node.children[part] = new TrieNode;
|
|
173
|
+
node.children[part] ??= new TrieNode;
|
|
196
174
|
node = node.children[part];
|
|
197
175
|
}
|
|
198
176
|
}
|
|
199
|
-
|
|
200
|
-
|
|
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
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
263
|
-
const handlerMap = new Map;
|
|
225
|
+
const self = this;
|
|
264
226
|
Bun.serve({
|
|
265
227
|
port,
|
|
266
|
-
fetch
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
|
253
|
+
console.log(`\uD83D\uDE80 PrinceJS running http://localhost:${port}`);
|
|
340
254
|
}
|
|
341
255
|
}
|
|
342
256
|
var prince = (dev = false) => new Prince(dev);
|
package/dist/middleware.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
105
|
+
let record = store.get(key);
|
|
91
106
|
if (!record || now > record.resetAt) {
|
|
92
107
|
record = { count: 1, resetAt: now + windowMs };
|
|
93
|
-
store.set(
|
|
108
|
+
store.set(key, record);
|
|
94
109
|
return await next();
|
|
95
110
|
}
|
|
96
111
|
if (record.count >= options.max) {
|
|
97
|
-
|
|
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: {
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
|
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
|
|
127
|
+
this.rawRoutes.push({ method, path, parts, handler });
|
|
128
|
+
this.router = null;
|
|
272
129
|
return this;
|
|
273
130
|
}
|
|
274
|
-
|
|
275
|
-
const
|
|
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
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
|
154
|
+
return { files, fields };
|
|
317
155
|
}
|
|
318
|
-
if (ct.
|
|
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
|
|
164
|
+
for (const r of this.rawRoutes) {
|
|
326
165
|
let node = root;
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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 (
|
|
335
|
-
|
|
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
|
-
|
|
349
|
-
node.paramChild = { name, node: new TrieNode };
|
|
174
|
+
node.paramChild ??= { name, node: new TrieNode };
|
|
350
175
|
node = node.paramChild.node;
|
|
351
176
|
} else {
|
|
352
|
-
|
|
353
|
-
node.children[part] = new TrieNode;
|
|
177
|
+
node.children[part] ??= new TrieNode;
|
|
354
178
|
node = node.children[part];
|
|
355
179
|
}
|
|
356
180
|
}
|
|
357
|
-
|
|
358
|
-
|
|
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
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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(
|
|
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
|
|
379
|
-
if (res instanceof Uint8Array
|
|
380
|
-
return new Response(res
|
|
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
|
|
421
|
-
const handlerMap = new Map;
|
|
241
|
+
const self = this;
|
|
422
242
|
Bun.serve({
|
|
423
243
|
port,
|
|
424
|
-
fetch
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
|
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
|
|
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.
|
|
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":
|
|
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
|
-
|
|
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/
|
|
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
|
}
|