@basicbenframework/core 0.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.
Files changed (186) hide show
  1. package/.github/workflows/publish.yml +35 -0
  2. package/README.md +588 -0
  3. package/bin/cli.js +8 -0
  4. package/create-basicben-app/index.js +205 -0
  5. package/create-basicben-app/package.json +30 -0
  6. package/create-basicben-app/template/.env.example +24 -0
  7. package/create-basicben-app/template/README.md +59 -0
  8. package/create-basicben-app/template/basicben.config.js +33 -0
  9. package/create-basicben-app/template/index.html +54 -0
  10. package/create-basicben-app/template/migrations/001_create_users.js +15 -0
  11. package/create-basicben-app/template/migrations/002_create_posts.js +18 -0
  12. package/create-basicben-app/template/public/.gitkeep +0 -0
  13. package/create-basicben-app/template/seeds/01_users.js +29 -0
  14. package/create-basicben-app/template/seeds/02_posts.js +43 -0
  15. package/create-basicben-app/template/src/client/components/Alert.jsx +11 -0
  16. package/create-basicben-app/template/src/client/components/Avatar.jsx +11 -0
  17. package/create-basicben-app/template/src/client/components/BackLink.jsx +10 -0
  18. package/create-basicben-app/template/src/client/components/Button.jsx +19 -0
  19. package/create-basicben-app/template/src/client/components/Card.jsx +10 -0
  20. package/create-basicben-app/template/src/client/components/Empty.jsx +6 -0
  21. package/create-basicben-app/template/src/client/components/Input.jsx +12 -0
  22. package/create-basicben-app/template/src/client/components/Loading.jsx +6 -0
  23. package/create-basicben-app/template/src/client/components/Logo.jsx +40 -0
  24. package/create-basicben-app/template/src/client/components/Nav/DarkModeToggle.jsx +23 -0
  25. package/create-basicben-app/template/src/client/components/Nav/DesktopNav.jsx +32 -0
  26. package/create-basicben-app/template/src/client/components/Nav/MobileNav.jsx +107 -0
  27. package/create-basicben-app/template/src/client/components/NavLink.jsx +10 -0
  28. package/create-basicben-app/template/src/client/components/PageHeader.jsx +8 -0
  29. package/create-basicben-app/template/src/client/components/PostCard.jsx +19 -0
  30. package/create-basicben-app/template/src/client/components/Textarea.jsx +12 -0
  31. package/create-basicben-app/template/src/client/components/ThemeContext.jsx +5 -0
  32. package/create-basicben-app/template/src/client/contexts/ToastContext.jsx +94 -0
  33. package/create-basicben-app/template/src/client/layouts/AppLayout.jsx +60 -0
  34. package/create-basicben-app/template/src/client/layouts/AuthLayout.jsx +33 -0
  35. package/create-basicben-app/template/src/client/layouts/DocsLayout.jsx +60 -0
  36. package/create-basicben-app/template/src/client/layouts/RootLayout.jsx +25 -0
  37. package/create-basicben-app/template/src/client/pages/Auth.jsx +55 -0
  38. package/create-basicben-app/template/src/client/pages/Authentication.jsx +236 -0
  39. package/create-basicben-app/template/src/client/pages/Database.jsx +426 -0
  40. package/create-basicben-app/template/src/client/pages/Feed.jsx +34 -0
  41. package/create-basicben-app/template/src/client/pages/FeedPost.jsx +37 -0
  42. package/create-basicben-app/template/src/client/pages/GettingStarted.jsx +136 -0
  43. package/create-basicben-app/template/src/client/pages/Home.jsx +206 -0
  44. package/create-basicben-app/template/src/client/pages/PostForm.jsx +69 -0
  45. package/create-basicben-app/template/src/client/pages/Posts.jsx +59 -0
  46. package/create-basicben-app/template/src/client/pages/Profile.jsx +68 -0
  47. package/create-basicben-app/template/src/client/pages/Routing.jsx +207 -0
  48. package/create-basicben-app/template/src/client/pages/Testing.jsx +251 -0
  49. package/create-basicben-app/template/src/client/pages/Validation.jsx +210 -0
  50. package/create-basicben-app/template/src/controllers/AuthController.js +81 -0
  51. package/create-basicben-app/template/src/controllers/HomeController.js +17 -0
  52. package/create-basicben-app/template/src/controllers/PostController.js +86 -0
  53. package/create-basicben-app/template/src/controllers/ProfileController.js +66 -0
  54. package/create-basicben-app/template/src/helpers/api.js +24 -0
  55. package/create-basicben-app/template/src/main.jsx +9 -0
  56. package/create-basicben-app/template/src/middleware/auth.js +16 -0
  57. package/create-basicben-app/template/src/models/Post.js +63 -0
  58. package/create-basicben-app/template/src/models/User.js +42 -0
  59. package/create-basicben-app/template/src/routes/App.jsx +38 -0
  60. package/create-basicben-app/template/src/routes/api/auth.js +7 -0
  61. package/create-basicben-app/template/src/routes/api/posts.js +15 -0
  62. package/create-basicben-app/template/src/routes/api/profile.js +8 -0
  63. package/create-basicben-app/template/src/server/index.js +16 -0
  64. package/create-basicben-app/template/vite.config.js +18 -0
  65. package/database.sqlite +0 -0
  66. package/my-test-app/.env.example +24 -0
  67. package/my-test-app/README.md +59 -0
  68. package/my-test-app/basicben.config.js +33 -0
  69. package/my-test-app/database.sqlite-shm +0 -0
  70. package/my-test-app/database.sqlite-wal +0 -0
  71. package/my-test-app/index.html +54 -0
  72. package/my-test-app/migrations/001_create_users.js +15 -0
  73. package/my-test-app/migrations/002_create_posts.js +18 -0
  74. package/my-test-app/package-lock.json +2160 -0
  75. package/my-test-app/package.json +29 -0
  76. package/my-test-app/public/.gitkeep +0 -0
  77. package/my-test-app/seeds/01_users.js +29 -0
  78. package/my-test-app/seeds/02_posts.js +43 -0
  79. package/my-test-app/src/client/components/Alert.jsx +11 -0
  80. package/my-test-app/src/client/components/Avatar.jsx +11 -0
  81. package/my-test-app/src/client/components/BackLink.jsx +10 -0
  82. package/my-test-app/src/client/components/Button.jsx +19 -0
  83. package/my-test-app/src/client/components/Card.jsx +10 -0
  84. package/my-test-app/src/client/components/Empty.jsx +6 -0
  85. package/my-test-app/src/client/components/Input.jsx +12 -0
  86. package/my-test-app/src/client/components/Loading.jsx +6 -0
  87. package/my-test-app/src/client/components/Logo.jsx +40 -0
  88. package/my-test-app/src/client/components/Nav/DarkModeToggle.jsx +23 -0
  89. package/my-test-app/src/client/components/Nav/DesktopNav.jsx +32 -0
  90. package/my-test-app/src/client/components/Nav/MobileNav.jsx +107 -0
  91. package/my-test-app/src/client/components/NavLink.jsx +10 -0
  92. package/my-test-app/src/client/components/PageHeader.jsx +8 -0
  93. package/my-test-app/src/client/components/PostCard.jsx +19 -0
  94. package/my-test-app/src/client/components/Textarea.jsx +12 -0
  95. package/my-test-app/src/client/components/ThemeContext.jsx +5 -0
  96. package/my-test-app/src/client/contexts/AppContext.jsx +13 -0
  97. package/my-test-app/src/client/contexts/ToastContext.jsx +94 -0
  98. package/my-test-app/src/client/layouts/AppLayout.jsx +60 -0
  99. package/my-test-app/src/client/layouts/AuthLayout.jsx +33 -0
  100. package/my-test-app/src/client/layouts/DocsLayout.jsx +60 -0
  101. package/my-test-app/src/client/layouts/RootLayout.jsx +25 -0
  102. package/my-test-app/src/client/pages/Auth.jsx +55 -0
  103. package/my-test-app/src/client/pages/Authentication.jsx +236 -0
  104. package/my-test-app/src/client/pages/Database.jsx +426 -0
  105. package/my-test-app/src/client/pages/Feed.jsx +34 -0
  106. package/my-test-app/src/client/pages/FeedPost.jsx +37 -0
  107. package/my-test-app/src/client/pages/GettingStarted.jsx +136 -0
  108. package/my-test-app/src/client/pages/Home.jsx +206 -0
  109. package/my-test-app/src/client/pages/PostForm.jsx +69 -0
  110. package/my-test-app/src/client/pages/Posts.jsx +59 -0
  111. package/my-test-app/src/client/pages/Profile.jsx +68 -0
  112. package/my-test-app/src/client/pages/Routing.jsx +207 -0
  113. package/my-test-app/src/client/pages/Testing.jsx +251 -0
  114. package/my-test-app/src/client/pages/Validation.jsx +210 -0
  115. package/my-test-app/src/controllers/AuthController.js +81 -0
  116. package/my-test-app/src/controllers/HomeController.js +17 -0
  117. package/my-test-app/src/controllers/PostController.js +86 -0
  118. package/my-test-app/src/controllers/ProfileController.js +66 -0
  119. package/my-test-app/src/helpers/api.js +24 -0
  120. package/my-test-app/src/main.jsx +9 -0
  121. package/my-test-app/src/middleware/auth.js +16 -0
  122. package/my-test-app/src/models/Post.js +63 -0
  123. package/my-test-app/src/models/User.js +42 -0
  124. package/my-test-app/src/routes/App.jsx +38 -0
  125. package/my-test-app/src/routes/api/auth.js +7 -0
  126. package/my-test-app/src/routes/api/posts.js +15 -0
  127. package/my-test-app/src/routes/api/profile.js +8 -0
  128. package/my-test-app/src/server/index.js +16 -0
  129. package/my-test-app/vite.config.js +18 -0
  130. package/package.json +61 -0
  131. package/scripts/test-app.sh +59 -0
  132. package/src/auth/jwt.js +195 -0
  133. package/src/auth/password.js +132 -0
  134. package/src/cli/colors.js +31 -0
  135. package/src/cli/dispatcher.js +168 -0
  136. package/src/cli/parser.js +91 -0
  137. package/src/client/context.js +4 -0
  138. package/src/client/hooks.js +50 -0
  139. package/src/client/index.js +3 -0
  140. package/src/client/router.js +184 -0
  141. package/src/commands/build.js +155 -0
  142. package/src/commands/dev.js +206 -0
  143. package/src/commands/help.js +84 -0
  144. package/src/commands/make-controller.js +36 -0
  145. package/src/commands/make-middleware.js +44 -0
  146. package/src/commands/make-migration.js +51 -0
  147. package/src/commands/make-model.js +38 -0
  148. package/src/commands/make-route.js +36 -0
  149. package/src/commands/make-seed.js +38 -0
  150. package/src/commands/migrate-fresh.js +32 -0
  151. package/src/commands/migrate-rollback.js +30 -0
  152. package/src/commands/migrate-status.js +41 -0
  153. package/src/commands/migrate.js +30 -0
  154. package/src/commands/seed.js +47 -0
  155. package/src/commands/start.js +69 -0
  156. package/src/commands/test.js +46 -0
  157. package/src/db/Grammar.js +125 -0
  158. package/src/db/QueryBuilder.js +476 -0
  159. package/src/db/adapters/neon.js +170 -0
  160. package/src/db/adapters/planetscale.js +146 -0
  161. package/src/db/adapters/postgres.js +166 -0
  162. package/src/db/adapters/sqlite.js +125 -0
  163. package/src/db/adapters/turso.js +165 -0
  164. package/src/db/index.js +156 -0
  165. package/src/db/migrator.js +250 -0
  166. package/src/db/seeder.js +124 -0
  167. package/src/index.js +12 -0
  168. package/src/scaffolding/index.js +152 -0
  169. package/src/server/body-parser.js +159 -0
  170. package/src/server/cors.js +63 -0
  171. package/src/server/default-entry.js +13 -0
  172. package/src/server/http.js +221 -0
  173. package/src/server/index.js +168 -0
  174. package/src/server/loader.js +128 -0
  175. package/src/server/router.js +281 -0
  176. package/src/server/static.js +139 -0
  177. package/src/validation/index.js +436 -0
  178. package/src/vite/config.js +49 -0
  179. package/stubs/controller.stub +48 -0
  180. package/stubs/middleware-auth.stub +29 -0
  181. package/stubs/middleware.stub +9 -0
  182. package/stubs/migration.stub +17 -0
  183. package/stubs/model.stub +77 -0
  184. package/stubs/route.stub +13 -0
  185. package/stubs/seed.stub +16 -0
  186. package/stubs/vite.config.stub +18 -0
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Body parser middleware.
3
+ * Parses JSON and URL-encoded bodies.
4
+ *
5
+ * No dependencies - uses native Node.js APIs.
6
+ */
7
+
8
+ /**
9
+ * Parse request body based on content-type
10
+ */
11
+ export function bodyParser(options = {}) {
12
+ const limit = options.limit || '1mb'
13
+ const maxBytes = parseSize(limit)
14
+
15
+ return async (req, res, next) => {
16
+ // Skip if no body expected
17
+ if (req.method === 'GET' || req.method === 'HEAD') {
18
+ return next()
19
+ }
20
+
21
+ const contentType = req.headers['content-type'] || ''
22
+
23
+ try {
24
+ const raw = await readBody(req, maxBytes)
25
+
26
+ if (contentType.includes('application/json')) {
27
+ req.body = raw ? JSON.parse(raw) : {}
28
+ } else if (contentType.includes('application/x-www-form-urlencoded')) {
29
+ req.body = parseUrlEncoded(raw)
30
+ } else {
31
+ req.body = raw
32
+ }
33
+
34
+ next()
35
+ } catch (err) {
36
+ if (err.code === 'BODY_TOO_LARGE') {
37
+ res.statusCode = 413
38
+ res.end(JSON.stringify({ error: 'Payload too large' }))
39
+ return
40
+ }
41
+
42
+ if (err instanceof SyntaxError) {
43
+ res.statusCode = 400
44
+ res.end(JSON.stringify({ error: 'Invalid JSON' }))
45
+ return
46
+ }
47
+
48
+ next(err)
49
+ }
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Read raw body from request stream
55
+ */
56
+ function readBody(req, maxBytes) {
57
+ return new Promise((resolve, reject) => {
58
+ const chunks = []
59
+ let size = 0
60
+
61
+ req.on('data', (chunk) => {
62
+ size += chunk.length
63
+ if (size > maxBytes) {
64
+ const err = new Error('Body too large')
65
+ err.code = 'BODY_TOO_LARGE'
66
+ reject(err)
67
+ req.destroy()
68
+ return
69
+ }
70
+ chunks.push(chunk)
71
+ })
72
+
73
+ req.on('end', () => {
74
+ resolve(Buffer.concat(chunks).toString('utf8'))
75
+ })
76
+
77
+ req.on('error', reject)
78
+ })
79
+ }
80
+
81
+ /**
82
+ * Parse URL-encoded body
83
+ */
84
+ function parseUrlEncoded(str) {
85
+ if (!str) return {}
86
+
87
+ const params = new URLSearchParams(str)
88
+ const result = {}
89
+
90
+ for (const [key, value] of params) {
91
+ // Handle array notation: items[]=a&items[]=b
92
+ if (key.endsWith('[]')) {
93
+ const arrayKey = key.slice(0, -2)
94
+ if (!result[arrayKey]) result[arrayKey] = []
95
+ result[arrayKey].push(value)
96
+ } else {
97
+ result[key] = value
98
+ }
99
+ }
100
+
101
+ return result
102
+ }
103
+
104
+ /**
105
+ * Parse size string to bytes
106
+ */
107
+ function parseSize(str) {
108
+ if (typeof str === 'number') return str
109
+
110
+ const units = {
111
+ b: 1,
112
+ kb: 1024,
113
+ mb: 1024 * 1024,
114
+ gb: 1024 * 1024 * 1024
115
+ }
116
+
117
+ const match = str.toLowerCase().match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)?$/)
118
+ if (!match) return 1024 * 1024 // default 1mb
119
+
120
+ const num = parseFloat(match[1])
121
+ const unit = match[2] || 'b'
122
+
123
+ return Math.floor(num * units[unit])
124
+ }
125
+
126
+ /**
127
+ * JSON-only body parser
128
+ */
129
+ export function json(options = {}) {
130
+ const limit = options.limit || '1mb'
131
+ const maxBytes = parseSize(limit)
132
+
133
+ return async (req, res, next) => {
134
+ if (req.method === 'GET' || req.method === 'HEAD') {
135
+ return next()
136
+ }
137
+
138
+ const contentType = req.headers['content-type'] || ''
139
+ if (!contentType.includes('application/json')) {
140
+ req.body = {}
141
+ return next()
142
+ }
143
+
144
+ try {
145
+ const raw = await readBody(req, maxBytes)
146
+ req.body = raw ? JSON.parse(raw) : {}
147
+ next()
148
+ } catch (err) {
149
+ if (err.code === 'BODY_TOO_LARGE') {
150
+ res.statusCode = 413
151
+ res.end(JSON.stringify({ error: 'Payload too large' }))
152
+ return
153
+ }
154
+
155
+ res.statusCode = 400
156
+ res.end(JSON.stringify({ error: 'Invalid JSON' }))
157
+ }
158
+ }
159
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * CORS middleware.
3
+ * Handles Cross-Origin Resource Sharing headers.
4
+ */
5
+
6
+ const defaults = {
7
+ origin: '*',
8
+ methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
9
+ allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
10
+ exposedHeaders: [],
11
+ credentials: false,
12
+ maxAge: 86400 // 24 hours
13
+ }
14
+
15
+ export function cors(options = {}) {
16
+ const config = { ...defaults, ...options }
17
+
18
+ return (req, res, next) => {
19
+ const origin = req.headers.origin
20
+
21
+ // Set origin header
22
+ if (config.origin === '*') {
23
+ res.setHeader('Access-Control-Allow-Origin', '*')
24
+ } else if (typeof config.origin === 'string') {
25
+ res.setHeader('Access-Control-Allow-Origin', config.origin)
26
+ res.setHeader('Vary', 'Origin')
27
+ } else if (Array.isArray(config.origin)) {
28
+ if (origin && config.origin.includes(origin)) {
29
+ res.setHeader('Access-Control-Allow-Origin', origin)
30
+ res.setHeader('Vary', 'Origin')
31
+ }
32
+ } else if (typeof config.origin === 'function') {
33
+ const allowed = config.origin(origin, req)
34
+ if (allowed) {
35
+ res.setHeader('Access-Control-Allow-Origin', typeof allowed === 'string' ? allowed : origin)
36
+ res.setHeader('Vary', 'Origin')
37
+ }
38
+ }
39
+
40
+ // Credentials
41
+ if (config.credentials) {
42
+ res.setHeader('Access-Control-Allow-Credentials', 'true')
43
+ }
44
+
45
+ // Exposed headers
46
+ if (config.exposedHeaders.length > 0) {
47
+ res.setHeader('Access-Control-Expose-Headers', config.exposedHeaders.join(', '))
48
+ }
49
+
50
+ // Handle preflight
51
+ if (req.method === 'OPTIONS') {
52
+ res.setHeader('Access-Control-Allow-Methods', config.methods.join(', '))
53
+ res.setHeader('Access-Control-Allow-Headers', config.allowedHeaders.join(', '))
54
+ res.setHeader('Access-Control-Max-Age', String(config.maxAge))
55
+
56
+ res.statusCode = 204
57
+ res.end()
58
+ return
59
+ }
60
+
61
+ next()
62
+ }
63
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Default server entry point
3
+ * Used when no custom src/server/index.js exists
4
+ */
5
+
6
+ import { createServer } from './index.js'
7
+
8
+ const app = await createServer()
9
+ const port = process.env.PORT || 3001
10
+
11
+ app.listen(port, () => {
12
+ console.log(`Server running at http://localhost:${port}`)
13
+ })
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Minimal HTTP Server
3
+ *
4
+ * Zero-dependency HTTP server using Node's built-in http module.
5
+ * Express/Polka-compatible middleware API.
6
+ */
7
+
8
+ import { createServer as createHttpServer } from 'node:http'
9
+
10
+ /**
11
+ * Create a minimal HTTP server instance
12
+ */
13
+ export function createApp(options = {}) {
14
+ const middleware = []
15
+ const routes = new Map() // method -> [{pattern, params, handlers}]
16
+ const onError = options.onError || defaultErrorHandler
17
+ const onNoMatch = options.onNoMatch || defaultNotFoundHandler
18
+
19
+ // Initialize route maps for each method
20
+ const methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']
21
+ for (const method of methods) {
22
+ routes.set(method, [])
23
+ }
24
+
25
+ /**
26
+ * Add global middleware
27
+ */
28
+ function use(...handlers) {
29
+ for (const handler of handlers) {
30
+ if (typeof handler === 'function') {
31
+ middleware.push(handler)
32
+ }
33
+ }
34
+ return app
35
+ }
36
+
37
+ /**
38
+ * Register a route
39
+ */
40
+ function addRoute(method, path, ...handlers) {
41
+ const { pattern, paramNames } = pathToPattern(path)
42
+ routes.get(method.toUpperCase()).push({
43
+ path,
44
+ pattern,
45
+ paramNames,
46
+ handlers
47
+ })
48
+ return app
49
+ }
50
+
51
+ /**
52
+ * Convert path with params to regex
53
+ * /users/:id -> /^\/users\/([^/]+)$/
54
+ */
55
+ function pathToPattern(path) {
56
+ const paramNames = []
57
+
58
+ let pattern = path
59
+ // Handle catch-all
60
+ .replace(/\/\*$/, '/(?<_catchAll>.*)')
61
+ // Handle :params
62
+ .replace(/:(\w+)/g, (_, name) => {
63
+ paramNames.push(name)
64
+ return '([^/]+)'
65
+ })
66
+ // Escape slashes
67
+ .replace(/\//g, '\\/')
68
+
69
+ return {
70
+ pattern: new RegExp(`^${pattern}$`),
71
+ paramNames
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Match a request to a route
77
+ */
78
+ function matchRoute(method, pathname) {
79
+ const routeList = routes.get(method.toUpperCase()) || []
80
+
81
+ for (const route of routeList) {
82
+ const match = pathname.match(route.pattern)
83
+ if (match) {
84
+ // Extract params
85
+ const params = {}
86
+ route.paramNames.forEach((name, i) => {
87
+ params[name] = match[i + 1]
88
+ })
89
+ // Handle catch-all
90
+ if (match.groups?._catchAll !== undefined) {
91
+ params._catchAll = match.groups._catchAll
92
+ }
93
+ return { route, params }
94
+ }
95
+ }
96
+
97
+ return null
98
+ }
99
+
100
+ /**
101
+ * Run middleware chain
102
+ */
103
+ async function runMiddleware(handlers, req, res) {
104
+ let index = 0
105
+
106
+ const next = async (err) => {
107
+ if (err) {
108
+ return onError(err, req, res)
109
+ }
110
+
111
+ if (index >= handlers.length) {
112
+ return
113
+ }
114
+
115
+ const handler = handlers[index++]
116
+
117
+ try {
118
+ await handler(req, res, next)
119
+ } catch (error) {
120
+ onError(error, req, res)
121
+ }
122
+ }
123
+
124
+ await next()
125
+ }
126
+
127
+ /**
128
+ * Handle incoming request
129
+ */
130
+ async function handleRequest(req, res) {
131
+ // Parse URL using WHATWG URL API
132
+ const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`)
133
+ req.path = url.pathname || '/'
134
+ req.query = Object.fromEntries(url.searchParams)
135
+ req.params = {}
136
+
137
+ // Find matching route
138
+ const matched = matchRoute(req.method, req.path)
139
+
140
+ if (matched) {
141
+ req.params = matched.params
142
+
143
+ // Combine global middleware + route handlers
144
+ const handlers = [...middleware, ...matched.route.handlers]
145
+ await runMiddleware(handlers, req, res)
146
+ } else {
147
+ // Run global middleware first, then 404
148
+ await runMiddleware([...middleware, () => onNoMatch(req, res)], req, res)
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Create the HTTP server
154
+ */
155
+ const server = createHttpServer((req, res) => {
156
+ handleRequest(req, res).catch(err => onError(err, req, res))
157
+ })
158
+
159
+ /**
160
+ * The app object
161
+ */
162
+ const app = {
163
+ use,
164
+ server,
165
+
166
+ // HTTP method shortcuts
167
+ get: (path, ...handlers) => addRoute('GET', path, ...handlers),
168
+ post: (path, ...handlers) => addRoute('POST', path, ...handlers),
169
+ put: (path, ...handlers) => addRoute('PUT', path, ...handlers),
170
+ patch: (path, ...handlers) => addRoute('PATCH', path, ...handlers),
171
+ delete: (path, ...handlers) => addRoute('DELETE', path, ...handlers),
172
+ head: (path, ...handlers) => addRoute('HEAD', path, ...handlers),
173
+ options: (path, ...handlers) => addRoute('OPTIONS', path, ...handlers),
174
+
175
+ /**
176
+ * Start listening
177
+ */
178
+ listen(port, callback) {
179
+ server.listen(port, callback)
180
+ return app
181
+ },
182
+
183
+ /**
184
+ * Close the server
185
+ */
186
+ close(callback) {
187
+ server.close(callback)
188
+ }
189
+ }
190
+
191
+ return app
192
+ }
193
+
194
+ /**
195
+ * Default error handler
196
+ */
197
+ function defaultErrorHandler(err, req, res) {
198
+ console.error('Server error:', err)
199
+
200
+ if (res.headersSent) return
201
+
202
+ const statusCode = err.statusCode || err.status || 500
203
+ const message = process.env.NODE_ENV === 'production'
204
+ ? 'Internal Server Error'
205
+ : err.message
206
+
207
+ res.statusCode = statusCode
208
+ res.setHeader('Content-Type', 'application/json')
209
+ res.end(JSON.stringify({ error: message }))
210
+ }
211
+
212
+ /**
213
+ * Default 404 handler
214
+ */
215
+ function defaultNotFoundHandler(req, res) {
216
+ if (res.headersSent) return
217
+
218
+ res.statusCode = 404
219
+ res.setHeader('Content-Type', 'application/json')
220
+ res.end(JSON.stringify({ error: 'Not Found' }))
221
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * BasicBen Server
3
+ *
4
+ * Zero-dependency HTTP server with custom router, middleware, and auto-loading.
5
+ */
6
+
7
+ import { createApp } from './http.js'
8
+ import { Router, createRouter } from './router.js'
9
+ import { bodyParser, json } from './body-parser.js'
10
+ import { cors } from './cors.js'
11
+ import { serveStatic } from './static.js'
12
+ import { loadRoutes, loadMiddleware, loadConfig } from './loader.js'
13
+
14
+ /**
15
+ * Create a BasicBen server instance
16
+ */
17
+ export async function createServer(options = {}) {
18
+ const config = await loadConfig()
19
+ const mergedConfig = { ...defaultConfig, ...config, ...options }
20
+
21
+ const app = createApp({
22
+ onError: mergedConfig.onError || defaultErrorHandler,
23
+ onNoMatch: mergedConfig.onNoMatch || defaultNotFoundHandler
24
+ })
25
+
26
+ // Core middleware
27
+ app.use(addResponseHelpers)
28
+
29
+ if (mergedConfig.cors) {
30
+ app.use(cors(mergedConfig.cors === true ? {} : mergedConfig.cors))
31
+ }
32
+
33
+ if (mergedConfig.bodyParser !== false) {
34
+ app.use(bodyParser(mergedConfig.bodyParser || {}))
35
+ }
36
+
37
+ if (mergedConfig.static) {
38
+ app.use(serveStatic(mergedConfig.static === true ? {} : mergedConfig.static))
39
+ }
40
+
41
+ // Load user middleware
42
+ if (mergedConfig.autoloadMiddleware !== false) {
43
+ const userMiddleware = await loadMiddleware(mergedConfig.middlewareDir)
44
+ for (const mw of userMiddleware) {
45
+ app.use(mw)
46
+ }
47
+ }
48
+
49
+ // Load routes
50
+ if (mergedConfig.autoloadRoutes !== false) {
51
+ const router = await loadRoutes(mergedConfig.routesDir)
52
+ router.applyTo(app)
53
+ }
54
+
55
+ // Add helper methods
56
+ app.router = createRouter()
57
+
58
+ /**
59
+ * Start the server
60
+ */
61
+ app.start = (port, callback) => {
62
+ const listenPort = port || mergedConfig.port || 3001
63
+
64
+ return new Promise((resolve, reject) => {
65
+ app.listen(listenPort, (err) => {
66
+ if (err) {
67
+ reject(err)
68
+ return
69
+ }
70
+
71
+ if (callback) callback()
72
+ resolve(app)
73
+ })
74
+ })
75
+ }
76
+
77
+ return app
78
+ }
79
+
80
+ /**
81
+ * Default configuration
82
+ */
83
+ const defaultConfig = {
84
+ port: 3001,
85
+ cors: true,
86
+ bodyParser: { limit: '1mb' },
87
+ static: { dir: 'public' },
88
+ routesDir: 'src/routes',
89
+ middlewareDir: 'src/middleware',
90
+ autoloadRoutes: true,
91
+ autoloadMiddleware: true
92
+ }
93
+
94
+ /**
95
+ * Default error handler
96
+ */
97
+ function defaultErrorHandler(err, req, res) {
98
+ console.error('Server error:', err)
99
+
100
+ const statusCode = err.statusCode || err.status || 500
101
+ const message = process.env.NODE_ENV === 'production'
102
+ ? 'Internal Server Error'
103
+ : err.message
104
+
105
+ res.statusCode = statusCode
106
+ res.setHeader('Content-Type', 'application/json')
107
+ res.end(JSON.stringify({ error: message }))
108
+ }
109
+
110
+ /**
111
+ * Default 404 handler
112
+ */
113
+ function defaultNotFoundHandler(req, res) {
114
+ res.statusCode = 404
115
+ res.setHeader('Content-Type', 'application/json')
116
+ res.end(JSON.stringify({ error: 'Not Found' }))
117
+ }
118
+
119
+ /**
120
+ * Response helpers - added to res object
121
+ */
122
+ export function addResponseHelpers(req, res, next) {
123
+ /**
124
+ * Send JSON response
125
+ */
126
+ res.json = (data, statusCode = 200) => {
127
+ res.statusCode = statusCode
128
+ res.setHeader('Content-Type', 'application/json')
129
+ res.end(JSON.stringify(data))
130
+ }
131
+
132
+ /**
133
+ * Send response with status code
134
+ */
135
+ res.status = (code) => {
136
+ res.statusCode = code
137
+ return res
138
+ }
139
+
140
+ /**
141
+ * Send text response
142
+ */
143
+ res.send = (data) => {
144
+ if (typeof data === 'object') {
145
+ return res.json(data)
146
+ }
147
+ res.end(String(data))
148
+ }
149
+
150
+ /**
151
+ * Redirect to URL
152
+ */
153
+ res.redirect = (url, statusCode = 302) => {
154
+ res.statusCode = statusCode
155
+ res.setHeader('Location', url)
156
+ res.end()
157
+ }
158
+
159
+ next()
160
+ }
161
+
162
+ // Re-export components
163
+ export { createApp } from './http.js'
164
+ export { Router, createRouter } from './router.js'
165
+ export { bodyParser, json } from './body-parser.js'
166
+ export { cors } from './cors.js'
167
+ export { serveStatic } from './static.js'
168
+ export { loadRoutes, loadMiddleware, loadConfig } from './loader.js'