@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.
- package/.github/workflows/publish.yml +35 -0
- package/README.md +588 -0
- package/bin/cli.js +8 -0
- package/create-basicben-app/index.js +205 -0
- package/create-basicben-app/package.json +30 -0
- package/create-basicben-app/template/.env.example +24 -0
- package/create-basicben-app/template/README.md +59 -0
- package/create-basicben-app/template/basicben.config.js +33 -0
- package/create-basicben-app/template/index.html +54 -0
- package/create-basicben-app/template/migrations/001_create_users.js +15 -0
- package/create-basicben-app/template/migrations/002_create_posts.js +18 -0
- package/create-basicben-app/template/public/.gitkeep +0 -0
- package/create-basicben-app/template/seeds/01_users.js +29 -0
- package/create-basicben-app/template/seeds/02_posts.js +43 -0
- package/create-basicben-app/template/src/client/components/Alert.jsx +11 -0
- package/create-basicben-app/template/src/client/components/Avatar.jsx +11 -0
- package/create-basicben-app/template/src/client/components/BackLink.jsx +10 -0
- package/create-basicben-app/template/src/client/components/Button.jsx +19 -0
- package/create-basicben-app/template/src/client/components/Card.jsx +10 -0
- package/create-basicben-app/template/src/client/components/Empty.jsx +6 -0
- package/create-basicben-app/template/src/client/components/Input.jsx +12 -0
- package/create-basicben-app/template/src/client/components/Loading.jsx +6 -0
- package/create-basicben-app/template/src/client/components/Logo.jsx +40 -0
- package/create-basicben-app/template/src/client/components/Nav/DarkModeToggle.jsx +23 -0
- package/create-basicben-app/template/src/client/components/Nav/DesktopNav.jsx +32 -0
- package/create-basicben-app/template/src/client/components/Nav/MobileNav.jsx +107 -0
- package/create-basicben-app/template/src/client/components/NavLink.jsx +10 -0
- package/create-basicben-app/template/src/client/components/PageHeader.jsx +8 -0
- package/create-basicben-app/template/src/client/components/PostCard.jsx +19 -0
- package/create-basicben-app/template/src/client/components/Textarea.jsx +12 -0
- package/create-basicben-app/template/src/client/components/ThemeContext.jsx +5 -0
- package/create-basicben-app/template/src/client/contexts/ToastContext.jsx +94 -0
- package/create-basicben-app/template/src/client/layouts/AppLayout.jsx +60 -0
- package/create-basicben-app/template/src/client/layouts/AuthLayout.jsx +33 -0
- package/create-basicben-app/template/src/client/layouts/DocsLayout.jsx +60 -0
- package/create-basicben-app/template/src/client/layouts/RootLayout.jsx +25 -0
- package/create-basicben-app/template/src/client/pages/Auth.jsx +55 -0
- package/create-basicben-app/template/src/client/pages/Authentication.jsx +236 -0
- package/create-basicben-app/template/src/client/pages/Database.jsx +426 -0
- package/create-basicben-app/template/src/client/pages/Feed.jsx +34 -0
- package/create-basicben-app/template/src/client/pages/FeedPost.jsx +37 -0
- package/create-basicben-app/template/src/client/pages/GettingStarted.jsx +136 -0
- package/create-basicben-app/template/src/client/pages/Home.jsx +206 -0
- package/create-basicben-app/template/src/client/pages/PostForm.jsx +69 -0
- package/create-basicben-app/template/src/client/pages/Posts.jsx +59 -0
- package/create-basicben-app/template/src/client/pages/Profile.jsx +68 -0
- package/create-basicben-app/template/src/client/pages/Routing.jsx +207 -0
- package/create-basicben-app/template/src/client/pages/Testing.jsx +251 -0
- package/create-basicben-app/template/src/client/pages/Validation.jsx +210 -0
- package/create-basicben-app/template/src/controllers/AuthController.js +81 -0
- package/create-basicben-app/template/src/controllers/HomeController.js +17 -0
- package/create-basicben-app/template/src/controllers/PostController.js +86 -0
- package/create-basicben-app/template/src/controllers/ProfileController.js +66 -0
- package/create-basicben-app/template/src/helpers/api.js +24 -0
- package/create-basicben-app/template/src/main.jsx +9 -0
- package/create-basicben-app/template/src/middleware/auth.js +16 -0
- package/create-basicben-app/template/src/models/Post.js +63 -0
- package/create-basicben-app/template/src/models/User.js +42 -0
- package/create-basicben-app/template/src/routes/App.jsx +38 -0
- package/create-basicben-app/template/src/routes/api/auth.js +7 -0
- package/create-basicben-app/template/src/routes/api/posts.js +15 -0
- package/create-basicben-app/template/src/routes/api/profile.js +8 -0
- package/create-basicben-app/template/src/server/index.js +16 -0
- package/create-basicben-app/template/vite.config.js +18 -0
- package/database.sqlite +0 -0
- package/my-test-app/.env.example +24 -0
- package/my-test-app/README.md +59 -0
- package/my-test-app/basicben.config.js +33 -0
- package/my-test-app/database.sqlite-shm +0 -0
- package/my-test-app/database.sqlite-wal +0 -0
- package/my-test-app/index.html +54 -0
- package/my-test-app/migrations/001_create_users.js +15 -0
- package/my-test-app/migrations/002_create_posts.js +18 -0
- package/my-test-app/package-lock.json +2160 -0
- package/my-test-app/package.json +29 -0
- package/my-test-app/public/.gitkeep +0 -0
- package/my-test-app/seeds/01_users.js +29 -0
- package/my-test-app/seeds/02_posts.js +43 -0
- package/my-test-app/src/client/components/Alert.jsx +11 -0
- package/my-test-app/src/client/components/Avatar.jsx +11 -0
- package/my-test-app/src/client/components/BackLink.jsx +10 -0
- package/my-test-app/src/client/components/Button.jsx +19 -0
- package/my-test-app/src/client/components/Card.jsx +10 -0
- package/my-test-app/src/client/components/Empty.jsx +6 -0
- package/my-test-app/src/client/components/Input.jsx +12 -0
- package/my-test-app/src/client/components/Loading.jsx +6 -0
- package/my-test-app/src/client/components/Logo.jsx +40 -0
- package/my-test-app/src/client/components/Nav/DarkModeToggle.jsx +23 -0
- package/my-test-app/src/client/components/Nav/DesktopNav.jsx +32 -0
- package/my-test-app/src/client/components/Nav/MobileNav.jsx +107 -0
- package/my-test-app/src/client/components/NavLink.jsx +10 -0
- package/my-test-app/src/client/components/PageHeader.jsx +8 -0
- package/my-test-app/src/client/components/PostCard.jsx +19 -0
- package/my-test-app/src/client/components/Textarea.jsx +12 -0
- package/my-test-app/src/client/components/ThemeContext.jsx +5 -0
- package/my-test-app/src/client/contexts/AppContext.jsx +13 -0
- package/my-test-app/src/client/contexts/ToastContext.jsx +94 -0
- package/my-test-app/src/client/layouts/AppLayout.jsx +60 -0
- package/my-test-app/src/client/layouts/AuthLayout.jsx +33 -0
- package/my-test-app/src/client/layouts/DocsLayout.jsx +60 -0
- package/my-test-app/src/client/layouts/RootLayout.jsx +25 -0
- package/my-test-app/src/client/pages/Auth.jsx +55 -0
- package/my-test-app/src/client/pages/Authentication.jsx +236 -0
- package/my-test-app/src/client/pages/Database.jsx +426 -0
- package/my-test-app/src/client/pages/Feed.jsx +34 -0
- package/my-test-app/src/client/pages/FeedPost.jsx +37 -0
- package/my-test-app/src/client/pages/GettingStarted.jsx +136 -0
- package/my-test-app/src/client/pages/Home.jsx +206 -0
- package/my-test-app/src/client/pages/PostForm.jsx +69 -0
- package/my-test-app/src/client/pages/Posts.jsx +59 -0
- package/my-test-app/src/client/pages/Profile.jsx +68 -0
- package/my-test-app/src/client/pages/Routing.jsx +207 -0
- package/my-test-app/src/client/pages/Testing.jsx +251 -0
- package/my-test-app/src/client/pages/Validation.jsx +210 -0
- package/my-test-app/src/controllers/AuthController.js +81 -0
- package/my-test-app/src/controllers/HomeController.js +17 -0
- package/my-test-app/src/controllers/PostController.js +86 -0
- package/my-test-app/src/controllers/ProfileController.js +66 -0
- package/my-test-app/src/helpers/api.js +24 -0
- package/my-test-app/src/main.jsx +9 -0
- package/my-test-app/src/middleware/auth.js +16 -0
- package/my-test-app/src/models/Post.js +63 -0
- package/my-test-app/src/models/User.js +42 -0
- package/my-test-app/src/routes/App.jsx +38 -0
- package/my-test-app/src/routes/api/auth.js +7 -0
- package/my-test-app/src/routes/api/posts.js +15 -0
- package/my-test-app/src/routes/api/profile.js +8 -0
- package/my-test-app/src/server/index.js +16 -0
- package/my-test-app/vite.config.js +18 -0
- package/package.json +61 -0
- package/scripts/test-app.sh +59 -0
- package/src/auth/jwt.js +195 -0
- package/src/auth/password.js +132 -0
- package/src/cli/colors.js +31 -0
- package/src/cli/dispatcher.js +168 -0
- package/src/cli/parser.js +91 -0
- package/src/client/context.js +4 -0
- package/src/client/hooks.js +50 -0
- package/src/client/index.js +3 -0
- package/src/client/router.js +184 -0
- package/src/commands/build.js +155 -0
- package/src/commands/dev.js +206 -0
- package/src/commands/help.js +84 -0
- package/src/commands/make-controller.js +36 -0
- package/src/commands/make-middleware.js +44 -0
- package/src/commands/make-migration.js +51 -0
- package/src/commands/make-model.js +38 -0
- package/src/commands/make-route.js +36 -0
- package/src/commands/make-seed.js +38 -0
- package/src/commands/migrate-fresh.js +32 -0
- package/src/commands/migrate-rollback.js +30 -0
- package/src/commands/migrate-status.js +41 -0
- package/src/commands/migrate.js +30 -0
- package/src/commands/seed.js +47 -0
- package/src/commands/start.js +69 -0
- package/src/commands/test.js +46 -0
- package/src/db/Grammar.js +125 -0
- package/src/db/QueryBuilder.js +476 -0
- package/src/db/adapters/neon.js +170 -0
- package/src/db/adapters/planetscale.js +146 -0
- package/src/db/adapters/postgres.js +166 -0
- package/src/db/adapters/sqlite.js +125 -0
- package/src/db/adapters/turso.js +165 -0
- package/src/db/index.js +156 -0
- package/src/db/migrator.js +250 -0
- package/src/db/seeder.js +124 -0
- package/src/index.js +12 -0
- package/src/scaffolding/index.js +152 -0
- package/src/server/body-parser.js +159 -0
- package/src/server/cors.js +63 -0
- package/src/server/default-entry.js +13 -0
- package/src/server/http.js +221 -0
- package/src/server/index.js +168 -0
- package/src/server/loader.js +128 -0
- package/src/server/router.js +281 -0
- package/src/server/static.js +139 -0
- package/src/validation/index.js +436 -0
- package/src/vite/config.js +49 -0
- package/stubs/controller.stub +48 -0
- package/stubs/middleware-auth.stub +29 -0
- package/stubs/middleware.stub +9 -0
- package/stubs/migration.stub +17 -0
- package/stubs/model.stub +77 -0
- package/stubs/route.stub +13 -0
- package/stubs/seed.stub +16 -0
- 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
|
+
}
|