@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,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'
|