@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,128 @@
1
+ /**
2
+ * Auto-loaders for routes and middleware.
3
+ * Scans directories and loads files automatically.
4
+ */
5
+
6
+ import { readdirSync, existsSync, statSync } from 'node:fs'
7
+ import { join, resolve } from 'node:path'
8
+ import { pathToFileURL } from 'node:url'
9
+ import { Router } from './router.js'
10
+
11
+ /**
12
+ * Load all route files from a directory.
13
+ * Each file should export a default function that receives the router.
14
+ *
15
+ * @param {string} dir - Directory to scan (default: src/routes)
16
+ * @returns {Router} - Router with all routes loaded
17
+ */
18
+ export async function loadRoutes(dir = 'src/routes') {
19
+ const router = new Router()
20
+ const routesDir = resolve(process.cwd(), dir)
21
+
22
+ if (!existsSync(routesDir)) {
23
+ return router
24
+ }
25
+
26
+ const files = getJsFiles(routesDir)
27
+
28
+ for (const file of files) {
29
+ try {
30
+ const fileUrl = pathToFileURL(file).href
31
+ const module = await import(fileUrl)
32
+
33
+ if (typeof module.default === 'function') {
34
+ module.default(router)
35
+ }
36
+ } catch (err) {
37
+ console.error(`Error loading route file: ${file}`)
38
+ console.error(err.message)
39
+ }
40
+ }
41
+
42
+ return router
43
+ }
44
+
45
+ /**
46
+ * Load all middleware files from a directory.
47
+ * Files are loaded in alphabetical order.
48
+ * Each file should export a default middleware function.
49
+ *
50
+ * @param {string} dir - Directory to scan (default: src/middleware)
51
+ * @returns {Function[]} - Array of middleware functions
52
+ */
53
+ export async function loadMiddleware(dir = 'src/middleware') {
54
+ const middleware = []
55
+ const middlewareDir = resolve(process.cwd(), dir)
56
+
57
+ if (!existsSync(middlewareDir)) {
58
+ return middleware
59
+ }
60
+
61
+ const files = getJsFiles(middlewareDir).sort()
62
+
63
+ for (const file of files) {
64
+ try {
65
+ const fileUrl = pathToFileURL(file).href
66
+ const module = await import(fileUrl)
67
+
68
+ if (typeof module.default === 'function') {
69
+ middleware.push(module.default)
70
+ }
71
+ } catch (err) {
72
+ console.error(`Error loading middleware file: ${file}`)
73
+ console.error(err.message)
74
+ }
75
+ }
76
+
77
+ return middleware
78
+ }
79
+
80
+ /**
81
+ * Get all .js files in a directory (recursive)
82
+ */
83
+ function getJsFiles(dir, files = []) {
84
+ const entries = readdirSync(dir)
85
+
86
+ for (const entry of entries) {
87
+ const fullPath = join(dir, entry)
88
+ const stat = statSync(fullPath)
89
+
90
+ if (stat.isDirectory()) {
91
+ getJsFiles(fullPath, files)
92
+ } else if (entry.endsWith('.js') && !entry.endsWith('.test.js')) {
93
+ files.push(fullPath)
94
+ }
95
+ }
96
+
97
+ return files
98
+ }
99
+
100
+ /**
101
+ * Load config file if it exists
102
+ *
103
+ * @returns {Object} - Config object or empty defaults
104
+ */
105
+ export async function loadConfig() {
106
+ const configPaths = [
107
+ 'basicben.config.js',
108
+ 'basicben.config.mjs'
109
+ ]
110
+
111
+ for (const configPath of configPaths) {
112
+ const fullPath = resolve(process.cwd(), configPath)
113
+
114
+ if (existsSync(fullPath)) {
115
+ try {
116
+ const fileUrl = pathToFileURL(fullPath).href
117
+ const module = await import(fileUrl)
118
+ return module.default || {}
119
+ } catch (err) {
120
+ console.error(`\x1b[31mError loading config:\x1b[0m ${configPath}`)
121
+ console.error(` ${err.message}`)
122
+ console.error(`\x1b[2mCheck your config file syntax.\x1b[0m\n`)
123
+ }
124
+ }
125
+ }
126
+
127
+ return {}
128
+ }
@@ -0,0 +1,281 @@
1
+ /**
2
+ * Custom Router
3
+ *
4
+ * Features:
5
+ * - Route registration (GET, POST, PUT, PATCH, DELETE)
6
+ * - Route groups with shared prefix/middleware
7
+ * - Per-route middleware
8
+ * - Named routes for URL generation
9
+ * - Parameter parsing
10
+ */
11
+
12
+ const METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options']
13
+
14
+ export class Router {
15
+ constructor(options = {}) {
16
+ this.prefix = options.prefix || ''
17
+ this.middleware = options.middleware || []
18
+ this.routes = []
19
+ this.namedRoutes = new Map()
20
+ this.groups = []
21
+ }
22
+
23
+ /**
24
+ * Register a route
25
+ */
26
+ #addRoute(method, path, ...handlers) {
27
+ const fullPath = this.#normalizePath(this.prefix + path)
28
+
29
+ // Last handler is the main handler, rest are middleware
30
+ const mainHandler = handlers.pop()
31
+ const routeMiddleware = handlers
32
+
33
+ // Check if first middleware arg is a string (route name)
34
+ let name = null
35
+ if (typeof routeMiddleware[0] === 'string') {
36
+ name = routeMiddleware.shift()
37
+ }
38
+
39
+ const route = {
40
+ method: method.toUpperCase(),
41
+ path: fullPath,
42
+ pattern: this.#pathToPattern(fullPath),
43
+ middleware: [...this.middleware, ...routeMiddleware],
44
+ handler: mainHandler,
45
+ name
46
+ }
47
+
48
+ this.routes.push(route)
49
+
50
+ if (name) {
51
+ this.namedRoutes.set(name, route)
52
+ }
53
+
54
+ return this
55
+ }
56
+
57
+ /**
58
+ * Normalize path - ensure leading slash, no trailing slash
59
+ */
60
+ #normalizePath(path) {
61
+ if (!path || path === '/') return '/'
62
+ let normalized = path.startsWith('/') ? path : '/' + path
63
+ return normalized.endsWith('/') ? normalized.slice(0, -1) : normalized
64
+ }
65
+
66
+ /**
67
+ * Convert path to regex pattern for matching
68
+ */
69
+ #pathToPattern(path) {
70
+ // Convert :param to named capture groups
71
+ // /users/:id -> /users/(?<id>[^/]+)
72
+ // Also handle catch-all: /* -> /(.*)
73
+ let pattern = path
74
+ .replace(/\/\*$/, '/(?<_catchAll>.*)') // Catch-all at end
75
+ .replace(/\/:(\w+)/g, '/(?<$1>[^/]+)')
76
+ .replace(/\//g, '\\/')
77
+
78
+ return new RegExp(`^${pattern}$`)
79
+ }
80
+
81
+ /**
82
+ * HTTP method shortcuts
83
+ */
84
+ get(path, ...handlers) {
85
+ return this.#addRoute('get', path, ...handlers)
86
+ }
87
+
88
+ post(path, ...handlers) {
89
+ return this.#addRoute('post', path, ...handlers)
90
+ }
91
+
92
+ put(path, ...handlers) {
93
+ return this.#addRoute('put', path, ...handlers)
94
+ }
95
+
96
+ patch(path, ...handlers) {
97
+ return this.#addRoute('patch', path, ...handlers)
98
+ }
99
+
100
+ delete(path, ...handlers) {
101
+ return this.#addRoute('delete', path, ...handlers)
102
+ }
103
+
104
+ head(path, ...handlers) {
105
+ return this.#addRoute('head', path, ...handlers)
106
+ }
107
+
108
+ options(path, ...handlers) {
109
+ return this.#addRoute('options', path, ...handlers)
110
+ }
111
+
112
+ /**
113
+ * Register route for all methods
114
+ */
115
+ all(path, ...handlers) {
116
+ for (const method of METHODS) {
117
+ this.#addRoute(method, path, ...handlers)
118
+ }
119
+ return this
120
+ }
121
+
122
+ /**
123
+ * Create a route group with shared prefix and/or middleware
124
+ *
125
+ * Usage:
126
+ * router.group('/admin', adminAuth, (group) => {
127
+ * group.get('/users', listUsers)
128
+ * group.get('/users/:id', showUser)
129
+ * })
130
+ */
131
+ group(prefix, ...args) {
132
+ const callback = args.pop()
133
+ const groupMiddleware = args
134
+
135
+ const group = new Router({
136
+ prefix: this.prefix + prefix,
137
+ middleware: [...this.middleware, ...groupMiddleware]
138
+ })
139
+
140
+ callback(group)
141
+
142
+ // Merge group routes into this router
143
+ this.routes.push(...group.routes)
144
+ for (const [name, route] of group.namedRoutes) {
145
+ this.namedRoutes.set(name, route)
146
+ }
147
+
148
+ return this
149
+ }
150
+
151
+ /**
152
+ * Add middleware to all routes in this router
153
+ */
154
+ use(...middleware) {
155
+ this.middleware.push(...middleware)
156
+ return this
157
+ }
158
+
159
+ /**
160
+ * Generate URL for a named route
161
+ *
162
+ * Usage:
163
+ * router.route('users.show', { id: 1 }) // => '/users/1'
164
+ */
165
+ route(name, params = {}) {
166
+ const route = this.namedRoutes.get(name)
167
+ if (!route) {
168
+ throw new Error(`Route '${name}' not found`)
169
+ }
170
+
171
+ let path = route.path
172
+ for (const [key, value] of Object.entries(params)) {
173
+ path = path.replace(`:${key}`, String(value))
174
+ }
175
+
176
+ return path
177
+ }
178
+
179
+ /**
180
+ * Match a request to a route
181
+ */
182
+ match(method, path) {
183
+ const normalizedPath = this.#normalizePath(path)
184
+
185
+ for (const route of this.routes) {
186
+ if (route.method !== method.toUpperCase()) continue
187
+
188
+ const match = normalizedPath.match(route.pattern)
189
+ if (match) {
190
+ return {
191
+ route,
192
+ params: match.groups || {}
193
+ }
194
+ }
195
+ }
196
+
197
+ return null
198
+ }
199
+
200
+ /**
201
+ * Apply routes to an app instance
202
+ */
203
+ applyTo(app) {
204
+ for (const route of this.routes) {
205
+ const method = route.method.toLowerCase()
206
+ const handlers = [...route.middleware, route.handler].map(wrapAsync)
207
+
208
+ app[method](route.path, ...handlers)
209
+ }
210
+
211
+ return app
212
+ }
213
+
214
+ /**
215
+ * Get all registered routes (for debugging/listing)
216
+ */
217
+ getRoutes() {
218
+ return this.routes.map(r => ({
219
+ method: r.method,
220
+ path: r.path,
221
+ name: r.name,
222
+ middlewareCount: r.middleware.length
223
+ }))
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Resource routing helper - generates CRUD routes
229
+ *
230
+ * Usage:
231
+ * router.resource('/users', UserController)
232
+ *
233
+ * Generates:
234
+ * GET /users -> index
235
+ * GET /users/:id -> show
236
+ * POST /users -> create
237
+ * PUT /users/:id -> update
238
+ * DELETE /users/:id -> destroy
239
+ */
240
+ Router.prototype.resource = function(path, controller, options = {}) {
241
+ const name = options.name || path.replace(/^\//, '').replace(/\//g, '.')
242
+ const only = options.only || ['index', 'show', 'create', 'update', 'destroy']
243
+ const middleware = options.middleware || []
244
+
245
+ if (only.includes('index') && controller.index) {
246
+ this.get(path, ...middleware, `${name}.index`, controller.index)
247
+ }
248
+ if (only.includes('show') && controller.show) {
249
+ this.get(`${path}/:id`, ...middleware, `${name}.show`, controller.show)
250
+ }
251
+ if (only.includes('create') && controller.create) {
252
+ this.post(path, ...middleware, `${name}.create`, controller.create)
253
+ }
254
+ if (only.includes('update') && controller.update) {
255
+ this.put(`${path}/:id`, ...middleware, `${name}.update`, controller.update)
256
+ }
257
+ if (only.includes('destroy') && controller.destroy) {
258
+ this.delete(`${path}/:id`, ...middleware, `${name}.destroy`, controller.destroy)
259
+ }
260
+
261
+ return this
262
+ }
263
+
264
+ /**
265
+ * Create a new router instance
266
+ */
267
+ export function createRouter(options) {
268
+ return new Router(options)
269
+ }
270
+
271
+ /**
272
+ * Wrap handler to catch async errors and pass to error handler
273
+ */
274
+ function wrapAsync(fn) {
275
+ return (req, res, next) => {
276
+ const result = fn(req, res, next)
277
+ if (result && typeof result.catch === 'function') {
278
+ result.catch(next)
279
+ }
280
+ }
281
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Static file serving middleware.
3
+ * Serves files from a directory with caching headers.
4
+ */
5
+
6
+ import { createReadStream, statSync } from 'node:fs'
7
+ import { join, extname, resolve } from 'node:path'
8
+
9
+ const mimeTypes = {
10
+ '.html': 'text/html',
11
+ '.htm': 'text/html',
12
+ '.css': 'text/css',
13
+ '.js': 'application/javascript',
14
+ '.mjs': 'application/javascript',
15
+ '.json': 'application/json',
16
+ '.png': 'image/png',
17
+ '.jpg': 'image/jpeg',
18
+ '.jpeg': 'image/jpeg',
19
+ '.gif': 'image/gif',
20
+ '.svg': 'image/svg+xml',
21
+ '.ico': 'image/x-icon',
22
+ '.webp': 'image/webp',
23
+ '.woff': 'font/woff',
24
+ '.woff2': 'font/woff2',
25
+ '.ttf': 'font/ttf',
26
+ '.eot': 'application/vnd.ms-fontobject',
27
+ '.otf': 'font/otf',
28
+ '.mp4': 'video/mp4',
29
+ '.webm': 'video/webm',
30
+ '.mp3': 'audio/mpeg',
31
+ '.wav': 'audio/wav',
32
+ '.pdf': 'application/pdf',
33
+ '.txt': 'text/plain',
34
+ '.xml': 'application/xml',
35
+ '.zip': 'application/zip'
36
+ }
37
+
38
+ const defaults = {
39
+ dir: 'public',
40
+ prefix: '/',
41
+ maxAge: 86400, // 24 hours in seconds
42
+ index: 'index.html',
43
+ dotfiles: 'ignore' // 'ignore', 'allow', 'deny'
44
+ }
45
+
46
+ export function serveStatic(options = {}) {
47
+ const config = { ...defaults, ...options }
48
+ const root = resolve(process.cwd(), config.dir)
49
+ const prefix = config.prefix.endsWith('/') ? config.prefix : config.prefix + '/'
50
+
51
+ return (req, res, next) => {
52
+ // Only handle GET and HEAD
53
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
54
+ return next()
55
+ }
56
+
57
+ // Check if path starts with prefix
58
+ let urlPath = req.path || req.url.split('?')[0]
59
+ if (!urlPath.startsWith(prefix) && urlPath !== config.prefix.replace(/\/$/, '')) {
60
+ return next()
61
+ }
62
+
63
+ // Remove prefix to get file path
64
+ let filePath = urlPath.slice(prefix.length - 1) || '/'
65
+
66
+ // Prevent directory traversal
67
+ if (filePath.includes('..')) {
68
+ res.statusCode = 403
69
+ res.end('Forbidden')
70
+ return
71
+ }
72
+
73
+ // Handle dotfiles
74
+ const segments = filePath.split('/')
75
+ const hasDotfile = segments.some(s => s.startsWith('.') && s !== '.')
76
+ if (hasDotfile) {
77
+ if (config.dotfiles === 'deny') {
78
+ res.statusCode = 403
79
+ res.end('Forbidden')
80
+ return
81
+ }
82
+ if (config.dotfiles === 'ignore') {
83
+ return next()
84
+ }
85
+ }
86
+
87
+ const fullPath = join(root, filePath)
88
+
89
+ // Check if file exists
90
+ let stat
91
+ try {
92
+ stat = statSync(fullPath)
93
+ } catch {
94
+ return next()
95
+ }
96
+
97
+ // If directory, try index file
98
+ if (stat.isDirectory()) {
99
+ const indexPath = join(fullPath, config.index)
100
+ try {
101
+ stat = statSync(indexPath)
102
+ if (!stat.isFile()) return next()
103
+ } catch {
104
+ return next()
105
+ }
106
+ }
107
+
108
+ if (!stat.isFile()) {
109
+ return next()
110
+ }
111
+
112
+ // Get mime type
113
+ const ext = extname(fullPath).toLowerCase()
114
+ const mime = mimeTypes[ext] || 'application/octet-stream'
115
+
116
+ // Set headers
117
+ res.setHeader('Content-Type', mime)
118
+ res.setHeader('Content-Length', stat.size)
119
+ res.setHeader('Cache-Control', `public, max-age=${config.maxAge}`)
120
+ res.setHeader('Last-Modified', stat.mtime.toUTCString())
121
+
122
+ // Handle HEAD request
123
+ if (req.method === 'HEAD') {
124
+ res.end()
125
+ return
126
+ }
127
+
128
+ // Stream file
129
+ const stream = createReadStream(fullPath)
130
+ stream.pipe(res)
131
+ stream.on('error', (err) => {
132
+ console.error('Static file error:', err)
133
+ if (!res.headersSent) {
134
+ res.statusCode = 500
135
+ res.end('Internal Server Error')
136
+ }
137
+ })
138
+ }
139
+ }