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,72 @@
1
+ /**
2
+ * Shared route listing — used by both `nx list` CLI and `.routes` REPL command.
3
+ */
4
+ import { readdirSync, statSync, readFileSync, existsSync } from 'node:fs'
5
+ import { join } from 'node:path'
6
+
7
+ const CWD = process.cwd()
8
+
9
+ export async function listRoutes(): Promise<void> {
10
+ const routesDir = join(CWD, 'routes')
11
+ if (!existsSync(routesDir)) {
12
+ console.log(' No routes directory found.')
13
+ return
14
+ }
15
+
16
+ const files = readdirSync(routesDir)
17
+ const prefix = process.env.ROUTER_PREFIX ?? '/api'
18
+
19
+ console.log('\n Bunigniter Routes')
20
+ console.log(' ─────────────────────────────────')
21
+
22
+ let count = 0
23
+ for (const file of files.sort()) {
24
+ if (!file.endsWith('.ts') || file.endsWith('.server.ts')) continue
25
+
26
+ const fullPath = join(routesDir, file)
27
+ if (!statSync(fullPath).isFile()) continue
28
+
29
+ const content = readFileSync(fullPath, 'utf-8')
30
+ const name = file.replace(/\.ts$/, '')
31
+ const isIndex = name === 'index'
32
+
33
+ const classMatch = content.match(/export (default )?class (\w+) extends Controller/)
34
+ const className = classMatch ? classMatch[2] : name
35
+
36
+ // Check for defineHandler pattern: export const GET = ...
37
+ const handlerMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']
38
+ let hasHandler = false
39
+
40
+ for (const verb of handlerMethods) {
41
+ if (content.includes(`export const ${verb} =`)) {
42
+ hasHandler = true
43
+ const path = isIndex ? prefix : `${prefix}/${name}`
44
+ console.log(` ${verb.padEnd(6)} ${path.padEnd(25)} ${className}.${verb}()`)
45
+ count++
46
+ }
47
+ }
48
+
49
+ if (hasHandler) continue
50
+
51
+ // Controller class methods
52
+ const methods = ['index', 'show', 'create', 'update', 'destroy']
53
+ const methodVerbs: Record<string, string> = {
54
+ index: 'GET', show: 'GET', create: 'POST',
55
+ update: 'PUT', destroy: 'DELETE',
56
+ }
57
+ const idMethods = new Set(['show', 'update', 'destroy'])
58
+
59
+ for (const method of methods) {
60
+ if (!content.includes(`async ${method}`)) continue
61
+ const verb = methodVerbs[method]
62
+ const path = idMethods.has(method)
63
+ ? (isIndex ? `${prefix}/:id` : `${prefix}/${name}/:id`)
64
+ : (isIndex ? prefix : `${prefix}/${name}`)
65
+ console.log(` ${verb.padEnd(6)} ${path.padEnd(25)} ${className}.${method}()`)
66
+ count++
67
+ }
68
+ }
69
+
70
+ if (count === 0) console.log(' (no routes found)')
71
+ console.log()
72
+ }
@@ -0,0 +1,461 @@
1
+ /**
2
+ * REPL — Interactive console for Bunigniter.
3
+ *
4
+ * Usage: `bun run bi repl`
5
+ *
6
+ * Provides access to db, cache, session, and app services.
7
+ *
8
+ * Commands:
9
+ * .help Show help
10
+ * .exit Exit REPL
11
+ * .routes List registered routes
12
+ * .services List available services
13
+ * .env Show environment variables
14
+ * .db Show database status
15
+ * .clear Clear screen
16
+ * .version Show version info
17
+ */
18
+ import { readFileSync, existsSync, appendFileSync } from "node:fs";
19
+ import { join } from "node:path";
20
+ import { createInterface, clearLine, cursorTo } from "node:readline";
21
+ import { inspect } from "node:util";
22
+
23
+ const HISTORY_FILE = join(process.cwd(), ".nexus_repl_history");
24
+ const HISTORY_MAX = 100;
25
+ const PING_SQL = "SELECT 1 as ok";
26
+
27
+ /** Build a pretty ASCII box banner. */
28
+ function banner(version: string, runtime: string): string {
29
+ const lines = [
30
+ "",
31
+ " ╔══════════════════════════════════════════════════════╗",
32
+ " ║ ║",
33
+ " ║ ◈ Bunigniter ◈ ║",
34
+ " ║ Interactive Development Console ║",
35
+ " ║ ║",
36
+ ` ║ version ${version.padEnd(25)} ║`,
37
+ ` ║ runtime ${runtime.padEnd(25)} ║`,
38
+ " ║ ║",
39
+ " ║ Type .help for available commands ║",
40
+ " ║ ║",
41
+ " ╚══════════════════════════════════════════════════════╝",
42
+ ];
43
+ return lines.join("\n");
44
+ }
45
+
46
+ /** Read version from package.json. */
47
+ function readVersion(): string {
48
+ try {
49
+ const pkgPath = join(process.cwd(), "package.json");
50
+ if (existsSync(pkgPath)) {
51
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
52
+ return pkg.version ?? "0.0.0";
53
+ }
54
+ } catch {
55
+ // ignore
56
+ }
57
+ return "0.0.0";
58
+ }
59
+
60
+ /**
61
+ * Start the REPL.
62
+ */
63
+ export async function startRepl() {
64
+ const version = readVersion();
65
+ const runtime = typeof Bun !== "undefined" ? `Bun ${Bun.version}` : "Node.js";
66
+
67
+ console.log(banner(version, runtime));
68
+ console.log("");
69
+
70
+ const context = await initializeContext(version, runtime);
71
+
72
+ const rl = createInterface({
73
+ input: process.stdin,
74
+ output: process.stdout,
75
+ prompt: "\x1b[36mnexus> \x1b[0m",
76
+ });
77
+
78
+ // Load history
79
+ const history: string[] = [];
80
+ try {
81
+ if (existsSync(HISTORY_FILE)) {
82
+ const lines = readFileSync(HISTORY_FILE, "utf-8")
83
+ .split("\n")
84
+ .filter(Boolean);
85
+ history.push(...lines.slice(-HISTORY_MAX));
86
+ }
87
+ } catch {
88
+ // ignore
89
+ }
90
+
91
+ let historyIndex = history.length;
92
+
93
+ rl.on("line", async (input: string) => {
94
+ const trimmed = input.trim();
95
+
96
+ if (!trimmed) {
97
+ rl.prompt();
98
+ return;
99
+ }
100
+
101
+ // Handle REPL commands
102
+ if (trimmed.startsWith(".")) {
103
+ await handleCommand(trimmed, context);
104
+ rl.prompt();
105
+ return;
106
+ }
107
+
108
+ // Save to history
109
+ history.push(trimmed);
110
+ historyIndex = history.length;
111
+ try {
112
+ appendFileSync(HISTORY_FILE, trimmed + "\n");
113
+ } catch {
114
+ // ignore
115
+ }
116
+
117
+ // Trim history
118
+ if (history.length > HISTORY_MAX) {
119
+ history.splice(0, history.length - HISTORY_MAX);
120
+ }
121
+
122
+ // Evaluate the expression
123
+ try {
124
+ const result = await evaluateExpression(trimmed, context);
125
+ if (result !== undefined) {
126
+ const output =
127
+ typeof result === "object" && result !== null
128
+ ? inspect(result, { depth: 3, colors: true, sorted: true })
129
+ : typeof result === "string"
130
+ ? `\x1b[33m"${result}"\x1b[0m`
131
+ : String(result);
132
+ console.log(` \x1b[90m=>\x1b[0m ${output}`);
133
+ }
134
+ } catch (err: any) {
135
+ console.error(` \x1b[31m✗ ${err.message ?? err}\x1b[0m`);
136
+ }
137
+
138
+ rl.prompt();
139
+ });
140
+
141
+ rl.on("SIGINT", () => {
142
+ console.log("\n \x1b[90mBye!\x1b[0m");
143
+ process.exit(0);
144
+ });
145
+
146
+ // Keypress handling for history navigation and tab completion
147
+ const stdin = process.stdin;
148
+ if (stdin.isTTY) {
149
+ stdin.on("data", (key: Buffer) => {
150
+ // Up arrow
151
+ if (key[0] === 0x1b && key[1] === 0x5b && key[2] === 0x41) {
152
+ if (historyIndex > 0) {
153
+ historyIndex--;
154
+ clearLine(process.stdout, 0);
155
+ cursorTo(process.stdout, 0);
156
+ rl.write(history[historyIndex] ?? "");
157
+ }
158
+ }
159
+ // Down arrow
160
+ if (key[0] === 0x1b && key[1] === 0x5b && key[2] === 0x42) {
161
+ if (historyIndex < history.length - 1) {
162
+ historyIndex++;
163
+ clearLine(process.stdout, 0);
164
+ cursorTo(process.stdout, 0);
165
+ rl.write(history[historyIndex] ?? "");
166
+ } else {
167
+ historyIndex = history.length;
168
+ clearLine(process.stdout, 0);
169
+ cursorTo(process.stdout, 0);
170
+ }
171
+ }
172
+ // Tab completion
173
+ if (key[0] === 0x09) {
174
+ const line = rl.line;
175
+ const matches = Object.keys(context).filter(
176
+ (k) => k.startsWith(line) && !k.startsWith("__"),
177
+ );
178
+ if (matches.length === 1) {
179
+ clearLine(process.stdout, 0);
180
+ cursorTo(process.stdout, 0);
181
+ rl.write(matches[0]);
182
+ } else if (matches.length > 1) {
183
+ console.log("\n " + matches.join(", "));
184
+ rl.prompt();
185
+ rl.write(line);
186
+ }
187
+ }
188
+ });
189
+ }
190
+
191
+ rl.prompt();
192
+ }
193
+
194
+ /** Initialize the REPL context with available services. */
195
+ async function initializeContext(
196
+ version: string,
197
+ runtime: string,
198
+ ): Promise<Record<string, any>> {
199
+ const ctx: Record<string, any> = {
200
+ process,
201
+ console,
202
+ Date,
203
+ Math,
204
+ JSON,
205
+ setTimeout,
206
+ setInterval,
207
+ clearTimeout,
208
+ clearInterval,
209
+ crypto,
210
+ Buffer,
211
+ URL,
212
+ parseInt,
213
+ parseFloat,
214
+ __version: version,
215
+ __runtime: runtime,
216
+ };
217
+
218
+ // Try to load Drizzle DB
219
+ try {
220
+ const { DbClient } = await import("../db/drizzle");
221
+ const dbConfig = loadDbConfig();
222
+ if (dbConfig) {
223
+ const db = new DbClient(dbConfig);
224
+ await db.open();
225
+ ctx.db = db;
226
+ ctx.query = (sql: string, params?: any[]) => db.query(sql, params);
227
+ ctx.first = (sql: string, params?: any[]) => db.first(sql, params);
228
+ console.log(" \x1b[32m◈\x1b[0m \x1b[90mdb\x1b[0m connected");
229
+ }
230
+ } catch {
231
+ console.log(" \x1b[33m◈\x1b[0m \x1b[90mdb\x1b[0m not loaded");
232
+ }
233
+
234
+ // Try to load Cache
235
+ try {
236
+ const { createCache } = await import("../helpers/cache");
237
+ ctx.cache = createCache();
238
+ console.log(" \x1b[32m◈\x1b[0m \x1b[90mcache\x1b[0m ready");
239
+ } catch {
240
+ console.log(" \x1b[33m◈\x1b[0m \x1b[90mcache\x1b[0m not available");
241
+ }
242
+
243
+ // Try to load HTTP client
244
+ try {
245
+ const { createHttp } = await import("../helpers/http");
246
+ ctx.http = createHttp();
247
+ console.log(" \x1b[32m◈\x1b[0m \x1b[90mhttp\x1b[0m ready");
248
+ } catch {
249
+ console.log(" \x1b[33m◈\x1b[0m \x1b[90mhttp\x1b[0m not available");
250
+ }
251
+
252
+ return ctx;
253
+ }
254
+
255
+ /** Try to load DB config from the app config. */
256
+ function loadDbConfig(): any {
257
+ try {
258
+ const configPath = join(process.cwd(), "config/app.ts");
259
+ if (existsSync(configPath)) {
260
+ const content = readFileSync(configPath, "utf-8");
261
+
262
+ const dialectMatch = content.match(/dialect:\s*'([^']+)'/);
263
+ const fileMatch = content.match(/filename:\s*'([^']+)'/);
264
+ if (dialectMatch) {
265
+ return {
266
+ dialect: dialectMatch[1],
267
+ connection: { filename: fileMatch?.[1] ?? "app.db" },
268
+ };
269
+ }
270
+ }
271
+ } catch {
272
+ // ignore
273
+ }
274
+ return null;
275
+ }
276
+
277
+ /** Describe a service entry for the .services command. */
278
+ function describeService(val: unknown): string {
279
+ const type = typeof val;
280
+ if (type === "function") {
281
+ return `\x1b[33mfn\x1b[0m ${(val as Function).name || "(anonymous)"}()`;
282
+ }
283
+ if (type === "object" && val !== null) {
284
+ return `\x1b[35mobj\x1b[0m ${(val as object).constructor?.name || "Object"}`;
285
+ }
286
+ return `\x1b[34m${type}\x1b[0m`;
287
+ }
288
+
289
+ /** Truncate an env value for display. */
290
+ function truncateEnvVal(val: string): string {
291
+ return val.length > 60 ? val.slice(0, 60) + "\x1b[90m...\x1b[0m" : val;
292
+ }
293
+
294
+ /** Handle REPL commands. */
295
+ async function handleCommand(
296
+ cmd: string,
297
+ ctx: Record<string, any>,
298
+ ): Promise<void> {
299
+ const args = cmd.split(/\s+/);
300
+ const command = args[0].toLowerCase();
301
+
302
+ switch (command) {
303
+ case ".help": {
304
+ console.log(`
305
+ \x1b[36m── Commands ──────────────────────────────────────\x1b[0m
306
+ \x1b[33m.help\x1b[0m Show this help
307
+ \x1b[33m.exit\x1b[0m Exit the REPL
308
+ \x1b[33m.quit\x1b[0m Exit the REPL
309
+ \x1b[33m.routes\x1b[0m List registered routes
310
+ \x1b[33m.services\x1b[0m List available services in context
311
+ \x1b[33m.env\x1b[0m Show environment variables
312
+ \x1b[33m.db\x1b[0m Show database status
313
+ \x1b[33m.clear\x1b[0m Clear screen
314
+ \x1b[33m.version\x1b[0m Show version info
315
+
316
+ \x1b[36m── Available Variables ────────────────────────────\x1b[0m
317
+ \x1b[33mdb\x1b[0m Database client (query, first, all)
318
+ \x1b[33mcache\x1b[0m Cache service (get, set, delete)
319
+ \x1b[33mhttp\x1b[0m HTTP client (get, post)
320
+ \x1b[33mquery()\x1b[0m Shortcut for db.query()
321
+ \x1b[33mfirst()\x1b[0m Shortcut for db.first()
322
+
323
+ \x1b[36m── Examples ──────────────────────────────────────\x1b[0m
324
+ \x1b[90mnexus>\x1b[0m await query('SELECT * FROM users')
325
+ \x1b[90mnexus>\x1b[0m await cache.get('my_key')
326
+ \x1b[90mnexus>\x1b[0m 1 + 2
327
+ \x1b[90mnexus>\x1b[0m const x = 42; x * 2
328
+ \x1b[90mnexus>\x1b[0m { hello: 'world' }
329
+ `);
330
+ break;
331
+ }
332
+
333
+ case ".exit":
334
+ case ".quit": {
335
+ console.log(" \x1b[90mBye!\x1b[0m");
336
+ process.exit(0);
337
+ break;
338
+ }
339
+
340
+ case ".routes":
341
+ case ".list": {
342
+ const { listRoutes } = await import("./list-routes");
343
+ await listRoutes();
344
+ break;
345
+ }
346
+
347
+ case ".services": {
348
+ console.log(`\n \x1b[36mAvailable services:\x1b[0m\n`);
349
+ for (const [key, val] of Object.entries(ctx)) {
350
+ if (key.startsWith("__")) continue;
351
+ console.log(` \x1b[32m${key}\x1b[0m ${describeService(val)}`);
352
+ }
353
+ console.log();
354
+ break;
355
+ }
356
+
357
+ case ".env": {
358
+ const envVars = Object.keys(process.env).sort();
359
+ console.log(
360
+ `\n \x1b[36mEnvironment (\x1b[33m${envVars.length}\x1b[36m vars):\x1b[0m\n`,
361
+ );
362
+ for (const key of envVars.slice(0, 30)) {
363
+ console.log(
364
+ ` \x1b[32m${key.padEnd(25)}\x1b[0m ${truncateEnvVal(process.env[key] ?? "")}`,
365
+ );
366
+ }
367
+ if (envVars.length > 30) {
368
+ console.log(` \x1b[90m... and ${envVars.length - 30} more\x1b[0m`);
369
+ }
370
+ console.log();
371
+ break;
372
+ }
373
+
374
+ case ".db": {
375
+ if (ctx.db) {
376
+ try {
377
+ await ctx.db.query(PING_SQL);
378
+ console.log(
379
+ `\n \x1b[32m◈\x1b[0m Database: \x1b[32mconnected\x1b[0m`,
380
+ );
381
+ console.log(
382
+ ` \x1b[32m◈\x1b[0m Dialect: ${ctx.db.dialectName ?? "unknown"}\n`,
383
+ );
384
+ } catch (e: any) {
385
+ console.log(
386
+ `\n \x1b[31m◈\x1b[0m Database: \x1b[31merror\x1b[0m — ${e.message}\n`,
387
+ );
388
+ }
389
+ } else {
390
+ console.log("\n \x1b[33m◈\x1b[0m No database connection.\n");
391
+ }
392
+ break;
393
+ }
394
+
395
+ case ".clear":
396
+ case "clear": {
397
+ console.clear();
398
+ console.log(banner(ctx.__version, ctx.__runtime));
399
+ console.log("");
400
+ break;
401
+ }
402
+
403
+ case ".version": {
404
+ console.log(`
405
+ \x1b[36mBunigniter\x1b[0m \x1b[33mv${ctx.__version}\x1b[0m
406
+ \x1b[36mRuntime\x1b[0m ${ctx.__runtime}
407
+ `);
408
+ break;
409
+ }
410
+
411
+ default: {
412
+ console.log(
413
+ ` \x1b[31mUnknown command:\x1b[0m ${command}. Type \x1b[33m.help\x1b[0m for available commands.`,
414
+ );
415
+ }
416
+ }
417
+ }
418
+
419
+ /** Evaluate a JavaScript expression in context. */
420
+ async function evaluateExpression(
421
+ input: string,
422
+ ctx: Record<string, any>,
423
+ ): Promise<any> {
424
+ const hasAwait = input.includes("await ");
425
+ const keys = Object.keys(ctx);
426
+ const values = Object.values(ctx);
427
+
428
+ // Try as expression first (fast path)
429
+ try {
430
+ const fn = new Function(...keys, `"use strict"; return (${input})`);
431
+ return fn(...values);
432
+ } catch {
433
+ // try fallthrough
434
+ }
435
+
436
+ // Try as async expression (handles top-level await)
437
+ try {
438
+ const fn = new Function(
439
+ ...keys,
440
+ `"use strict"; return (async () => { ${hasAwait ? "" : "return "}${input} })()`,
441
+ );
442
+ return fn(...values);
443
+ } catch {
444
+ // try fallthrough
445
+ }
446
+
447
+ // Try as statement
448
+ try {
449
+ const fn = new Function(...keys, `"use strict"; ${input}`);
450
+ return fn(...values);
451
+ } catch {
452
+ // try fallthrough
453
+ }
454
+
455
+ // Last resort — async statement
456
+ const fn = new Function(
457
+ ...keys,
458
+ `"use strict"; return (async () => { ${input} })()`,
459
+ );
460
+ return fn(...values);
461
+ }