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,96 @@
1
+ /**
2
+ * Page — Inertia-style page response for server-rendered views.
3
+ *
4
+ * Combines a component name with props and renders either:
5
+ * - Full HTML (first request, SEO-friendly)
6
+ * - JSON page object (subsequent Inertia navigation)
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * // pages/users.ts
11
+ * export class Users extends Controller {
12
+ * async index() {
13
+ * const users = await this.db.query('SELECT * FROM users')
14
+ * return this.page('Users/Index', { users })
15
+ * }
16
+ * }
17
+ * ```
18
+ */
19
+ export interface PageOptions {
20
+ /** HTTP status code. Default: 200 */
21
+ status?: number
22
+
23
+ /** Page title (injected into HTML shell). */
24
+ title?: string
25
+
26
+ /** Shared props merged with component props. */
27
+ shared?: Record<string, any>
28
+
29
+ /** Layout to wrap the page. */
30
+ layout?: string | false
31
+
32
+ /** Flash data (shown once then cleared). */
33
+ flash?: Record<string, any>
34
+
35
+ /** Asset version for cache busting. */
36
+ version?: string
37
+ }
38
+
39
+ /**
40
+ * Page response — returned from a controller to render a full page.
41
+ */
42
+ export class PageResponse {
43
+ public readonly component: string
44
+ public readonly props: Record<string, any>
45
+ public readonly options: PageOptions
46
+
47
+ constructor(component: string, props: Record<string, any> = {}, options: PageOptions = {}) {
48
+ this.component = component
49
+ this.props = props
50
+ this.options = options
51
+ }
52
+
53
+ /** Get the page object as JSON (for Inertia protocol). */
54
+ toInertiaJson(sharedProps: Record<string, any> = {}): string {
55
+ return JSON.stringify({
56
+ component: this.component,
57
+ props: { ...sharedProps, ...this.props },
58
+ url: '', // set by the router
59
+ version: this.options.version ?? null,
60
+ flash: this.options.flash ?? null,
61
+ })
62
+ }
63
+
64
+ /** Render full HTML shell with embedded page data. */
65
+ toHtml(sharedProps: Record<string, any> = {}, url = '/'): string {
66
+ const pageJson = JSON.stringify({
67
+ component: this.component,
68
+ props: { ...sharedProps, ...this.props },
69
+ url,
70
+ version: this.options.version ?? null,
71
+ flash: this.options.flash ?? null,
72
+ })
73
+
74
+ const title = this.options.title ?? this.component
75
+ const escapedPage = pageJson
76
+ .replace(/&/g, '&amp;')
77
+ .replace(/'/g, '&#39;')
78
+ .replace(/"/g, '&quot;')
79
+
80
+ return `<!DOCTYPE html>
81
+ <html lang="en">
82
+ <head>
83
+ <meta charset="UTF-8">
84
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
85
+ <title>${escapeHtml(title)}</title>
86
+ </head>
87
+ <body>
88
+ <div id="app" data-page='${escapedPage}'></div>
89
+ </body>
90
+ </html>`
91
+ }
92
+ }
93
+
94
+ function escapeHtml(s: string): string {
95
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
96
+ }
@@ -0,0 +1,390 @@
1
+ /**
2
+ * View Renderer — Simple server-side React rendering for Bunigniter.
3
+ *
4
+ * Controllers return `this.view('ComponentName', { props })` and the
5
+ * framework SSR-renders the React component to HTML.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * // views/TodoList.tsx (React component)
10
+ * export default function TodoList({ todos, stats }: any) {
11
+ * return (
12
+ * <div>
13
+ * <h1>📋 Todo App</h1>
14
+ * <ul>{todos.map((t: any) => <li key={t.id}>{t.title}</li>)}</ul>
15
+ * </div>
16
+ * )
17
+ * }
18
+ * ```
19
+ *
20
+ * ```ts
21
+ * // pages/todos.ts (Controller)
22
+ * async index() {
23
+ * const todos = await this.db.query('SELECT * FROM todos')
24
+ * return this.view('TodoList', { todos: todos.rows })
25
+ * }
26
+ * ```
27
+ */
28
+ import { existsSync, readFileSync } from 'node:fs'
29
+ import { join, extname } from 'node:path'
30
+ import React from 'react'
31
+ import { renderToString } from 'react-dom/server'
32
+
33
+ /** Registry of view components. */
34
+ const registry = new Map<string, any>()
35
+
36
+ /** MDX modules cache. */
37
+ const mdxCache = new Map<string, any>()
38
+
39
+ /** Views directory. */
40
+ let viewsDir = 'views'
41
+
42
+ /**
43
+ * Set the views directory.
44
+ * Called automatically from config.
45
+ */
46
+ export function setViewsDir(dir: string): void {
47
+ viewsDir = dir
48
+ }
49
+
50
+ /**
51
+ * Register a view component.
52
+ * Called automatically when the view file is first loaded.
53
+ */
54
+ export function registerView(name: string, component: any): void {
55
+ registry.set(name, component)
56
+ }
57
+
58
+ /**
59
+ * Render a view component to an HTML string.
60
+ *
61
+ * @param name - Component name (e.g. 'TodoList')
62
+ * @param props - Props object passed to the component
63
+ * @param options - Rendering options
64
+ * @returns HTML string
65
+ */
66
+ export async function renderView(
67
+ name: string,
68
+ props: Record<string, any> = {},
69
+ options: { title?: string; scripts?: string[] } = {},
70
+ viewBase?: string
71
+ ): Promise<string | Response> {
72
+ // Try to load the component from views/ directory
73
+ let Component = registry.get(name)
74
+
75
+ if (!Component) {
76
+ const base = viewBase ?? join(process.cwd(), viewsDir)
77
+ const candidates = [
78
+ join(base, `${name}.tsx`),
79
+ join(base, `${name}.mdx`),
80
+ join(base, `${name}.md`),
81
+ join(base, `${name}.html`),
82
+ join(base, name, 'index.tsx'),
83
+ ]
84
+
85
+ let targetPath: string | null = null
86
+ for (const p of candidates) {
87
+ if (existsSync(p)) { targetPath = p; break }
88
+ }
89
+
90
+ if (targetPath) {
91
+ const ext = extname(targetPath)
92
+ if (ext === '.html') {
93
+ // HTML templates use Rendu engine (PHP-like <?= ?> syntax)
94
+ return renderHTML(targetPath, props)
95
+ } else if (ext === '.mdx' || ext === '.md') {
96
+ // MDX with Rendu support (<?= ?>, {{ }}) + markdown rendering
97
+ return renderMDXView(targetPath, props)
98
+ } else {
99
+ const mod = await import(targetPath)
100
+ Component = mod.default ?? mod[name]
101
+ }
102
+ if (Component) {
103
+ registry.set(name, Component)
104
+ }
105
+ }
106
+ }
107
+
108
+ if (!Component) {
109
+ return renderFallback(name, props, options)
110
+ }
111
+
112
+ try {
113
+ const html = renderToString(React.createElement(Component, props))
114
+ const title = options.title ?? name
115
+ const serialized = JSON.stringify({ component: name, props })
116
+ const scripts = (options.scripts ?? []).map(s => `<script src="${s}"></script>`).join('\n')
117
+ return `<!DOCTYPE html>
118
+ <html lang="en">
119
+ <head>
120
+ <meta charset="UTF-8">
121
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
122
+ <title>${escapeHtml(title)}</title>
123
+ </head>
124
+ <body>
125
+ <div id="app">${html}</div>
126
+ <script id="__NEXUS_DATA__" type="application/json">${escapeHtml(serialized)}</script>
127
+ ${scripts}
128
+ </body>
129
+ </html>`
130
+ } catch (err: any) {
131
+ return renderError(err, name, props)
132
+ }
133
+ }
134
+
135
+ /** Fallback: render data-page shell (same as before). */
136
+ function renderFallback(name: string, props: Record<string, any>, options: { title?: string; scripts?: string[] }): string {
137
+ const title = options.title ?? name
138
+ const serialized = JSON.stringify({ component: name, props })
139
+ const scripts = (options.scripts ?? []).map(s => `<script src="${s}"></script>`).join('\n')
140
+
141
+ return `<!DOCTYPE html>
142
+ <html lang="en">
143
+ <head>
144
+ <meta charset="UTF-8">
145
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
146
+ <title>${escapeHtml(title)}</title>
147
+ </head>
148
+ <body>
149
+ <div id="app" data-page='${escapeHtml(serialized)}'></div>
150
+ ${scripts}
151
+ </body>
152
+ </html>`
153
+ }
154
+
155
+ /**
156
+ * Compile a plain HTML file using Rendu (PHP-like template engine).
157
+ * Supports <?= expr ?>, <?js code ?>, {{ expr }}, {{{ expr }}} syntax.
158
+ */
159
+ function compileHTML(filePath: string, props: Record<string, any>): any {
160
+ const source = readFileSync(filePath, 'utf-8')
161
+
162
+ // Build a component that renders the template at request time
163
+ const HtmlComponent = () => {
164
+ // Use React state/effect to render on mount, but for SSR we need sync
165
+ // Actually, return a placeholder that gets stream-rendered
166
+ throw new Error('Use renderHTML() instead of React for .html templates')
167
+ }
168
+
169
+ return HtmlComponent
170
+ }
171
+
172
+ /**
173
+ * Render an HTML template using Rendu engine.
174
+ * Handles PHP-style <?= ?>, control flow <?js ?>, and {{ }} mustache syntax.
175
+ * Returns a Promise<Response> with rendered HTML.
176
+ */
177
+ export async function renderHTML(filePath: string, props: Record<string, any> = {}): Promise<Response> {
178
+ const source = readFileSync(filePath, 'utf-8')
179
+
180
+ // Compile template using Rendu
181
+ const { compileTemplate } = await import('rendu')
182
+
183
+ /** Include a sub-template (partial) file. */
184
+ const includeSub = async (name: string): Promise<string> => {
185
+ const dir = filePath.substring(0, filePath.lastIndexOf('/'))
186
+ let subPath = join(dir, name)
187
+ if (!existsSync(subPath)) {
188
+ const ext = name.endsWith('.html') ? '' : '.html'
189
+ subPath = join(dir, name + ext)
190
+ }
191
+ if (!existsSync(subPath)) return `<!-- missing partial: ${name} -->`
192
+
193
+ const subSource = readFileSync(subPath, 'utf-8')
194
+ const subFn = compileTemplate(subSource)
195
+ const subStream = await subFn(ctx)
196
+ const reader = subStream.getReader()
197
+ let result = ''
198
+ while (true) {
199
+ const { done, value } = await reader.read()
200
+ if (done) break
201
+ result += new TextDecoder().decode(value)
202
+ }
203
+ return result
204
+ }
205
+
206
+ const fn = compileTemplate(source)
207
+ const ctx = {
208
+ htmlspecialchars: (s: unknown) =>
209
+ String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'),
210
+ include: async (name: string) => await includeSub(name),
211
+ ...props,
212
+ }
213
+
214
+ // Execute with props + rendu context
215
+ const stream = await fn(ctx)
216
+
217
+ // Collect stream into string
218
+ const reader = stream.getReader()
219
+ const chunks: Uint8Array[] = []
220
+ while (true) {
221
+ const { done, value } = await reader.read()
222
+ if (done) break
223
+ chunks.push(value)
224
+ }
225
+
226
+ let html = new TextDecoder().decode(concatUint8Arrays(chunks))
227
+
228
+ // Auto-layout: wrap with _layout.html from same directory
229
+ const layoutPath = join(filePath.substring(0, filePath.lastIndexOf('/')), '_layout.html')
230
+ if (existsSync(layoutPath)) {
231
+ const layoutSource = readFileSync(layoutPath, 'utf-8')
232
+ const layoutFn = compileTemplate(layoutSource)
233
+ const layoutStream = await layoutFn({
234
+ htmlspecialchars: (s: unknown) => String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'),
235
+ slot: html,
236
+ ...props,
237
+ })
238
+ const layoutReader = layoutStream.getReader()
239
+ const layoutChunks: Uint8Array[] = []
240
+ while (true) {
241
+ const { done, value } = await layoutReader.read()
242
+ if (done) break
243
+ layoutChunks.push(value)
244
+ }
245
+ html = new TextDecoder().decode(concatUint8Arrays(layoutChunks))
246
+ }
247
+
248
+ return new Response(html, {
249
+ headers: { 'content-type': 'text/html; charset=utf-8' }
250
+ })
251
+ }
252
+
253
+ function concatUint8Arrays(arrays: Uint8Array[]): Uint8Array {
254
+ const total = arrays.reduce((s, a) => s + a.length, 0)
255
+ const result = new Uint8Array(total)
256
+ let offset = 0
257
+ for (const a of arrays) {
258
+ result.set(a, offset)
259
+ offset += a.length
260
+ }
261
+ return result
262
+ }
263
+
264
+ /**
265
+ * Render an MDX view file to HTML using Rendu for interpolation
266
+ * and Bun.markdown.react() for markdown rendering.
267
+ *
268
+ * Supports:
269
+ * <?= expr ?> — PHP-style output (Rendu)
270
+ * {{ expr }} — Jinja-style escaped output (Rendu)
271
+ * {{{ expr }}} — Jinja-style raw output (Rendu)
272
+ * <?js code ?> — JavaScript control flow (Rendu)
273
+ *
274
+ * Then standard markdown via Bun.markdown.react().
275
+ */
276
+ export async function renderMDXView(filePath: string, props: Record<string, any> = {}): Promise<Response> {
277
+ const source = readFileSync(filePath, 'utf-8')
278
+ let content = source.replace(/^---[\s\S]*?---\n?/, '')
279
+
280
+ // Step 1: Pre-render partials (_*.html) and replace include() calls
281
+ // with their rendered HTML (added AFTER markdown rendering to avoid conflicts)
282
+ const renderedPartials: string[] = []
283
+ let processed = content.replace(
284
+ /<\?=\s*await\s+include\s*\(\s*['"]([^'"]+)['"]\s*\)\s*\?>/g,
285
+ (_match: string, partialName: string) => {
286
+ const placeholder = `<!--INCLUDE_PLACEHOLDER_${renderedPartials.length}-->`
287
+ renderedPartials.push(partialName)
288
+ return placeholder
289
+ }
290
+ )
291
+ .replace(/`<\?=(.+?)`/g, '`&lt;?=$1`')
292
+ .replace(/`<\?js(.+?)\?>`/g, '`&lt;?js$1?&gt;`')
293
+
294
+ // Step 2: Render through Rendu for <?= ?> and {{ }} interpolation
295
+ const { compileTemplate } = await import('rendu')
296
+ const fn = compileTemplate(processed)
297
+ const ctx = {
298
+ htmlspecialchars: (s: unknown) => String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'),
299
+ ...props,
300
+ }
301
+ const stream = await fn(ctx)
302
+ const reader = stream.getReader()
303
+ let interpolated = ''
304
+ while (true) {
305
+ const { done, value } = await reader.read()
306
+ if (done) break
307
+ interpolated += new TextDecoder().decode(value)
308
+ }
309
+
310
+ // Step 3: Split at placeholders, render markdown parts, insert rendered partials
311
+ const parts = interpolated.split(/<!--INCLUDE_PLACEHOLDER_\d+-->/)
312
+ let finalHtml = ''
313
+
314
+ const mdOverrides = {
315
+ h1: function(p: any) { return React.createElement('h1', { id: p.id, style: { color: '#e94560' } }, p.children) },
316
+ a: function(p: any) { return React.createElement('a', { href: p.href, style: { color: '#70a1ff' } }, p.children) },
317
+ pre: function(p: any) { return React.createElement('pre', { style: { background: '#1a1a3e', padding: 16, borderRadius: 8, overflow: 'auto' } }, p.children) },
318
+ code: function(p: any) { return React.createElement('code', { style: { background: '#333', padding: '2px 6px', borderRadius: 3, fontSize: '0.9em' } }, p.children) },
319
+ }
320
+ const mdOptions = { tables: true, strikethrough: true, tasklists: true, autolinks: true, headings: { ids: true } }
321
+
322
+ for (let i = 0; i < parts.length; i++) {
323
+ const mdPart = parts[i].trim()
324
+ if (mdPart) {
325
+ const elements = Bun.markdown.react(mdPart, mdOverrides, mdOptions)
326
+ finalHtml += renderToString(elements)
327
+ }
328
+
329
+ // Insert rendered partial after each markdown part (except the last)
330
+ if (i < renderedPartials.length) {
331
+ const partialName = renderedPartials[i]
332
+ const dir = filePath.substring(0, filePath.lastIndexOf('/'))
333
+ let subPath = join(dir, partialName)
334
+ if (!existsSync(subPath)) subPath = join(dir, partialName + (partialName.endsWith('.html') ? '' : '.html'))
335
+
336
+ if (existsSync(subPath)) {
337
+ const subSource = readFileSync(subPath, 'utf-8')
338
+ const subFn = compileTemplate(subSource)
339
+ const subStream = await subFn(ctx)
340
+ const subReader = subStream.getReader()
341
+ let subHtml = ''
342
+ while (true) {
343
+ const { done, value } = await subReader.read()
344
+ if (done) break
345
+ subHtml += new TextDecoder().decode(value)
346
+ }
347
+ finalHtml += subHtml
348
+ }
349
+ }
350
+ }
351
+
352
+ // Auto-layout: wrap with _layout.html from same directory
353
+ const mdxLayoutPath = join(filePath.substring(0, filePath.lastIndexOf('/')), '_layout.html')
354
+ if (existsSync(mdxLayoutPath)) {
355
+ const layoutSource = readFileSync(mdxLayoutPath, 'utf-8')
356
+ const layoutFn = compileTemplate(layoutSource)
357
+ const layoutStream = await layoutFn({
358
+ htmlspecialchars: (s: unknown) => String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'),
359
+ slot: finalHtml,
360
+ ...props,
361
+ })
362
+ const lr = layoutStream.getReader()
363
+ const lc: Uint8Array[] = []
364
+ while (true) { const { done, value } = await lr.read(); if (done) break; lc.push(value) }
365
+ finalHtml = new TextDecoder().decode(concatUint8Arrays(lc))
366
+ }
367
+
368
+ // Wrap in full HTML document
369
+ return new Response(finalHtml, {
370
+ headers: { 'content-type': 'text/html; charset=utf-8' }
371
+ })
372
+ }
373
+
374
+ /** Error page. */
375
+ function renderError(err: Error, name: string, props: Record<string, any>): string {
376
+ return `<!DOCTYPE html>
377
+ <html lang="en">
378
+ <head><meta charset="UTF-8"><title>View Error</title></head>
379
+ <body>
380
+ <h1>Failed to render view: ${escapeHtml(name)}</h1>
381
+ <pre style="color:red">${escapeHtml(err.message)}</pre>
382
+ <pre>${escapeHtml(JSON.stringify(props, null, 2))}</pre>
383
+ </body>
384
+ </html>`
385
+ }
386
+
387
+ function escapeHtml(s: string): string {
388
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
389
+ .replace(/"/g, '&quot;').replace(/'/g, '&#39;')
390
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * ViewResponse — returned by Controller.view() for SSR-rendered views.
3
+ */
4
+ export class ViewResponse {
5
+ constructor(
6
+ public readonly name: string,
7
+ public readonly props: Record<string, any> = {},
8
+ public readonly options: { title?: string; scripts?: string[] } = {}
9
+ ) {}
10
+ }
package/package.json ADDED
@@ -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
+ }