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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +229 -0
  3. package/dist/LICENSE +21 -0
  4. package/dist/README.md +229 -0
  5. package/dist/base/controller.ts +324 -0
  6. package/dist/base/index.ts +5 -0
  7. package/dist/base/service.ts +21 -0
  8. package/dist/cli/index.ts +318 -0
  9. package/dist/cli/list-routes.ts +72 -0
  10. package/dist/cli/repl.ts +461 -0
  11. package/dist/cli/templates.ts +283 -0
  12. package/dist/client/index.ts +159 -0
  13. package/dist/db/drizzle.ts +550 -0
  14. package/dist/db/validators.ts +229 -0
  15. package/dist/edge-builder.ts +120 -0
  16. package/dist/edge.ts +69 -0
  17. package/dist/helpers/cache.ts +173 -0
  18. package/dist/helpers/cors.ts +103 -0
  19. package/dist/helpers/csrf.ts +155 -0
  20. package/dist/helpers/debug.ts +158 -0
  21. package/dist/helpers/env.ts +147 -0
  22. package/dist/helpers/handler.ts +158 -0
  23. package/dist/helpers/http.ts +194 -0
  24. package/dist/helpers/image.ts +217 -0
  25. package/dist/helpers/jwt.ts +147 -0
  26. package/dist/helpers/logger.ts +96 -0
  27. package/dist/helpers/mail.ts +272 -0
  28. package/dist/helpers/middleware-loader.ts +116 -0
  29. package/dist/helpers/middleware.ts +57 -0
  30. package/dist/helpers/modules.ts +115 -0
  31. package/dist/helpers/openapi.ts +140 -0
  32. package/dist/helpers/pagination.ts +159 -0
  33. package/dist/helpers/queue.ts +186 -0
  34. package/dist/helpers/request-context.ts +13 -0
  35. package/dist/helpers/request.ts +376 -0
  36. package/dist/helpers/schedule.ts +173 -0
  37. package/dist/helpers/session-middleware.ts +89 -0
  38. package/dist/helpers/session.ts +286 -0
  39. package/dist/helpers/sse.ts +90 -0
  40. package/dist/helpers/throttle.ts +156 -0
  41. package/dist/helpers/upload.ts +417 -0
  42. package/dist/helpers/validator.ts +287 -0
  43. package/dist/helpers/ws.ts +123 -0
  44. package/dist/index.ts +221 -0
  45. package/dist/package.json +70 -0
  46. package/dist/router/file-router.ts +541 -0
  47. package/dist/router/server-router.ts +103 -0
  48. package/dist/view/page.ts +96 -0
  49. package/dist/view/renderer.tsx +390 -0
  50. package/dist/view/view-response.ts +10 -0
  51. 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
+ }