princejs 1.9.7 โ 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +209 -7
- package/dist/adapters/node.d.ts +47 -0
- package/dist/adapters/node.d.ts.map +1 -0
- package/dist/middleware.js +246 -272
- package/dist/prince.d.ts +26 -4
- package/dist/prince.d.ts.map +1 -1
- package/dist/prince.js +92 -7
- package/package.json +11 -8
package/README.md
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
<img src="./src/images/og.png" alt="PrinceJS"/>
|
|
4
|
-
|
|
5
3
|
# ๐ PrinceJS
|
|
6
4
|
|
|
7
5
|
**Ultra-clean, modern & minimal Bun web framework.**
|
|
@@ -65,6 +63,7 @@ app.listen(3000);
|
|
|
65
63
|
| Routing | `princejs` |
|
|
66
64
|
| Middleware (CORS, Logger, Rate Limit, Auth, JWT) | `princejs/middleware` |
|
|
67
65
|
| Zod Validation | `princejs/middleware` |
|
|
66
|
+
| **Cookies & IP Detection** | `princejs` |
|
|
68
67
|
| File Uploads | `princejs/helpers` |
|
|
69
68
|
| WebSockets | `princejs` |
|
|
70
69
|
| Server-Sent Events | `princejs/helpers` |
|
|
@@ -77,7 +76,93 @@ app.listen(3000);
|
|
|
77
76
|
| SQLite Database | `princejs/db` |
|
|
78
77
|
| Plugin System | `princejs` |
|
|
79
78
|
| End-to-End Type Safety | `princejs/client` |
|
|
80
|
-
| Deploy Adapters | `princejs/vercel` ยท `princejs/cloudflare` ยท `princejs/deno` |
|
|
79
|
+
| Deploy Adapters | `princejs/vercel` ยท `princejs/cloudflare` ยท `princejs/deno` ยท `princejs/node` |
|
|
80
|
+
| Lifecycle Hooks | `princejs` |
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## ๐ช Cookies & ๐ IP Detection
|
|
85
|
+
|
|
86
|
+
### Reading Cookies
|
|
87
|
+
|
|
88
|
+
Cookies are automatically parsed from the request:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
app.get("/profile", (req) => ({
|
|
92
|
+
sessionId: req.cookies?.sessionId,
|
|
93
|
+
theme: req.cookies?.theme,
|
|
94
|
+
allCookies: req.cookies // Record<string, string>
|
|
95
|
+
}));
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Setting Cookies
|
|
99
|
+
|
|
100
|
+
Use the response builder to set cookies with full control:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
app.get("/login", (req) =>
|
|
104
|
+
app.response()
|
|
105
|
+
.status(200)
|
|
106
|
+
.json({ ok: true })
|
|
107
|
+
.cookie("sessionId", "abc123", {
|
|
108
|
+
maxAge: 3600, // 1 hour
|
|
109
|
+
path: "/",
|
|
110
|
+
httpOnly: true, // not accessible from JS
|
|
111
|
+
secure: true, // HTTPS only
|
|
112
|
+
sameSite: "Strict" // CSRF protection
|
|
113
|
+
})
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// Chain multiple cookies
|
|
117
|
+
app.response()
|
|
118
|
+
.json({ ok: true })
|
|
119
|
+
.cookie("session", "xyz")
|
|
120
|
+
.cookie("theme", "dark")
|
|
121
|
+
.cookie("lang", "en");
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Client IP Detection
|
|
125
|
+
|
|
126
|
+
Automatically detect client IP from request headers:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
app.get("/api/data", (req) => ({
|
|
130
|
+
clientIp: req.ip,
|
|
131
|
+
data: [...]
|
|
132
|
+
}));
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Supported headers** (in priority order):
|
|
136
|
+
- `X-Forwarded-For` โ Load balancers, proxies (first IP in list)
|
|
137
|
+
- `X-Real-IP` โ Nginx, Apache reverse proxy
|
|
138
|
+
- `CF-Connecting-IP` โ Cloudflare
|
|
139
|
+
- `X-Client-IP` โ Other proxy services
|
|
140
|
+
- Fallback โ `127.0.0.1` (localhost)
|
|
141
|
+
|
|
142
|
+
**Use cases:**
|
|
143
|
+
- ๐ Rate limiting per IP
|
|
144
|
+
- ๐ Geolocation analytics
|
|
145
|
+
- ๐จ IP-based access control
|
|
146
|
+
- ๐ฅ User tracking & fraud detection
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
// Rate limit by IP
|
|
150
|
+
app.use((req, next) => {
|
|
151
|
+
const ip = req.ip;
|
|
152
|
+
const count = ipTracker.getCount(ip) || 0;
|
|
153
|
+
if (count > 100) return new Response("Too many requests", { status: 429 });
|
|
154
|
+
ipTracker.increment(ip);
|
|
155
|
+
return next();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// IP-based security
|
|
159
|
+
app.post("/admin", (req) => {
|
|
160
|
+
if (!ALLOWED_IPS.includes(req.ip!)) {
|
|
161
|
+
return new Response("Forbidden", { status: 403 });
|
|
162
|
+
}
|
|
163
|
+
return { authorized: true };
|
|
164
|
+
});
|
|
165
|
+
```
|
|
81
166
|
|
|
82
167
|
---
|
|
83
168
|
|
|
@@ -160,6 +245,57 @@ app.plugin(usersPlugin, { prefix: "/api" });
|
|
|
160
245
|
|
|
161
246
|
---
|
|
162
247
|
|
|
248
|
+
## ๐ฃ Lifecycle Hooks
|
|
249
|
+
|
|
250
|
+
React to key moments in request processing with lifecycle hooks:
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
import { prince } from "princejs";
|
|
254
|
+
|
|
255
|
+
const app = prince();
|
|
256
|
+
|
|
257
|
+
// Called for every incoming request
|
|
258
|
+
app.onRequest((req) => {
|
|
259
|
+
console.log(`๐ฅ Request received: ${req.method} ${req.url}`);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Called before handler execution
|
|
263
|
+
app.onBeforeHandle((req, path, method) => {
|
|
264
|
+
console.log(`๐ About to handle: ${method} ${path}`);
|
|
265
|
+
(req as any).startTime = Date.now();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Called after successful handler execution
|
|
269
|
+
app.onAfterHandle((req, res, path, method) => {
|
|
270
|
+
const duration = Date.now() - (req as any).startTime;
|
|
271
|
+
console.log(`โ
Response: ${method} ${path} ${res.status} (${duration}ms)`);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// Called when handler throws an error
|
|
275
|
+
app.onError((err, req, path, method) => {
|
|
276
|
+
console.error(`โ Error in ${method} ${path}:`, err.message);
|
|
277
|
+
// Send alert, log to monitoring service, etc.
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
app.get("/users", () => ({ users: [] }));
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**Hook execution order:**
|
|
284
|
+
1. `onRequest` โ early for request-wide setup
|
|
285
|
+
2. `onBeforeHandle` โ just before route handler runs
|
|
286
|
+
3. Handler executes
|
|
287
|
+
4. `onAfterHandle` โ after success (on error, skipped)
|
|
288
|
+
5. `onError` โ only if handler throws (skips onAfterHandle)
|
|
289
|
+
|
|
290
|
+
**Use cases:**
|
|
291
|
+
- ๐ Metrics & observability
|
|
292
|
+
- ๐ Request inspection & debugging
|
|
293
|
+
- โฑ๏ธ Timing & performance monitoring
|
|
294
|
+
- ๐จ Error tracking & alerting
|
|
295
|
+
- ๐ Security audits & compliance logging
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
163
299
|
## ๐ End-to-End Type Safety
|
|
164
300
|
|
|
165
301
|
Define a contract once โ your client gets full TypeScript autocompletion automatically:
|
|
@@ -206,6 +342,25 @@ import { toDeno } from "princejs/deno";
|
|
|
206
342
|
Deno.serve(toDeno(app));
|
|
207
343
|
```
|
|
208
344
|
|
|
345
|
+
**Node Adapter** - `server.ts`
|
|
346
|
+
```ts
|
|
347
|
+
import { createServer } from "http";
|
|
348
|
+
import { toNode, toExpress } from "princejs/node";
|
|
349
|
+
|
|
350
|
+
const app = prince();
|
|
351
|
+
app.get("/", () => ({ message: "Hello from Node!" }));
|
|
352
|
+
|
|
353
|
+
// Native Node.js http
|
|
354
|
+
const server = createServer(toNode(app));
|
|
355
|
+
server.listen(3000);
|
|
356
|
+
|
|
357
|
+
// Or with Express
|
|
358
|
+
import express from "express";
|
|
359
|
+
const expressApp = express();
|
|
360
|
+
expressApp.all("*", toExpress(app));
|
|
361
|
+
expressApp.listen(3000);
|
|
362
|
+
```
|
|
363
|
+
|
|
209
364
|
---
|
|
210
365
|
|
|
211
366
|
## ๐ฏ Full Example
|
|
@@ -222,7 +377,29 @@ import { z } from "zod";
|
|
|
222
377
|
|
|
223
378
|
const app = prince(true);
|
|
224
379
|
|
|
225
|
-
//
|
|
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
|
+
});
|
|
390
|
+
|
|
391
|
+
app.onAfterHandle((req, res, path, method) => {
|
|
392
|
+
const duration = Date.now() - (req as any).startTime;
|
|
393
|
+
console.log(`โ
${method} ${path} โ ${res.status} (${duration}ms)`);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
app.onError((err, req, path, method) => {
|
|
397
|
+
console.error(`โ ${method} ${path} failed:`, err.message);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// ==========================================
|
|
401
|
+
// GLOBAL MIDDLEWARE
|
|
402
|
+
// ==========================================
|
|
226
403
|
app.use(cors());
|
|
227
404
|
app.use(logger());
|
|
228
405
|
app.use(rateLimit({ max: 100, window: 60 }));
|
|
@@ -231,10 +408,31 @@ app.use(jwt(key));
|
|
|
231
408
|
app.use(session({ secret: "key" }));
|
|
232
409
|
app.use(compress());
|
|
233
410
|
|
|
411
|
+
// ==========================================
|
|
412
|
+
// ROUTES
|
|
413
|
+
// ==========================================
|
|
414
|
+
|
|
234
415
|
// JSX
|
|
235
416
|
const Page = () => Html(Head("Test Page"), Body(H1("Hello World"), P("This is a test")));
|
|
236
417
|
app.get("/jsx", () => render(Page()));
|
|
237
418
|
|
|
419
|
+
// Cookies & IP Detection
|
|
420
|
+
app.post("/login", (req) =>
|
|
421
|
+
app.response()
|
|
422
|
+
.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
|
|
428
|
+
})
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
app.get("/profile", (req) => ({
|
|
432
|
+
sessionId: req.cookies?.sessionId,
|
|
433
|
+
clientIp: req.ip,
|
|
434
|
+
}));
|
|
435
|
+
|
|
238
436
|
// Database
|
|
239
437
|
const users = db.sqlite("./db.sqlite", "CREATE TABLE users...");
|
|
240
438
|
app.get("/users", () => users.query("SELECT * FROM users"));
|
|
@@ -247,7 +445,7 @@ app.ws("/chat", {
|
|
|
247
445
|
|
|
248
446
|
// Auth
|
|
249
447
|
app.get("/protected", auth(), (req) => ({ user: req.user }));
|
|
250
|
-
app.get("/api", apiKey({ keys: ["key_123"] }),
|
|
448
|
+
app.get("/api", apiKey({ keys: ["key_123"] }), (req) => ({ ok: true }));
|
|
251
449
|
|
|
252
450
|
// Helpers
|
|
253
451
|
app.get("/data", cache(60)(() => ({ time: Date.now() })));
|
|
@@ -256,10 +454,14 @@ app.get("/events", sse(), (req) => {
|
|
|
256
454
|
setInterval(() => req.sseSend({ time: Date.now() }), 1000);
|
|
257
455
|
});
|
|
258
456
|
|
|
259
|
-
//
|
|
457
|
+
// ==========================================
|
|
458
|
+
// CRON JOBS
|
|
459
|
+
// ==========================================
|
|
260
460
|
cron("*/1 * * * *", () => console.log("PrinceJS heartbeat"));
|
|
261
461
|
|
|
262
|
-
//
|
|
462
|
+
// ==========================================
|
|
463
|
+
// OPENAPI + SCALAR DOCS
|
|
464
|
+
// ==========================================
|
|
263
465
|
const api = app.openapi({ title: "PrinceJS App", version: "1.0.0" }, "/docs");
|
|
264
466
|
api.route("GET", "/items", {
|
|
265
467
|
summary: "List items",
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Official Node.js deploy adapter for PrinceJS
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* // server.ts
|
|
6
|
+
* import { createServer } from "http";
|
|
7
|
+
* import { prince } from "princejs";
|
|
8
|
+
* import { toNode } from "princejs/node";
|
|
9
|
+
*
|
|
10
|
+
* const app = prince();
|
|
11
|
+
* app.get("/", () => ({ message: "Hello from Node.js!" }));
|
|
12
|
+
*
|
|
13
|
+
* const server = createServer(toNode(app));
|
|
14
|
+
* server.listen(3000, () => {
|
|
15
|
+
* console.log("Listening on http://localhost:3000");
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // With Express.js
|
|
20
|
+
* import express from "express";
|
|
21
|
+
* import { prince } from "princejs";
|
|
22
|
+
* import { toExpress } from "princejs/node";
|
|
23
|
+
*
|
|
24
|
+
* const app = express();
|
|
25
|
+
* const princeApp = prince();
|
|
26
|
+
* princeApp.get("/", () => ({ message: "Hello!" }));
|
|
27
|
+
*
|
|
28
|
+
* app.all("*", toExpress(princeApp));
|
|
29
|
+
* app.listen(3000);
|
|
30
|
+
*/
|
|
31
|
+
export type PrinceApp = {
|
|
32
|
+
fetch(request: Request): Promise<Response>;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Converts a Prince app to a Node.js http.createServer() handler
|
|
36
|
+
* @example
|
|
37
|
+
* const server = createServer(toNode(app));
|
|
38
|
+
* server.listen(3000);
|
|
39
|
+
*/
|
|
40
|
+
export declare function toNode(app: PrinceApp): (req: any, res: any) => Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Converts a Prince app to an Express.js middleware handler
|
|
43
|
+
* @example
|
|
44
|
+
* app.all("*", toExpress(princeApp));
|
|
45
|
+
*/
|
|
46
|
+
export declare function toExpress(app: PrinceApp): (req: any, res: any, next?: any) => Promise<void>;
|
|
47
|
+
//# sourceMappingURL=node.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../../src/adapters/node.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,MAAM,MAAM,SAAS,GAAG;IAAE,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;CAAE,CAAC;AAEvE;;;;;GAKG;AACH,wBAAgB,MAAM,CACpB,GAAG,EAAE,SAAS,GACb,CACD,GAAG,EAAE,GAAG,EACR,GAAG,EAAE,GAAG,KACL,OAAO,CAAC,IAAI,CAAC,CAuCjB;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,GAAG,EAAE,SAAS,GACb,CACD,GAAG,EAAE,GAAG,EACR,GAAG,EAAE,GAAG,EACR,IAAI,CAAC,EAAE,GAAG,KACP,OAAO,CAAC,IAAI,CAAC,CAoCjB"}
|