bunigniter 0.2.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/LICENSE +21 -0
- package/README.md +229 -0
- package/dist/LICENSE +21 -0
- package/dist/README.md +229 -0
- package/dist/base/controller.ts +324 -0
- package/dist/base/index.ts +5 -0
- package/dist/base/service.ts +21 -0
- package/dist/cli/index.ts +318 -0
- package/dist/cli/list-routes.ts +72 -0
- package/dist/cli/repl.ts +461 -0
- package/dist/cli/templates.ts +283 -0
- package/dist/client/index.ts +159 -0
- package/dist/db/drizzle.ts +550 -0
- package/dist/db/validators.ts +229 -0
- package/dist/edge-builder.ts +120 -0
- package/dist/edge.ts +69 -0
- package/dist/helpers/cache.ts +173 -0
- package/dist/helpers/cors.ts +103 -0
- package/dist/helpers/csrf.ts +155 -0
- package/dist/helpers/debug.ts +158 -0
- package/dist/helpers/env.ts +147 -0
- package/dist/helpers/handler.ts +158 -0
- package/dist/helpers/http.ts +194 -0
- package/dist/helpers/image.ts +217 -0
- package/dist/helpers/jwt.ts +147 -0
- package/dist/helpers/logger.ts +96 -0
- package/dist/helpers/mail.ts +272 -0
- package/dist/helpers/middleware-loader.ts +116 -0
- package/dist/helpers/middleware.ts +57 -0
- package/dist/helpers/modules.ts +115 -0
- package/dist/helpers/openapi.ts +140 -0
- package/dist/helpers/pagination.ts +159 -0
- package/dist/helpers/queue.ts +186 -0
- package/dist/helpers/request-context.ts +13 -0
- package/dist/helpers/request.ts +376 -0
- package/dist/helpers/schedule.ts +173 -0
- package/dist/helpers/session-middleware.ts +89 -0
- package/dist/helpers/session.ts +286 -0
- package/dist/helpers/sse.ts +90 -0
- package/dist/helpers/throttle.ts +156 -0
- package/dist/helpers/upload.ts +417 -0
- package/dist/helpers/validator.ts +287 -0
- package/dist/helpers/ws.ts +123 -0
- package/dist/index.ts +221 -0
- package/dist/package.json +70 -0
- package/dist/router/file-router.ts +541 -0
- package/dist/router/server-router.ts +103 -0
- package/dist/view/page.ts +96 -0
- package/dist/view/renderer.tsx +390 -0
- package/dist/view/view-response.ts +10 -0
- package/package.json +70 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket — simple real-time communication.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* // routes/ws.ts — define WebSocket handlers per room/topic
|
|
7
|
+
* import { ws } from 'bunigniter/helpers/ws'
|
|
8
|
+
*
|
|
9
|
+
* // Echo server
|
|
10
|
+
* ws.handle('/ws/echo', {
|
|
11
|
+
* open(ws) { console.log('connected') },
|
|
12
|
+
* message(ws, data) { ws.send('echo: ' + data) },
|
|
13
|
+
* close(ws) { console.log('disconnected') },
|
|
14
|
+
* })
|
|
15
|
+
*
|
|
16
|
+
* // Chat room
|
|
17
|
+
* ws.handle('/ws/chat', {
|
|
18
|
+
* message(ws, data) {
|
|
19
|
+
* ws.publish('chat', JSON.stringify({ user: ws.id, msg: data }))
|
|
20
|
+
* },
|
|
21
|
+
* })
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
import { Elysia } from 'elysia'
|
|
25
|
+
|
|
26
|
+
type WSHandler = {
|
|
27
|
+
open?: (ws: any) => void
|
|
28
|
+
message?: (ws: any, data: any) => void
|
|
29
|
+
close?: (ws: any) => void
|
|
30
|
+
drain?: (ws: any) => void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type WSOptions = {
|
|
34
|
+
/** Schema for incoming messages (validated by Elysia). */
|
|
35
|
+
body?: any
|
|
36
|
+
/** Schema for outgoing messages. */
|
|
37
|
+
response?: any
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class WSManager {
|
|
41
|
+
private app: any = null
|
|
42
|
+
private handlers: Map<string, WSHandler> = new Map()
|
|
43
|
+
private options: Map<string, WSOptions> = new Map()
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Register a WebSocket handler for a path.
|
|
47
|
+
*
|
|
48
|
+
* @param path - WebSocket endpoint path (e.g. '/ws/chat')
|
|
49
|
+
* @param handler - Event handlers
|
|
50
|
+
* @param options - Optional schema config
|
|
51
|
+
*/
|
|
52
|
+
handle(path: string, handler: WSHandler, options?: WSOptions): void {
|
|
53
|
+
this.handlers.set(path, handler)
|
|
54
|
+
if (options) this.options.set(path, options)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get the Elysia WS config object for a path.
|
|
59
|
+
* Called internally during app initialization.
|
|
60
|
+
*/
|
|
61
|
+
getConfig(path: string): any {
|
|
62
|
+
const handler = this.handlers.get(path)
|
|
63
|
+
if (!handler) return null
|
|
64
|
+
|
|
65
|
+
const config: any = {}
|
|
66
|
+
if (handler.open) config.open = handler.open
|
|
67
|
+
if (handler.drain) config.drain = handler.drain
|
|
68
|
+
if (handler.close) handler.close
|
|
69
|
+
|
|
70
|
+
if (handler.message) {
|
|
71
|
+
config.message = handler.message
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const opts = this.options.get(path)
|
|
75
|
+
if (opts?.body) config.body = opts.body
|
|
76
|
+
if (opts?.response) config.response = opts.response
|
|
77
|
+
|
|
78
|
+
return config
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Get all registered WS paths. */
|
|
82
|
+
get paths(): string[] {
|
|
83
|
+
return [...this.handlers.keys()]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Mount all registered WebSocket handlers onto an Elysia app.
|
|
88
|
+
* Called automatically by the framework.
|
|
89
|
+
*/
|
|
90
|
+
mount(app: any): void {
|
|
91
|
+
for (const [path, handler] of this.handlers) {
|
|
92
|
+
const opts = this.options.get(path) ?? {}
|
|
93
|
+
const config: any = {}
|
|
94
|
+
|
|
95
|
+
if (handler.open) config.open = handler.open
|
|
96
|
+
if (handler.message) config.message = handler.message
|
|
97
|
+
if (handler.close) config.close = handler.close
|
|
98
|
+
if (handler.drain) config.drain = handler.drain
|
|
99
|
+
if (opts.body) config.body = opts.body
|
|
100
|
+
if (opts.response) config.response = opts.response
|
|
101
|
+
|
|
102
|
+
// Elysia v2: app.ws(path, options) or app.ws(path, handler)
|
|
103
|
+
// If there's only a message handler, use the 2-arg form
|
|
104
|
+
if (handler.open || handler.close || handler.drain || opts.body || opts.response) {
|
|
105
|
+
;(app as any).ws(path, config)
|
|
106
|
+
} else {
|
|
107
|
+
;(app as any).ws(path, handler.message!)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log(`[ws] ${path}`)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Singleton WS manager. */
|
|
116
|
+
export const ws = new WSManager()
|
|
117
|
+
|
|
118
|
+
/** Broadcast a message to all clients in a room. */
|
|
119
|
+
export function broadcast(room: string, message: string): void {
|
|
120
|
+
// Elysia handles room broadcast via ws.publish
|
|
121
|
+
// This function is a placeholder for server-level broadcast
|
|
122
|
+
console.log(`[ws] broadcast to ${room}: ${message}`)
|
|
123
|
+
}
|
package/dist/index.ts
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bunigniter — Main Entry Point.
|
|
3
|
+
*
|
|
4
|
+
* Bun-native fullstack framework inspired by CodeIgniter.
|
|
5
|
+
* Built on Elysia v2 + Drizzle ORM.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```bash
|
|
9
|
+
* bun run src/index.ts
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* Or with hot reload:
|
|
13
|
+
* ```bash
|
|
14
|
+
* bun --hot run src/index.ts
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { existsSync } from "node:fs";
|
|
19
|
+
import { Elysia } from "elysia";
|
|
20
|
+
import { DbClient } from "./db/drizzle";
|
|
21
|
+
import { registerFileRoutes } from "./router/file-router";
|
|
22
|
+
import { registerServerRoutes } from "./router/server-router";
|
|
23
|
+
import { loadEnv } from "./helpers/env";
|
|
24
|
+
import {
|
|
25
|
+
sessionMiddleware,
|
|
26
|
+
authMiddleware,
|
|
27
|
+
} from "./helpers/session-middleware";
|
|
28
|
+
import { applyMiddleware } from "./helpers/middleware";
|
|
29
|
+
import type { MiddlewareConfig } from "./helpers/middleware";
|
|
30
|
+
import { setViewsDir } from "./view/renderer";
|
|
31
|
+
import { registerModules } from "./helpers/modules";
|
|
32
|
+
import { ws } from "./helpers/ws";
|
|
33
|
+
import { openapi } from "./helpers/openapi";
|
|
34
|
+
import { createCache } from "./helpers/cache";
|
|
35
|
+
import { createQueue } from "./helpers/queue";
|
|
36
|
+
import { createUpload } from "./helpers/upload";
|
|
37
|
+
import { createMail } from "./helpers/mail";
|
|
38
|
+
|
|
39
|
+
export { Controller, Service } from "./base/index";
|
|
40
|
+
export { DbClient } from "./db/drizzle";
|
|
41
|
+
export { RequestProxy } from "./helpers/request";
|
|
42
|
+
|
|
43
|
+
interface AppConfig {
|
|
44
|
+
port?: number | string;
|
|
45
|
+
host?: string;
|
|
46
|
+
db?: { dialect: string; connection: Record<string, any>; logging?: boolean };
|
|
47
|
+
databases?: Record<
|
|
48
|
+
string,
|
|
49
|
+
{ dialect: string; connection: Record<string, any>; logging?: boolean }
|
|
50
|
+
>;
|
|
51
|
+
router?: { prefix?: string; directory?: string };
|
|
52
|
+
view?: { directory?: string; scripts?: string[] };
|
|
53
|
+
app?: { key?: string; debug?: boolean };
|
|
54
|
+
middleware?: MiddlewareConfig;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function loadConfig(): Promise<AppConfig> {
|
|
58
|
+
// Load .env files first
|
|
59
|
+
loadEnv();
|
|
60
|
+
try {
|
|
61
|
+
const mod = await import(
|
|
62
|
+
/* @vite-ignore */ join(process.cwd(), "config/app.ts")
|
|
63
|
+
);
|
|
64
|
+
return (mod as any).default ?? {};
|
|
65
|
+
} catch {
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Create and start the application.
|
|
72
|
+
*/
|
|
73
|
+
async function main() {
|
|
74
|
+
const config: AppConfig = await loadConfig();
|
|
75
|
+
const port = Number(config.port ?? 3000);
|
|
76
|
+
const dbConfig = config.db;
|
|
77
|
+
|
|
78
|
+
// ─── Database ─────────────────────────────────────────────
|
|
79
|
+
let db: DbClient | undefined;
|
|
80
|
+
|
|
81
|
+
if (dbConfig) {
|
|
82
|
+
db = new DbClient(dbConfig);
|
|
83
|
+
await db.open();
|
|
84
|
+
console.log(`[db] connected: ${dbConfig.dialect}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ─── Elysia App ────────────────────────────────────────────
|
|
88
|
+
const app = new Elysia();
|
|
89
|
+
|
|
90
|
+
// Make db available in Elysia context
|
|
91
|
+
if (db) {
|
|
92
|
+
app.decorate("db", db);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Apply global middleware (CORS, Logger, CSRF, Rate Limit)
|
|
96
|
+
applyMiddleware(app, config.middleware);
|
|
97
|
+
|
|
98
|
+
// Global middleware: Session + Auth
|
|
99
|
+
app.use(sessionMiddleware({ key: config.app?.key }));
|
|
100
|
+
app.use(authMiddleware());
|
|
101
|
+
|
|
102
|
+
// ─── View Engine ───────────────────────────────────────────
|
|
103
|
+
if (config.view?.directory) {
|
|
104
|
+
setViewsDir(config.view.directory);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ─── Named Databases ───────────────────────────────────────
|
|
108
|
+
const namedDbs: Record<string, DbClient> = {};
|
|
109
|
+
if (config.databases) {
|
|
110
|
+
for (const [name, dbConfig] of Object.entries(config.databases)) {
|
|
111
|
+
try {
|
|
112
|
+
const namedDb = new DbClient(dbConfig);
|
|
113
|
+
await namedDb.open();
|
|
114
|
+
namedDbs[name] = namedDb;
|
|
115
|
+
console.log(`[db] ${name}: ${dbConfig.dialect}`);
|
|
116
|
+
} catch (e: any) {
|
|
117
|
+
console.error(`[db] ${name}: failed - ${e.message}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ─── Services (Cache, Queue, Upload, Mail) ─────────────────
|
|
123
|
+
const cache = createCache();
|
|
124
|
+
const queue = createQueue();
|
|
125
|
+
const upload = createUpload();
|
|
126
|
+
const mail = createMail({
|
|
127
|
+
transport:
|
|
128
|
+
process.env.NODE_ENV === "production"
|
|
129
|
+
? undefined // SMTP in production
|
|
130
|
+
: undefined, // Null in dev (set via env)
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// ─── File-based Routes ────────────────────────────────────
|
|
134
|
+
const routerPrefix = config.router?.prefix ?? "/api";
|
|
135
|
+
const pagesDir = config.router?.directory ?? "routes";
|
|
136
|
+
|
|
137
|
+
await registerFileRoutes(app, {
|
|
138
|
+
directory: pagesDir,
|
|
139
|
+
prefix: routerPrefix,
|
|
140
|
+
db,
|
|
141
|
+
dbs: namedDbs,
|
|
142
|
+
cache,
|
|
143
|
+
queue,
|
|
144
|
+
upload,
|
|
145
|
+
mail,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// ─── Static Files ────────────────────────────────────────
|
|
149
|
+
const publicDir = join(process.cwd(), "public");
|
|
150
|
+
if (existsSync(publicDir)) {
|
|
151
|
+
const { readFileSync } = await import("node:fs");
|
|
152
|
+
app.get("/public/:file", async (ctx: any) => {
|
|
153
|
+
const file = ctx.params?.file;
|
|
154
|
+
if (!file || file.includes(".."))
|
|
155
|
+
return new Response("Not Found", { status: 404 });
|
|
156
|
+
try {
|
|
157
|
+
const content = readFileSync(join(publicDir, file));
|
|
158
|
+
const ext = file.split(".").pop();
|
|
159
|
+
const types: Record<string, string> = {
|
|
160
|
+
js: "application/javascript",
|
|
161
|
+
css: "text/css",
|
|
162
|
+
html: "text/html",
|
|
163
|
+
png: "image/png",
|
|
164
|
+
svg: "image/svg+xml",
|
|
165
|
+
};
|
|
166
|
+
return new Response(content, {
|
|
167
|
+
headers: { "content-type": types[ext] ?? "application/octet-stream" },
|
|
168
|
+
});
|
|
169
|
+
} catch {
|
|
170
|
+
return new Response("Not Found", { status: 404 });
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ─── HMVC Modules ──────────────────────────────────────────
|
|
176
|
+
await registerModules(app, { db, dbs: namedDbs, cache, queue, upload, mail });
|
|
177
|
+
|
|
178
|
+
// ─── WebSocket ───────────────────────────────────────────────
|
|
179
|
+
ws.mount(app);
|
|
180
|
+
|
|
181
|
+
// ─── Server Routes (Void-style) ───────────────────────────
|
|
182
|
+
const routesDir = "routes";
|
|
183
|
+
if (existsSync(routesDir)) {
|
|
184
|
+
await registerServerRoutes(app, routesDir, "");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ─── OpenAPI Documentation ───────────────────────────────
|
|
188
|
+
openapi(app, {
|
|
189
|
+
title: "Bunigniter API",
|
|
190
|
+
version: "0.1.0",
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// ─── Health Check ─────────────────────────────────────────
|
|
194
|
+
app.get(
|
|
195
|
+
"/health",
|
|
196
|
+
() =>
|
|
197
|
+
new Response(
|
|
198
|
+
JSON.stringify({
|
|
199
|
+
status: "ok",
|
|
200
|
+
uptime: process.uptime(),
|
|
201
|
+
timestamp: new Date().toISOString(),
|
|
202
|
+
}),
|
|
203
|
+
{
|
|
204
|
+
headers: { "content-type": "application/json" },
|
|
205
|
+
},
|
|
206
|
+
),
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
// ─── Start Server ─────────────────────────────────────────
|
|
210
|
+
app.listen(port, () => {
|
|
211
|
+
console.log(`\n 🚀 Bunigniter ready at http://localhost:${port}`);
|
|
212
|
+
console.log(` 📁 Routes: ./${pagesDir}/`);
|
|
213
|
+
console.log(` 🔗 Routes: ${routerPrefix}/*`);
|
|
214
|
+
console.log(` 💾 Database: ${dbConfig?.dialect ?? "none"}\n`);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
main().catch((err) => {
|
|
219
|
+
console.error("[nexus] failed to start:", err);
|
|
220
|
+
process.exit(1);
|
|
221
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bunigniter",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Bun-native fullstack framework — CodeIgniter spirit × Elysia performance × Edge-ready",
|
|
5
|
+
"homepage": "https://github.com/nexus-ts/bunigniter",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist/",
|
|
9
|
+
"README.md",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
12
|
+
"main": "dist/index.ts",
|
|
13
|
+
"module": "dist/index.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": "./dist/index.ts",
|
|
16
|
+
"./base": "./dist/base/index.ts",
|
|
17
|
+
"./controller": "./dist/base/controller.ts",
|
|
18
|
+
"./helpers/env": "./dist/helpers/env.ts",
|
|
19
|
+
"./helpers/validator": "./dist/helpers/validator.ts",
|
|
20
|
+
"./helpers/http": "./dist/helpers/http.ts",
|
|
21
|
+
"./helpers/image": "./dist/helpers/image.ts",
|
|
22
|
+
"./helpers/pagination": "./dist/helpers/pagination.ts",
|
|
23
|
+
"./helpers/session": "./dist/helpers/session.ts",
|
|
24
|
+
"./helpers/cache": "./dist/helpers/cache.ts",
|
|
25
|
+
"./helpers/queue": "./dist/helpers/queue.ts",
|
|
26
|
+
"./helpers/upload": "./dist/helpers/upload.ts",
|
|
27
|
+
"./helpers/mail": "./dist/helpers/mail.ts",
|
|
28
|
+
"./helpers/modules": "./dist/helpers/modules.ts",
|
|
29
|
+
"./helpers/openapi": "./dist/helpers/openapi.ts",
|
|
30
|
+
"./helpers/jwt": "./dist/helpers/jwt.ts",
|
|
31
|
+
"./helpers/ws": "./dist/helpers/ws.ts",
|
|
32
|
+
"./helpers/sse": "./dist/helpers/sse.ts",
|
|
33
|
+
"./helpers/schedule": "./dist/helpers/schedule.ts"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build:dist": "bun run scripts/build-dist.ts",
|
|
37
|
+
"prepublishOnly": "bun run build:dist",
|
|
38
|
+
"postpublish": "rm -rf dist",
|
|
39
|
+
"dev": "bun --hot run src/index.ts",
|
|
40
|
+
"start": "bun run src/index.ts",
|
|
41
|
+
"build": "bun build ./src/index.ts --outdir ./dist --target bun",
|
|
42
|
+
"bi": "bun run src/cli/index.ts",
|
|
43
|
+
"make:controller": "bun run src/cli/index.ts make:controller",
|
|
44
|
+
"make:model": "bun run src/cli/index.ts make:model",
|
|
45
|
+
"test": "bun x vitest run",
|
|
46
|
+
"test:smoke": "bun x vitest run tests/smoke.test.ts",
|
|
47
|
+
"test:unit": "bun x vitest run tests/ --exclude 'tests/smoke*' ",
|
|
48
|
+
"typecheck": "tsc --noEmit"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"drizzle-orm": "^0.45.0",
|
|
52
|
+
"elysia": "2.0.0-exp.9",
|
|
53
|
+
"openapi-types": "^12.1.3",
|
|
54
|
+
"rendu": "^0.1.0",
|
|
55
|
+
"zod": "^4.4.3"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@types/bun": "^1.3.14",
|
|
59
|
+
"@types/react": "^19.2.17",
|
|
60
|
+
"@types/react-dom": "^19.2.3",
|
|
61
|
+
"react": "^19.2.7",
|
|
62
|
+
"react-dom": "^19.2.7",
|
|
63
|
+
"typescript": "^5.7.0",
|
|
64
|
+
"vitest": "^4.1.9"
|
|
65
|
+
},
|
|
66
|
+
"engines": {
|
|
67
|
+
"bun": ">=1.3.0"
|
|
68
|
+
},
|
|
69
|
+
"license": "MIT"
|
|
70
|
+
}
|