princejs 2.1.4 → 2.1.6
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 +135 -154
- package/dist/prince.d.ts +4 -1
- package/dist/prince.d.ts.map +1 -1
- package/dist/prince.js +144 -95
- package/package.json +1 -1
package/Readme.md
CHANGED
|
@@ -20,23 +20,24 @@ Built by a 13-year-old Nigerian developer. Among the top three in performance.
|
|
|
20
20
|
|
|
21
21
|
Benchmarked with `oha -c 100 -z 30s` on Windows 10:
|
|
22
22
|
|
|
23
|
-
| Framework | Req/s |
|
|
24
|
-
|
|
25
|
-
| Elysia |
|
|
26
|
-
|
|
|
27
|
-
|
|
|
28
|
-
|
|
|
23
|
+
| Framework | Avg Req/s | Peak Req/s |
|
|
24
|
+
|-----------|----------:|-----------:|
|
|
25
|
+
| Elysia | 27,606 | 27,834 |
|
|
26
|
+
| **PrinceJS** | **17,985** | **18,507** |
|
|
27
|
+
| Hono | 17,914 | 18,826 |
|
|
28
|
+
| Fastify | 15,519 | 16,434 |
|
|
29
|
+
| Express | 13,138 | 13,458 |
|
|
29
30
|
|
|
30
|
-
> PrinceJS is **2.3× faster than Express
|
|
31
|
+
> PrinceJS is **2.3× faster than Express**, matches Hono head-to-head, and sits at just **5.1kB gzipped** — loads in ~101ms on a slow 3G connection.
|
|
31
32
|
|
|
32
33
|
---
|
|
33
34
|
|
|
34
35
|
## 🚀 Quick Start
|
|
35
36
|
|
|
36
37
|
```bash
|
|
37
|
-
npm install princejs
|
|
38
38
|
bun add princejs
|
|
39
|
-
|
|
39
|
+
# or
|
|
40
|
+
npm install princejs
|
|
40
41
|
```
|
|
41
42
|
|
|
42
43
|
```ts
|
|
@@ -49,7 +50,7 @@ app.use(cors());
|
|
|
49
50
|
app.use(logger());
|
|
50
51
|
|
|
51
52
|
app.get("/", () => ({ message: "Hello PrinceJS!" }));
|
|
52
|
-
app.get("/users/:id", (req) => ({ id: req.params
|
|
53
|
+
app.get("/users/:id", (req) => ({ id: req.params?.id }));
|
|
53
54
|
|
|
54
55
|
app.listen(3000);
|
|
55
56
|
```
|
|
@@ -60,24 +61,17 @@ app.listen(3000);
|
|
|
60
61
|
|
|
61
62
|
| Feature | Import |
|
|
62
63
|
|---------|--------|
|
|
63
|
-
| Routing | `princejs` |
|
|
64
|
-
|
|
|
65
|
-
|
|
|
66
|
-
| **Cookies & IP Detection** | `princejs` |
|
|
67
|
-
| File Uploads | `princejs/helpers` |
|
|
68
|
-
| WebSockets | `princejs` |
|
|
69
|
-
| Server-Sent Events | `princejs/helpers` |
|
|
70
|
-
| Sessions | `princejs/middleware` |
|
|
71
|
-
| Response Compression | `princejs/middleware` |
|
|
72
|
-
| In-memory Cache | `princejs/helpers` |
|
|
64
|
+
| Routing, WebSockets, OpenAPI, Plugins, Lifecycle Hooks, Cookies, IP | `princejs` |
|
|
65
|
+
| CORS, Logger, JWT, Auth, Rate Limit, Validate, Compress, Session, API Key | `princejs/middleware` |
|
|
66
|
+
| File Uploads, SSE, In-memory Cache | `princejs/helpers` |
|
|
73
67
|
| Cron Scheduler | `princejs/scheduler` |
|
|
74
|
-
| **OpenAPI + Scalar Docs** | `princejs` |
|
|
75
68
|
| JSX / SSR | `princejs/jsx` |
|
|
76
69
|
| SQLite Database | `princejs/db` |
|
|
77
|
-
| Plugin System | `princejs` |
|
|
78
70
|
| End-to-End Type Safety | `princejs/client` |
|
|
79
|
-
|
|
|
80
|
-
|
|
|
71
|
+
| Vercel Edge adapter | `princejs/vercel` |
|
|
72
|
+
| Cloudflare Workers adapter | `princejs/cloudflare` |
|
|
73
|
+
| Deno Deploy adapter | `princejs/deno` |
|
|
74
|
+
| Node.js / Express adapter | `princejs/node` |
|
|
81
75
|
|
|
82
76
|
---
|
|
83
77
|
|
|
@@ -85,31 +79,35 @@ app.listen(3000);
|
|
|
85
79
|
|
|
86
80
|
### Reading Cookies
|
|
87
81
|
|
|
88
|
-
Cookies are automatically parsed
|
|
82
|
+
Cookies are automatically parsed and available on every request:
|
|
89
83
|
|
|
90
84
|
```ts
|
|
85
|
+
import { prince } from "princejs";
|
|
86
|
+
|
|
87
|
+
const app = prince();
|
|
88
|
+
|
|
91
89
|
app.get("/profile", (req) => ({
|
|
92
90
|
sessionId: req.cookies?.sessionId,
|
|
93
91
|
theme: req.cookies?.theme,
|
|
94
|
-
allCookies: req.cookies // Record<string, string>
|
|
92
|
+
allCookies: req.cookies, // Record<string, string>
|
|
95
93
|
}));
|
|
96
94
|
```
|
|
97
95
|
|
|
98
96
|
### Setting Cookies
|
|
99
97
|
|
|
100
|
-
Use the response builder
|
|
98
|
+
Use the response builder for full cookie control:
|
|
101
99
|
|
|
102
100
|
```ts
|
|
103
|
-
app.get("/login", (req) =>
|
|
101
|
+
app.get("/login", (req) =>
|
|
104
102
|
app.response()
|
|
105
103
|
.status(200)
|
|
106
104
|
.json({ ok: true })
|
|
107
105
|
.cookie("sessionId", "abc123", {
|
|
108
|
-
maxAge: 3600,
|
|
106
|
+
maxAge: 3600, // 1 hour
|
|
109
107
|
path: "/",
|
|
110
|
-
httpOnly: true,
|
|
111
|
-
secure: true,
|
|
112
|
-
sameSite: "Strict"
|
|
108
|
+
httpOnly: true, // not accessible from JS
|
|
109
|
+
secure: true, // HTTPS only
|
|
110
|
+
sameSite: "Strict", // CSRF protection
|
|
113
111
|
})
|
|
114
112
|
);
|
|
115
113
|
|
|
@@ -123,39 +121,30 @@ app.response()
|
|
|
123
121
|
|
|
124
122
|
### Client IP Detection
|
|
125
123
|
|
|
126
|
-
Automatically detect client IP from request headers:
|
|
127
|
-
|
|
128
124
|
```ts
|
|
129
125
|
app.get("/api/data", (req) => ({
|
|
130
126
|
clientIp: req.ip,
|
|
131
|
-
data: [
|
|
127
|
+
data: [],
|
|
132
128
|
}));
|
|
133
129
|
```
|
|
134
130
|
|
|
135
131
|
**Supported headers** (in priority order):
|
|
136
|
-
- `X-Forwarded-For` —
|
|
132
|
+
- `X-Forwarded-For` — load balancers, proxies (first IP in list)
|
|
137
133
|
- `X-Real-IP` — Nginx, Apache reverse proxy
|
|
138
134
|
- `CF-Connecting-IP` — Cloudflare
|
|
139
|
-
- `X-Client-IP` —
|
|
140
|
-
- Fallback — `127.0.0.1`
|
|
141
|
-
|
|
142
|
-
**Use cases:**
|
|
143
|
-
- 🔒 Rate limiting per IP
|
|
144
|
-
- 📊 Geolocation analytics
|
|
145
|
-
- 🚨 IP-based access control
|
|
146
|
-
- 👥 User tracking & fraud detection
|
|
135
|
+
- `X-Client-IP` — other proxy services
|
|
136
|
+
- Fallback — `127.0.0.1`
|
|
147
137
|
|
|
148
138
|
```ts
|
|
149
|
-
//
|
|
139
|
+
// IP-based rate limiting
|
|
150
140
|
app.use((req, next) => {
|
|
151
|
-
const
|
|
152
|
-
const count = ipTracker.getCount(ip) || 0;
|
|
141
|
+
const count = ipTracker.getCount(req.ip) || 0;
|
|
153
142
|
if (count > 100) return new Response("Too many requests", { status: 429 });
|
|
154
|
-
ipTracker.increment(ip);
|
|
143
|
+
ipTracker.increment(req.ip);
|
|
155
144
|
return next();
|
|
156
145
|
});
|
|
157
146
|
|
|
158
|
-
// IP
|
|
147
|
+
// IP allowlist
|
|
159
148
|
app.post("/admin", (req) => {
|
|
160
149
|
if (!ALLOWED_IPS.includes(req.ip!)) {
|
|
161
150
|
return new Response("Forbidden", { status: 403 });
|
|
@@ -168,7 +157,7 @@ app.post("/admin", (req) => {
|
|
|
168
157
|
|
|
169
158
|
## 📖 OpenAPI + Scalar Docs ✨
|
|
170
159
|
|
|
171
|
-
Auto-generate an OpenAPI 3.0 spec and serve a beautiful [Scalar](https://scalar.com) UI — all from a single `app.openapi()` call.
|
|
160
|
+
Auto-generate an OpenAPI 3.0 spec and serve a beautiful [Scalar](https://scalar.com) UI — all from a single `app.openapi()` call.
|
|
172
161
|
|
|
173
162
|
```ts
|
|
174
163
|
import { prince } from "princejs";
|
|
@@ -203,16 +192,16 @@ app.listen(3000);
|
|
|
203
192
|
`api.route()` does three things at once:
|
|
204
193
|
|
|
205
194
|
- ✅ Registers the route on PrinceJS
|
|
206
|
-
- ✅ Auto-wires
|
|
195
|
+
- ✅ Auto-wires body validation — no separate middleware needed
|
|
207
196
|
- ✅ Writes the full OpenAPI spec entry
|
|
208
197
|
|
|
209
|
-
| `schema` key | Runtime | Scalar
|
|
198
|
+
| `schema` key | Runtime effect | Scalar docs |
|
|
210
199
|
|---|---|---|
|
|
211
|
-
| `body` | ✅ Validates
|
|
200
|
+
| `body` | ✅ Validates & rejects bad requests | ✅ requestBody model |
|
|
212
201
|
| `query` | — | ✅ Typed query params |
|
|
213
202
|
| `response` | — | ✅ 200 response model |
|
|
214
203
|
|
|
215
|
-
> Routes on `app.get()` / `app.post()` stay private — never appear in docs.
|
|
204
|
+
> Routes on `app.get()` / `app.post()` stay private — they never appear in the docs.
|
|
216
205
|
|
|
217
206
|
**Themes:** `default` · `moon` · `purple` · `solarized` · `bluePlanet` · `deepSpace` · `saturn` · `kepler` · `mars`
|
|
218
207
|
|
|
@@ -220,8 +209,6 @@ app.listen(3000);
|
|
|
220
209
|
|
|
221
210
|
## 🔌 Plugin System
|
|
222
211
|
|
|
223
|
-
Share bundles of routes and middleware as reusable plugins:
|
|
224
|
-
|
|
225
212
|
```ts
|
|
226
213
|
import { prince, type PrincePlugin } from "princejs";
|
|
227
214
|
|
|
@@ -241,66 +228,53 @@ const usersPlugin: PrincePlugin<{ prefix?: string }> = (app, opts) => {
|
|
|
241
228
|
|
|
242
229
|
const app = prince();
|
|
243
230
|
app.plugin(usersPlugin, { prefix: "/api" });
|
|
231
|
+
app.listen(3000);
|
|
244
232
|
```
|
|
245
233
|
|
|
246
234
|
---
|
|
247
235
|
|
|
248
236
|
## 🎣 Lifecycle Hooks
|
|
249
237
|
|
|
250
|
-
React to key moments in request processing with lifecycle hooks:
|
|
251
|
-
|
|
252
238
|
```ts
|
|
253
239
|
import { prince } from "princejs";
|
|
254
240
|
|
|
255
241
|
const app = prince();
|
|
256
242
|
|
|
257
|
-
// Called for every incoming request
|
|
258
243
|
app.onRequest((req) => {
|
|
259
|
-
|
|
244
|
+
(req as any).startTime = Date.now();
|
|
260
245
|
});
|
|
261
246
|
|
|
262
|
-
// Called before handler execution
|
|
263
247
|
app.onBeforeHandle((req, path, method) => {
|
|
264
|
-
console.log(`🔍
|
|
265
|
-
(req as any).startTime = Date.now();
|
|
248
|
+
console.log(`🔍 ${method} ${path}`);
|
|
266
249
|
});
|
|
267
250
|
|
|
268
|
-
// Called after successful handler execution
|
|
269
251
|
app.onAfterHandle((req, res, path, method) => {
|
|
270
|
-
const
|
|
271
|
-
console.log(`✅
|
|
252
|
+
const ms = Date.now() - (req as any).startTime;
|
|
253
|
+
console.log(`✅ ${method} ${path} ${res.status} (${ms}ms)`);
|
|
272
254
|
});
|
|
273
255
|
|
|
274
|
-
// Called when handler throws an error
|
|
275
256
|
app.onError((err, req, path, method) => {
|
|
276
|
-
console.error(`❌
|
|
277
|
-
// Send alert, log to monitoring service, etc.
|
|
257
|
+
console.error(`❌ ${method} ${path}:`, err.message);
|
|
278
258
|
});
|
|
279
259
|
|
|
280
260
|
app.get("/users", () => ({ users: [] }));
|
|
261
|
+
app.listen(3000);
|
|
281
262
|
```
|
|
282
263
|
|
|
283
|
-
**
|
|
284
|
-
1. `onRequest` —
|
|
285
|
-
2. `onBeforeHandle` — just before
|
|
264
|
+
**Execution order:**
|
|
265
|
+
1. `onRequest` — runs before routing, good for setup
|
|
266
|
+
2. `onBeforeHandle` — just before the handler
|
|
286
267
|
3. Handler executes
|
|
287
|
-
4. `onAfterHandle` — after success (on error
|
|
288
|
-
5. `onError` — only
|
|
289
|
-
|
|
290
|
-
**Use cases:**
|
|
291
|
-
- 📊 Metrics & observability
|
|
292
|
-
- 🔍 Request inspection & debugging
|
|
293
|
-
- ⏱️ Timing & performance monitoring
|
|
294
|
-
- 🚨 Error tracking & alerting
|
|
295
|
-
- 🔐 Security audits & compliance logging
|
|
268
|
+
4. `onAfterHandle` — after success (skipped on error)
|
|
269
|
+
5. `onError` — only when handler throws
|
|
296
270
|
|
|
297
271
|
---
|
|
298
272
|
|
|
299
273
|
## 🔒 End-to-End Type Safety
|
|
300
274
|
|
|
301
|
-
Define a contract once — your client gets full TypeScript autocompletion automatically:
|
|
302
|
-
|
|
303
275
|
```ts
|
|
276
|
+
import { createClient, type PrinceApiContract } from "princejs/client";
|
|
277
|
+
|
|
304
278
|
type ApiContract = {
|
|
305
279
|
"GET /users/:id": {
|
|
306
280
|
params: { id: string };
|
|
@@ -312,12 +286,13 @@ type ApiContract = {
|
|
|
312
286
|
};
|
|
313
287
|
};
|
|
314
288
|
|
|
315
|
-
import { createClient } from "princejs/client";
|
|
316
|
-
|
|
317
289
|
const client = createClient<ApiContract>("http://localhost:3000");
|
|
318
290
|
|
|
319
291
|
const user = await client.get("/users/:id", { params: { id: "42" } });
|
|
320
292
|
console.log(user.name); // typed as string ✅
|
|
293
|
+
|
|
294
|
+
const created = await client.post("/users", { body: { name: "Alice" } });
|
|
295
|
+
console.log(created.id); // typed as string ✅
|
|
321
296
|
```
|
|
322
297
|
|
|
323
298
|
---
|
|
@@ -342,20 +317,19 @@ import { toDeno } from "princejs/deno";
|
|
|
342
317
|
Deno.serve(toDeno(app));
|
|
343
318
|
```
|
|
344
319
|
|
|
345
|
-
**Node
|
|
320
|
+
**Node.js** — `server.ts`
|
|
346
321
|
```ts
|
|
347
322
|
import { createServer } from "http";
|
|
348
323
|
import { toNode, toExpress } from "princejs/node";
|
|
324
|
+
import express from "express";
|
|
349
325
|
|
|
350
326
|
const app = prince();
|
|
351
|
-
app.get("/", () => ({ message: "Hello
|
|
327
|
+
app.get("/", () => ({ message: "Hello!" }));
|
|
352
328
|
|
|
353
|
-
// Native Node
|
|
354
|
-
|
|
355
|
-
server.listen(3000);
|
|
329
|
+
// Native Node http
|
|
330
|
+
createServer(toNode(app)).listen(3000);
|
|
356
331
|
|
|
357
|
-
// Or
|
|
358
|
-
import express from "express";
|
|
332
|
+
// Or drop into Express
|
|
359
333
|
const expressApp = express();
|
|
360
334
|
expressApp.all("*", toExpress(app));
|
|
361
335
|
expressApp.listen(3000);
|
|
@@ -367,102 +341,102 @@ expressApp.listen(3000);
|
|
|
367
341
|
|
|
368
342
|
```ts
|
|
369
343
|
import { prince } from "princejs";
|
|
370
|
-
import {
|
|
371
|
-
|
|
344
|
+
import {
|
|
345
|
+
cors,
|
|
346
|
+
logger,
|
|
347
|
+
rateLimit,
|
|
348
|
+
auth,
|
|
349
|
+
apiKey,
|
|
350
|
+
jwt,
|
|
351
|
+
signJWT,
|
|
352
|
+
session,
|
|
353
|
+
compress,
|
|
354
|
+
validate,
|
|
355
|
+
} from "princejs/middleware";
|
|
372
356
|
import { cache, upload, sse } from "princejs/helpers";
|
|
373
357
|
import { cron } from "princejs/scheduler";
|
|
374
358
|
import { Html, Head, Body, H1, P, render } from "princejs/jsx";
|
|
375
359
|
import { db } from "princejs/db";
|
|
376
360
|
import { z } from "zod";
|
|
377
361
|
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
// ==========================================
|
|
381
|
-
// LIFECYCLE HOOKS - Timing & Observability
|
|
382
|
-
// ==========================================
|
|
383
|
-
app.onRequest((req) => {
|
|
384
|
-
(req as any).startTime = Date.now();
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
app.onBeforeHandle((req, path, method) => {
|
|
388
|
-
console.log(`🔍 Handling: ${method} ${path}`);
|
|
389
|
-
});
|
|
362
|
+
const SECRET = new TextEncoder().encode("your-secret");
|
|
363
|
+
const app = prince();
|
|
390
364
|
|
|
365
|
+
// ── Lifecycle hooks ───────────────────────────────────────
|
|
366
|
+
app.onRequest((req) => { (req as any).t = Date.now(); });
|
|
391
367
|
app.onAfterHandle((req, res, path, method) => {
|
|
392
|
-
|
|
393
|
-
console.log(`✅ ${method} ${path} → ${res.status} (${duration}ms)`);
|
|
368
|
+
console.log(`✅ ${method} ${path} ${res.status} (${Date.now() - (req as any).t}ms)`);
|
|
394
369
|
});
|
|
395
|
-
|
|
396
370
|
app.onError((err, req, path, method) => {
|
|
397
|
-
console.error(`❌ ${method} ${path}
|
|
371
|
+
console.error(`❌ ${method} ${path}:`, err.message);
|
|
398
372
|
});
|
|
399
373
|
|
|
400
|
-
//
|
|
401
|
-
// GLOBAL MIDDLEWARE
|
|
402
|
-
// ==========================================
|
|
374
|
+
// ── Global middleware ─────────────────────────────────────
|
|
403
375
|
app.use(cors());
|
|
404
376
|
app.use(logger());
|
|
405
|
-
app.use(rateLimit(
|
|
406
|
-
app.use(
|
|
407
|
-
app.use(
|
|
408
|
-
app.use(session({ secret: "key" }));
|
|
377
|
+
app.use(rateLimit(100, 60));
|
|
378
|
+
app.use(jwt(SECRET));
|
|
379
|
+
app.use(session({ secret: "session-secret" }));
|
|
409
380
|
app.use(compress());
|
|
410
381
|
|
|
411
|
-
//
|
|
412
|
-
|
|
413
|
-
|
|
382
|
+
// ── JSX SSR ───────────────────────────────────────────────
|
|
383
|
+
const Page = () => Html(Head("Home"), Body(H1("Hello World"), P("Welcome!")));
|
|
384
|
+
app.get("/", () => render(Page()));
|
|
414
385
|
|
|
415
|
-
//
|
|
416
|
-
|
|
417
|
-
app.get("/jsx", () => render(Page()));
|
|
418
|
-
|
|
419
|
-
// Cookies & IP Detection
|
|
420
|
-
app.post("/login", (req) =>
|
|
386
|
+
// ── Cookies & IP ──────────────────────────────────────────
|
|
387
|
+
app.post("/login", (req) =>
|
|
421
388
|
app.response()
|
|
422
389
|
.json({ ok: true, ip: req.ip })
|
|
423
|
-
.cookie("sessionId", "user_123", {
|
|
424
|
-
httpOnly: true,
|
|
425
|
-
secure: true,
|
|
426
|
-
sameSite: "Strict",
|
|
427
|
-
maxAge: 86400 // 24 hours
|
|
390
|
+
.cookie("sessionId", "user_123", {
|
|
391
|
+
httpOnly: true, secure: true, sameSite: "Strict", maxAge: 86400,
|
|
428
392
|
})
|
|
429
393
|
);
|
|
430
|
-
|
|
431
394
|
app.get("/profile", (req) => ({
|
|
432
395
|
sessionId: req.cookies?.sessionId,
|
|
433
396
|
clientIp: req.ip,
|
|
434
397
|
}));
|
|
435
398
|
|
|
436
|
-
// Database
|
|
437
|
-
const users = db.sqlite("./
|
|
399
|
+
// ── Database ──────────────────────────────────────────────
|
|
400
|
+
const users = db.sqlite("./app.sqlite", `
|
|
401
|
+
CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, name TEXT NOT NULL)
|
|
402
|
+
`);
|
|
438
403
|
app.get("/users", () => users.query("SELECT * FROM users"));
|
|
439
404
|
|
|
440
|
-
// WebSockets
|
|
405
|
+
// ── WebSockets ────────────────────────────────────────────
|
|
441
406
|
app.ws("/chat", {
|
|
442
|
-
open:
|
|
407
|
+
open: (ws) => ws.send("Welcome!"),
|
|
443
408
|
message: (ws, msg) => ws.send(`Echo: ${msg}`),
|
|
409
|
+
close: (ws) => console.log("disconnected"),
|
|
444
410
|
});
|
|
445
411
|
|
|
446
|
-
// Auth
|
|
412
|
+
// ── Auth & API keys ───────────────────────────────────────
|
|
447
413
|
app.get("/protected", auth(), (req) => ({ user: req.user }));
|
|
448
|
-
app.get("/api", apiKey({ keys: ["key_123"] }), (
|
|
449
|
-
|
|
450
|
-
// Helpers
|
|
451
|
-
app.get("/
|
|
452
|
-
app.post("/upload", upload()
|
|
453
|
-
app.get("/events",
|
|
454
|
-
|
|
414
|
+
app.get("/api", apiKey({ keys: ["key_123"] }), () => ({ ok: true }));
|
|
415
|
+
|
|
416
|
+
// ── Helpers ───────────────────────────────────────────────
|
|
417
|
+
app.get("/cached", cache(60)(() => ({ time: Date.now() })));
|
|
418
|
+
app.post("/upload", upload());
|
|
419
|
+
app.get("/events", sse(), (req) => {
|
|
420
|
+
let i = 0;
|
|
421
|
+
const id = setInterval(() => {
|
|
422
|
+
req.sseSend({ count: i++ });
|
|
423
|
+
if (i >= 10) clearInterval(id);
|
|
424
|
+
}, 1000);
|
|
455
425
|
});
|
|
456
426
|
|
|
457
|
-
//
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
427
|
+
// ── Validation ────────────────────────────────────────────
|
|
428
|
+
app.post(
|
|
429
|
+
"/items",
|
|
430
|
+
validate(z.object({ name: z.string().min(1), price: z.number().positive() })),
|
|
431
|
+
(req) => ({ created: req.parsedBody })
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
// ── Cron ──────────────────────────────────────────────────
|
|
435
|
+
cron("* * * * *", () => console.log("💓 heartbeat"));
|
|
461
436
|
|
|
462
|
-
//
|
|
463
|
-
// OPENAPI + SCALAR DOCS
|
|
464
|
-
// ==========================================
|
|
437
|
+
// ── OpenAPI + Scalar ──────────────────────────────────────
|
|
465
438
|
const api = app.openapi({ title: "PrinceJS App", version: "1.0.0" }, "/docs");
|
|
439
|
+
|
|
466
440
|
api.route("GET", "/items", {
|
|
467
441
|
summary: "List items",
|
|
468
442
|
tags: ["items"],
|
|
@@ -472,6 +446,15 @@ api.route("GET", "/items", {
|
|
|
472
446
|
},
|
|
473
447
|
}, () => [{ id: "1", name: "Widget" }]);
|
|
474
448
|
|
|
449
|
+
api.route("POST", "/items", {
|
|
450
|
+
summary: "Create item",
|
|
451
|
+
tags: ["items"],
|
|
452
|
+
schema: {
|
|
453
|
+
body: z.object({ name: z.string().min(1), price: z.number().positive() }),
|
|
454
|
+
response: z.object({ id: z.string(), name: z.string() }),
|
|
455
|
+
},
|
|
456
|
+
}, (req) => ({ id: crypto.randomUUID(), name: req.parsedBody.name }));
|
|
457
|
+
|
|
475
458
|
app.listen(3000);
|
|
476
459
|
```
|
|
477
460
|
|
|
@@ -483,8 +466,6 @@ app.listen(3000);
|
|
|
483
466
|
bun add princejs
|
|
484
467
|
# or
|
|
485
468
|
npm install princejs
|
|
486
|
-
# or
|
|
487
|
-
yarn add princejs
|
|
488
469
|
```
|
|
489
470
|
|
|
490
471
|
---
|
|
@@ -511,7 +492,7 @@ bun test
|
|
|
511
492
|
|
|
512
493
|
<div align="center">
|
|
513
494
|
|
|
514
|
-
**PrinceJS:
|
|
495
|
+
**PrinceJS: 5.1kB. Hono-speed. Everything included. 👑**
|
|
515
496
|
|
|
516
497
|
*Built with ❤️ in Nigeria*
|
|
517
498
|
|
package/dist/prince.d.ts
CHANGED
|
@@ -75,6 +75,7 @@ export declare class Prince {
|
|
|
75
75
|
private router;
|
|
76
76
|
private staticRoutes;
|
|
77
77
|
private staticMiddlewares;
|
|
78
|
+
private staticComposed;
|
|
78
79
|
private routeCache;
|
|
79
80
|
private onRequestHooks;
|
|
80
81
|
private onBeforeHandleHooks;
|
|
@@ -111,8 +112,10 @@ export declare class Prince {
|
|
|
111
112
|
private add;
|
|
112
113
|
private buildRouter;
|
|
113
114
|
private insertRoute;
|
|
115
|
+
private composeMW;
|
|
114
116
|
private findRoute;
|
|
115
|
-
private
|
|
117
|
+
private composeRouterMiddlewares;
|
|
118
|
+
private composeStaticMiddlewares;
|
|
116
119
|
private matchRoute;
|
|
117
120
|
private parseBody;
|
|
118
121
|
private executeHandler;
|
package/dist/prince.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prince.d.ts","sourceRoot":"","sources":["../src/prince.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAe,cAAc,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE9E,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,KAAK,IAAI,GAAG,MAAM,OAAO,CAAC,QAAQ,CAAC,CAAC;AACpC,KAAK,UAAU,GAAG,CAAC,GAAG,EAAE,aAAa,EAAE,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,GAAG,QAAQ,GAAG,SAAS,CAAC;AAC3G,KAAK,aAAa,GAAG,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,GAAG,UAAU,CAAC;AAE1E,MAAM,WAAW,aAAc,SAAQ,OAAO;IAC5C,UAAU,CAAC,EAAE,GAAG,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAC7B,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB,OAAO,CAAC,EAAE,GAAG,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAGD,MAAM,MAAM,SAAS,GAAG,CAAC,GAAG,EAAE,aAAa,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AACrE,MAAM,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AACxG,MAAM,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,aAAa,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AACtH,MAAM,MAAM,OAAO,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE3G,UAAU,gBAAgB;IACxB,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,GAAG,KAAK,IAAI,CAAC;IACzB,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,KAAK,IAAI,CAAC;IAClD,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,GAAG,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1D,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,GAAG,KAAK,IAAI,CAAC;CAC3B;AAED,KAAK,YAAY,GAAG,CAAC,GAAG,EAAE,aAAa,KAAK,OAAO,CAAC,aAAa,CAAC,GAAG,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"prince.d.ts","sourceRoot":"","sources":["../src/prince.ts"],"names":[],"mappings":"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"}
|
package/dist/prince.js
CHANGED
|
@@ -177,27 +177,50 @@ function parseCookies(cookieHeader) {
|
|
|
177
177
|
const cookies = {};
|
|
178
178
|
if (!cookieHeader)
|
|
179
179
|
return cookies;
|
|
180
|
-
cookieHeader.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
180
|
+
const len = cookieHeader.length;
|
|
181
|
+
let i = 0;
|
|
182
|
+
while (i < len) {
|
|
183
|
+
while (i < len && cookieHeader.charCodeAt(i) === 32)
|
|
184
|
+
i++;
|
|
185
|
+
const eqIdx = cookieHeader.indexOf("=", i);
|
|
186
|
+
if (eqIdx === -1)
|
|
187
|
+
break;
|
|
188
|
+
const semIdx = cookieHeader.indexOf(";", eqIdx);
|
|
189
|
+
const end = semIdx === -1 ? len : semIdx;
|
|
190
|
+
const name = cookieHeader.slice(i, eqIdx).trimEnd();
|
|
191
|
+
const val = cookieHeader.slice(eqIdx + 1, end).trim();
|
|
192
|
+
if (name) {
|
|
193
|
+
try {
|
|
194
|
+
cookies[decodeURIComponent(name)] = decodeURIComponent(val);
|
|
195
|
+
} catch {
|
|
196
|
+
cookies[name] = val;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
i = end + 1;
|
|
200
|
+
}
|
|
185
201
|
return cookies;
|
|
186
202
|
}
|
|
203
|
+
function extractPathname(url) {
|
|
204
|
+
const slashSlash = url.indexOf("//");
|
|
205
|
+
if (slashSlash === -1)
|
|
206
|
+
return "/";
|
|
207
|
+
const pathStart = url.indexOf("/", slashSlash + 2);
|
|
208
|
+
if (pathStart === -1)
|
|
209
|
+
return "/";
|
|
210
|
+
const qIdx = url.indexOf("?", pathStart);
|
|
211
|
+
return qIdx === -1 ? url.slice(pathStart) : url.slice(pathStart, qIdx);
|
|
212
|
+
}
|
|
213
|
+
function extractSearch(url) {
|
|
214
|
+
const qIdx = url.indexOf("?");
|
|
215
|
+
return qIdx === -1 ? "" : url.slice(qIdx + 1);
|
|
216
|
+
}
|
|
187
217
|
function detectIP(req) {
|
|
188
218
|
const forwarded = req.headers.get("x-forwarded-for");
|
|
189
|
-
if (forwarded)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const cfIp = req.headers.get("cf-connecting-ip");
|
|
195
|
-
if (cfIp)
|
|
196
|
-
return cfIp;
|
|
197
|
-
const clientIp = req.headers.get("x-client-ip");
|
|
198
|
-
if (clientIp)
|
|
199
|
-
return clientIp;
|
|
200
|
-
return "127.0.0.1";
|
|
219
|
+
if (forwarded) {
|
|
220
|
+
const comma = forwarded.indexOf(",");
|
|
221
|
+
return comma === -1 ? forwarded.trim() : forwarded.slice(0, comma).trim();
|
|
222
|
+
}
|
|
223
|
+
return req.headers.get("x-real-ip") ?? req.headers.get("cf-connecting-ip") ?? req.headers.get("x-client-ip") ?? "127.0.0.1";
|
|
201
224
|
}
|
|
202
225
|
function zodToJsonSchema(schema) {
|
|
203
226
|
const d = schema._def;
|
|
@@ -353,6 +376,7 @@ class Prince {
|
|
|
353
376
|
router = null;
|
|
354
377
|
staticRoutes = new Map;
|
|
355
378
|
staticMiddlewares = new Map;
|
|
379
|
+
staticComposed = new Map;
|
|
356
380
|
routeCache = new Map;
|
|
357
381
|
onRequestHooks = [];
|
|
358
382
|
onBeforeHandleHooks = [];
|
|
@@ -363,6 +387,9 @@ class Prince {
|
|
|
363
387
|
}
|
|
364
388
|
use(mw) {
|
|
365
389
|
this.middlewares.push(mw);
|
|
390
|
+
this.routeCache.clear();
|
|
391
|
+
this.staticComposed.clear();
|
|
392
|
+
this.router = null;
|
|
366
393
|
return this;
|
|
367
394
|
}
|
|
368
395
|
plugin(plugin, options) {
|
|
@@ -448,6 +475,8 @@ class Prince {
|
|
|
448
475
|
pattern: "",
|
|
449
476
|
handlers: {},
|
|
450
477
|
middlewares: {},
|
|
478
|
+
composedMiddlewares: {},
|
|
479
|
+
allowedMethods: new Set,
|
|
451
480
|
children: []
|
|
452
481
|
};
|
|
453
482
|
for (const route of this.rawRoutes) {
|
|
@@ -476,6 +505,8 @@ class Prince {
|
|
|
476
505
|
pattern: part,
|
|
477
506
|
handlers: {},
|
|
478
507
|
middlewares: {},
|
|
508
|
+
composedMiddlewares: {},
|
|
509
|
+
allowedMethods: new Set,
|
|
479
510
|
children: []
|
|
480
511
|
};
|
|
481
512
|
if (part.startsWith(":")) {
|
|
@@ -490,76 +521,71 @@ class Prince {
|
|
|
490
521
|
}
|
|
491
522
|
}
|
|
492
523
|
currentNode.handlers[route.method] = route.handler;
|
|
524
|
+
currentNode.allowedMethods.add(route.method);
|
|
493
525
|
if (route.middlewares.length > 0) {
|
|
494
526
|
currentNode.middlewares[route.method] = route.middlewares;
|
|
495
527
|
}
|
|
496
528
|
}
|
|
529
|
+
composeMW(routeMW) {
|
|
530
|
+
if (this.middlewares.length === 0)
|
|
531
|
+
return routeMW;
|
|
532
|
+
if (routeMW.length === 0)
|
|
533
|
+
return this.middlewares;
|
|
534
|
+
return [...this.middlewares, ...routeMW];
|
|
535
|
+
}
|
|
497
536
|
findRoute(method, pathname) {
|
|
498
537
|
const cacheKey = `${method}:${pathname}`;
|
|
499
538
|
if (this.routeCache.has(cacheKey)) {
|
|
500
539
|
return this.routeCache.get(cacheKey);
|
|
501
540
|
}
|
|
502
|
-
const
|
|
503
|
-
const staticHandler = this.staticRoutes.get(staticKey);
|
|
541
|
+
const staticHandler = this.staticRoutes.get(cacheKey);
|
|
504
542
|
if (staticHandler) {
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
params: {},
|
|
508
|
-
middlewares: this.staticMiddlewares.get(staticKey) || []
|
|
509
|
-
};
|
|
543
|
+
const composed = this.staticComposed.get(cacheKey) ?? this.composeMW(this.staticMiddlewares.get(cacheKey) ?? []);
|
|
544
|
+
const result2 = { handler: staticHandler, params: {}, middlewares: composed };
|
|
510
545
|
this.routeCache.set(cacheKey, result2);
|
|
511
546
|
return result2;
|
|
512
547
|
}
|
|
548
|
+
if (this.staticRoutes.size > 0) {
|
|
549
|
+
const methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
|
|
550
|
+
const allowed = methods.filter((m) => m !== method && this.staticRoutes.has(`${m}:${pathname}`));
|
|
551
|
+
if (allowed.length > 0) {
|
|
552
|
+
const r = { handler: null, params: {}, middlewares: [], allowedMethods: allowed };
|
|
553
|
+
this.routeCache.set(cacheKey, r);
|
|
554
|
+
return r;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
513
557
|
const segments = pathname === "/" ? [""] : pathname.split("/").slice(1);
|
|
514
558
|
const result = this.matchRoute(this.buildRouter(), segments, method);
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
allowedMethods.add(route.method);
|
|
523
|
-
}
|
|
559
|
+
this.routeCache.set(cacheKey, result);
|
|
560
|
+
return result;
|
|
561
|
+
}
|
|
562
|
+
composeRouterMiddlewares(node) {
|
|
563
|
+
for (const method of Object.keys(node.handlers)) {
|
|
564
|
+
const routeMW = node.middlewares[method] ?? [];
|
|
565
|
+
node.composedMiddlewares[method] = this.middlewares.length === 0 ? routeMW : routeMW.length === 0 ? this.middlewares : [...this.middlewares, ...routeMW];
|
|
524
566
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
handler: null,
|
|
528
|
-
params: {},
|
|
529
|
-
middlewares: [],
|
|
530
|
-
allowedMethods: Array.from(allowedMethods)
|
|
531
|
-
};
|
|
532
|
-
this.routeCache.set(cacheKey, methodNotAllowed);
|
|
533
|
-
return methodNotAllowed;
|
|
567
|
+
for (const child of node.children) {
|
|
568
|
+
this.composeRouterMiddlewares(child);
|
|
534
569
|
}
|
|
535
|
-
this.routeCache.set(cacheKey, null);
|
|
536
|
-
return null;
|
|
537
570
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
return true;
|
|
544
|
-
return false;
|
|
571
|
+
composeStaticMiddlewares() {
|
|
572
|
+
this.staticComposed.clear();
|
|
573
|
+
for (const [key, handler] of this.staticRoutes) {
|
|
574
|
+
const routeMW = this.staticMiddlewares.get(key) ?? [];
|
|
575
|
+
this.staticComposed.set(key, this.middlewares.length === 0 ? routeMW : routeMW.length === 0 ? this.middlewares : [...this.middlewares, ...routeMW]);
|
|
545
576
|
}
|
|
546
|
-
for (let i = 0;i < routeParts.length; i++) {
|
|
547
|
-
const routePart = routeParts[i];
|
|
548
|
-
const requestPart = requestParts[i];
|
|
549
|
-
if (routePart.startsWith(":") || routePart === "*" || routePart === "**") {
|
|
550
|
-
continue;
|
|
551
|
-
}
|
|
552
|
-
if (routePart !== requestPart) {
|
|
553
|
-
return false;
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
return true;
|
|
557
577
|
}
|
|
558
578
|
matchRoute(node, segments, method, params = {}, index = 0) {
|
|
559
579
|
if (index === segments.length) {
|
|
560
580
|
const handler = node.handlers[method];
|
|
561
|
-
|
|
562
|
-
|
|
581
|
+
if (handler) {
|
|
582
|
+
const middlewares = node.composedMiddlewares[method] ?? this.composeMW(node.middlewares[method] ?? []);
|
|
583
|
+
return { handler, params, middlewares };
|
|
584
|
+
}
|
|
585
|
+
if (node.allowedMethods.size > 0) {
|
|
586
|
+
return { handler: null, params, middlewares: [], allowedMethods: [...node.allowedMethods] };
|
|
587
|
+
}
|
|
588
|
+
return null;
|
|
563
589
|
}
|
|
564
590
|
const segment = segments[index];
|
|
565
591
|
for (const child of node.children) {
|
|
@@ -573,11 +599,15 @@ class Prince {
|
|
|
573
599
|
}
|
|
574
600
|
for (const child of node.children) {
|
|
575
601
|
if (child.paramName) {
|
|
602
|
+
const saved = params[child.paramName];
|
|
576
603
|
params[child.paramName] = segment;
|
|
577
604
|
const result = this.matchRoute(child, segments, method, params, index + 1);
|
|
578
605
|
if (result)
|
|
579
606
|
return result;
|
|
580
|
-
|
|
607
|
+
if (saved === undefined)
|
|
608
|
+
delete params[child.paramName];
|
|
609
|
+
else
|
|
610
|
+
params[child.paramName] = saved;
|
|
581
611
|
}
|
|
582
612
|
}
|
|
583
613
|
for (const child of node.children) {
|
|
@@ -590,39 +620,32 @@ class Prince {
|
|
|
590
620
|
for (const child of node.children) {
|
|
591
621
|
if (child.isCatchAll) {
|
|
592
622
|
const handler = child.handlers[method];
|
|
593
|
-
const middlewares = child.middlewares[method] || [];
|
|
594
623
|
if (handler) {
|
|
624
|
+
const middlewares = child.composedMiddlewares[method] ?? this.composeMW(child.middlewares[method] ?? []);
|
|
595
625
|
return { handler, params, middlewares };
|
|
596
626
|
}
|
|
627
|
+
if (child.allowedMethods.size > 0) {
|
|
628
|
+
return { handler: null, params, middlewares: [], allowedMethods: [...child.allowedMethods] };
|
|
629
|
+
}
|
|
597
630
|
}
|
|
598
631
|
}
|
|
599
632
|
return null;
|
|
600
633
|
}
|
|
601
634
|
async parseBody(req) {
|
|
602
635
|
const ct = req.headers.get("content-type") || "";
|
|
603
|
-
const clonedReq = req.clone();
|
|
604
636
|
try {
|
|
605
637
|
if (ct.includes("application/json")) {
|
|
606
|
-
return await
|
|
638
|
+
return await req.json();
|
|
607
639
|
}
|
|
608
640
|
if (ct.includes("application/x-www-form-urlencoded")) {
|
|
609
|
-
const text = await
|
|
641
|
+
const text = await req.text();
|
|
610
642
|
return Object.fromEntries(new URLSearchParams(text));
|
|
611
643
|
}
|
|
612
644
|
if (ct.startsWith("multipart/form-data")) {
|
|
613
|
-
|
|
614
|
-
const files = {};
|
|
615
|
-
const fields = {};
|
|
616
|
-
for (const [k, v] of fd.entries()) {
|
|
617
|
-
if (v instanceof File)
|
|
618
|
-
files[k] = v;
|
|
619
|
-
else
|
|
620
|
-
fields[k] = v;
|
|
621
|
-
}
|
|
622
|
-
return { files, fields };
|
|
645
|
+
return null;
|
|
623
646
|
}
|
|
624
647
|
if (ct.startsWith("text/")) {
|
|
625
|
-
return await
|
|
648
|
+
return await req.text();
|
|
626
649
|
}
|
|
627
650
|
} catch (error) {
|
|
628
651
|
console.error("Body parsing error:", error);
|
|
@@ -630,11 +653,31 @@ class Prince {
|
|
|
630
653
|
}
|
|
631
654
|
return null;
|
|
632
655
|
}
|
|
633
|
-
async executeHandler(req, handler, params,
|
|
656
|
+
async executeHandler(req, handler, params, search, routeMiddlewares, method, pathname) {
|
|
634
657
|
req.params = params;
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
658
|
+
let _query;
|
|
659
|
+
Object.defineProperty(req, "query", {
|
|
660
|
+
get() {
|
|
661
|
+
return _query ??= new URLSearchParams(search);
|
|
662
|
+
},
|
|
663
|
+
configurable: true
|
|
664
|
+
});
|
|
665
|
+
Object.defineProperty(req, "cookies", {
|
|
666
|
+
get() {
|
|
667
|
+
const val = parseCookies(req.headers.get("cookie") ?? "");
|
|
668
|
+
Object.defineProperty(req, "cookies", { value: val, configurable: true });
|
|
669
|
+
return val;
|
|
670
|
+
},
|
|
671
|
+
configurable: true
|
|
672
|
+
});
|
|
673
|
+
Object.defineProperty(req, "ip", {
|
|
674
|
+
get() {
|
|
675
|
+
const val = detectIP(req);
|
|
676
|
+
Object.defineProperty(req, "ip", { value: val, configurable: true });
|
|
677
|
+
return val;
|
|
678
|
+
},
|
|
679
|
+
configurable: true
|
|
680
|
+
});
|
|
638
681
|
if (["POST", "PUT", "PATCH"].includes(req.method) && !req.parsedBody) {
|
|
639
682
|
const parsed = await this.parseBody(req);
|
|
640
683
|
if (parsed) {
|
|
@@ -649,7 +692,7 @@ class Prince {
|
|
|
649
692
|
for (const hook of this.onBeforeHandleHooks) {
|
|
650
693
|
await hook(req, pathname, method);
|
|
651
694
|
}
|
|
652
|
-
const allMiddlewares = routeMiddlewares
|
|
695
|
+
const allMiddlewares = routeMiddlewares;
|
|
653
696
|
let i = 0;
|
|
654
697
|
const next = async () => {
|
|
655
698
|
while (i < allMiddlewares.length) {
|
|
@@ -669,16 +712,18 @@ class Prince {
|
|
|
669
712
|
return this.json(res);
|
|
670
713
|
};
|
|
671
714
|
const response = await next();
|
|
672
|
-
|
|
673
|
-
|
|
715
|
+
if (this.onAfterHandleHooks.length > 0) {
|
|
716
|
+
for (const hook of this.onAfterHandleHooks) {
|
|
717
|
+
await hook(req, response, pathname, method);
|
|
718
|
+
}
|
|
674
719
|
}
|
|
675
720
|
return response;
|
|
676
721
|
}
|
|
677
722
|
async handleFetch(req) {
|
|
678
|
-
const
|
|
723
|
+
const rawUrl = req.url;
|
|
724
|
+
const pathname = extractPathname(rawUrl);
|
|
679
725
|
const r = req;
|
|
680
726
|
const method = req.method;
|
|
681
|
-
const pathname = url.pathname;
|
|
682
727
|
for (const hook of this.onRequestHooks) {
|
|
683
728
|
await hook(r);
|
|
684
729
|
}
|
|
@@ -695,15 +740,16 @@ class Prince {
|
|
|
695
740
|
}
|
|
696
741
|
});
|
|
697
742
|
}
|
|
698
|
-
|
|
743
|
+
const search = extractSearch(rawUrl);
|
|
744
|
+
return this.executeHandler(r, routeMatch.handler, routeMatch.params, search, routeMatch.middlewares, method, pathname);
|
|
699
745
|
}
|
|
700
746
|
async fetch(req) {
|
|
701
|
-
const
|
|
702
|
-
const pathname = url.pathname;
|
|
747
|
+
const rawUrl = req.url;
|
|
703
748
|
const method = req.method;
|
|
704
749
|
try {
|
|
705
750
|
return await this.handleFetch(req);
|
|
706
751
|
} catch (err) {
|
|
752
|
+
const pathname = extractPathname(rawUrl);
|
|
707
753
|
for (const hook of this.onErrorHooks) {
|
|
708
754
|
await hook(err, req, pathname, method);
|
|
709
755
|
}
|
|
@@ -768,12 +814,15 @@ class Prince {
|
|
|
768
814
|
}
|
|
769
815
|
listen(port = 3000) {
|
|
770
816
|
const self = this;
|
|
817
|
+
const router = this.buildRouter();
|
|
818
|
+
this.composeRouterMiddlewares(router);
|
|
819
|
+
this.composeStaticMiddlewares();
|
|
771
820
|
Bun.serve({
|
|
772
821
|
port,
|
|
773
822
|
fetch: (req, server) => {
|
|
774
|
-
const
|
|
775
|
-
if (self.wsRoutes[
|
|
776
|
-
data: { path:
|
|
823
|
+
const pathname = extractPathname(req.url);
|
|
824
|
+
if (self.wsRoutes[pathname] && server.upgrade(req, {
|
|
825
|
+
data: { path: pathname }
|
|
777
826
|
})) {
|
|
778
827
|
return;
|
|
779
828
|
}
|
package/package.json
CHANGED