instaserve 1.1.3 → 1.1.6

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/README.md CHANGED
@@ -107,6 +107,39 @@ export default {
107
107
  }
108
108
  ```
109
109
 
110
+ ### Method-Specific Routes
111
+
112
+ Routes can be defined with HTTP method prefixes to handle different methods on the same path. Supported methods: `GET`, `POST`, `PUT`, `DELETE`.
113
+
114
+ ```javascript
115
+ export default {
116
+ // Method-specific routes
117
+ 'POST /users': (req, res, data) => {
118
+ return { message: 'Create user', data }
119
+ },
120
+
121
+ 'GET /users': (req, res, data) => {
122
+ return { message: 'Get users' }
123
+ },
124
+
125
+ 'PUT /users': (req, res, data) => {
126
+ return { message: 'Update user', data }
127
+ },
128
+
129
+ 'DELETE /users': (req, res, data) => {
130
+ return { message: 'Delete user', data }
131
+ },
132
+
133
+ // Path-only routes still work (backward compatible)
134
+ // These match any HTTP method
135
+ hello: (req, res, data) => {
136
+ return { message: 'Hello World' }
137
+ }
138
+ }
139
+ ```
140
+
141
+ Method-specific routes take precedence over path-only routes. If no method-specific route matches, the server falls back to path-only route matching.
142
+
110
143
  ### Special Routes (Middleware)
111
144
 
112
145
  Routes starting with `_` are middleware functions that run on **every request** before the main route handler. They are useful for:
@@ -152,6 +185,25 @@ Each route function receives:
152
185
  - URL query parameters
153
186
  - Form data
154
187
 
188
+ ### Returning Status Codes
189
+
190
+ Routes can return a 3-digit number (100-999) to set the HTTP status code with an empty response body:
191
+
192
+ ```javascript
193
+ export default {
194
+ 'GET /notfound': () => 404,
195
+ 'GET /unauthorized': () => 401,
196
+ 'GET /forbidden': () => 403,
197
+ 'GET /teapot': () => 418, // I'm a teapot
198
+ 'GET /created': () => 201
199
+ }
200
+ ```
201
+
202
+ Routes can also return:
203
+ - **Strings** - Sent as plain text response
204
+ - **Objects** - Automatically serialized as JSON
205
+ - **Status codes** - 3-digit numbers (100-999) set HTTP status with empty body
206
+
155
207
  ### Example Routes File
156
208
 
157
209
  ```javascript
@@ -163,7 +215,19 @@ export default {
163
215
  return false // Continue to next route
164
216
  },
165
217
 
166
- // API endpoint
218
+ // Method-specific routes
219
+ 'POST /api/users': (req, res, data) => {
220
+ return { status: 'created', data }
221
+ },
222
+
223
+ 'GET /api/users': (req, res, data) => {
224
+ return { status: 'ok', users: [] }
225
+ },
226
+
227
+ 'GET /api/notfound': () => 404,
228
+ 'GET /api/unauthorized': () => 401,
229
+
230
+ // Path-only route (matches any method)
167
231
  api: (req, res, data) => {
168
232
  return { status: 'ok', data }
169
233
  },
package/instaserve CHANGED
@@ -27,9 +27,9 @@ const args = process.argv.slice(2)
27
27
 
28
28
  // Handle generate-routes command
29
29
  if (args[0] === 'generate-routes') {
30
- const routesFile = './routes.js'
30
+ const routesFile = path.resolve(process.cwd(), './routes.js')
31
31
  if (fs.existsSync(routesFile)) {
32
- console.error(chalk.red(`Error: ${routesFile} already exists`))
32
+ console.error(chalk.red(`Error: routes.js already exists`))
33
33
  process.exit(1)
34
34
  }
35
35
 
@@ -59,12 +59,21 @@ if (args[0] === 'generate-routes') {
59
59
 
60
60
  api: (req, res, data) => {
61
61
  return { message: 'API endpoint', data }
62
+ },
63
+
64
+ // Method-specific routes (POST, GET, PUT, DELETE)
65
+ 'POST /users': (req, res, data) => {
66
+ return { message: 'Create user', data }
67
+ },
68
+
69
+ 'GET /users': (req, res, data) => {
70
+ return { message: 'Get users' }
62
71
  }
63
72
  }
64
73
  `
65
74
 
66
75
  fs.writeFileSync(routesFile, sampleRoutes)
67
- console.log(chalk.green(`✓ Created ${routesFile}`))
76
+ console.log(chalk.green(`✓ Created routes.js`))
68
77
  process.exit(0)
69
78
  }
70
79
 
@@ -87,6 +96,7 @@ for (let i = 0; i < args.length; i++) {
87
96
 
88
97
  // Load routes file
89
98
  let routes = {}
99
+ let routesFilePath = null
90
100
  const routesFileParam = params.api || './routes.js'
91
101
  const routesFileSpecified = !!params.api
92
102
 
@@ -117,10 +127,11 @@ if (fs.existsSync(routesFile)) {
117
127
  process.exit(1)
118
128
  }
119
129
  }
130
+ routesFilePath = routesFile
120
131
  } catch (e) {
121
132
  console.error(chalk.red(`Error: Could not load routes file "${routesFileParam}": ${e.message}`))
122
133
  process.exit(1)
123
134
  }
124
135
  }
125
136
 
126
- server(routes)
137
+ server(routes, params.port ? parseInt(params.port) : undefined, params.ip, routesFilePath)
package/module.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import http from 'node:http'
2
2
  import https from 'node:https'
3
3
  import fs from 'node:fs'
4
+ import path from 'node:path'
4
5
 
5
6
  const args = process.argv.slice(2)
6
7
  const params = {}
@@ -18,9 +19,9 @@ for (let i = 0; i < args.length; i++) {
18
19
  }
19
20
  }
20
21
 
21
- function public_file(r, s) {
22
+ function public_file(r, s, publicDir) {
22
23
  if (r.url == '/') r.url = '/index.html'
23
- const fn = `${params.public || './public'}${r.url.replace(/\.\./g, '')}`
24
+ const fn = path.resolve(publicDir, r.url.replace(/\.\./g, '').replace(/^\//, ''))
24
25
  if (fs.existsSync(fn)) {
25
26
  const content = fs.readFileSync(fn, 'utf-8')
26
27
  if (fn.match(/.js$/)) {
@@ -34,8 +35,11 @@ function public_file(r, s) {
34
35
  return false // Indicate no file was served
35
36
  }
36
37
 
37
- export default async function (routes, port = params.port || 3000, ip = params.ip || '127.0.0.1') {
38
- const publicDir = params.public || './public'
38
+ export default async function (routes, port = params.port || 3000, ip = params.ip || '127.0.0.1', routesFilePath = null) {
39
+ const publicDirParam = params.public || './public'
40
+ const publicDir = path.isAbsolute(publicDirParam)
41
+ ? publicDirParam
42
+ : path.resolve(process.cwd(), publicDirParam)
39
43
  if (publicDir.includes('..')) {
40
44
  throw new Error('Public directory path cannot contain ".."')
41
45
  }
@@ -47,6 +51,21 @@ export default async function (routes, port = params.port || 3000, ip = params.i
47
51
  let sdata = '', rrurl = r.url || ''
48
52
  let responseSent = false
49
53
 
54
+ // Helper to check if value is a 3-digit HTTP status code
55
+ const isStatusCode = (val) => {
56
+ return typeof val === 'number' && val >= 100 && val <= 999 && Math.floor(val) === val
57
+ }
58
+
59
+ // Helper to send response, handling status codes
60
+ const sendResponse = (result) => {
61
+ if (isStatusCode(result)) {
62
+ s.writeHead(result)
63
+ s.end()
64
+ } else {
65
+ s.end(typeof result === 'string' ? result : JSON.stringify(result))
66
+ }
67
+ }
68
+
50
69
  r.on('data', (s) => sdata += s.toString().trim())
51
70
  r.on('end', (x) => {
52
71
  try {
@@ -64,7 +83,7 @@ export default async function (routes, port = params.port || 3000, ip = params.i
64
83
  const result = routes[k](r, s, data)
65
84
  if (result && !responseSent) {
66
85
  responseSent = true
67
- s.end(typeof result === 'string' ? result : JSON.stringify(result))
86
+ sendResponse(result)
68
87
  }
69
88
  return result
70
89
  })
@@ -73,17 +92,47 @@ export default async function (routes, port = params.port || 3000, ip = params.i
73
92
  if(responseSent || s.writableEnded) return
74
93
 
75
94
  // Try to serve public file
76
- if(public_file(r, s)) {
95
+ if(public_file(r, s, publicDir)) {
77
96
  responseSent = true
78
97
  return
79
98
  }
80
99
 
81
- const url = rrurl.split('/')[1].split('?')[0]
82
- if (routes[url]) {
83
- const resp = routes[url](r, s, data)
100
+ const urlParts = rrurl.split('/')
101
+ const url = urlParts.length > 1 ? urlParts[1].split('?')[0] : ''
102
+ const method = (r.method || 'GET').toUpperCase()
103
+
104
+ // Try method-specific route first (e.g., "POST /endp", "GET /")
105
+ const methodRoute = url ? `${method} /${url}` : `${method} /`
106
+ let routeHandler = routes[methodRoute]
107
+
108
+ // If no exact match, check if any method-specific route exists for this path
109
+ if (!routeHandler) {
110
+ const methods = ['GET', 'POST', 'PUT', 'DELETE']
111
+ const pathRoute = url ? `/${url}` : `/`
112
+ const hasMethodSpecificRoute = methods.some(m => {
113
+ const checkRoute = url ? `${m} /${url}` : `${m} /`
114
+ return routes[checkRoute] !== undefined
115
+ })
116
+
117
+ // If method-specific route exists but for different method, return 405
118
+ if (hasMethodSpecificRoute) {
119
+ if (!responseSent && !s.writableEnded) {
120
+ responseSent = true
121
+ s.writeHead(405)
122
+ s.end()
123
+ }
124
+ return
125
+ }
126
+
127
+ // Fall back to path-only route (backward compatible)
128
+ routeHandler = routes[url]
129
+ }
130
+
131
+ if (routeHandler) {
132
+ const resp = routeHandler(r, s, data)
84
133
  if (!responseSent && !s.writableEnded) {
85
134
  responseSent = true
86
- s.end(typeof resp === 'string' ? resp:JSON.stringify(resp))
135
+ sendResponse(resp)
87
136
  }
88
137
  return
89
138
  }
@@ -105,8 +154,8 @@ export default async function (routes, port = params.port || 3000, ip = params.i
105
154
 
106
155
  let server
107
156
  if (params.secure) {
108
- const certPath = './cert.pem'
109
- const keyPath = './key.pem'
157
+ const certPath = path.resolve(process.cwd(), './cert.pem')
158
+ const keyPath = path.resolve(process.cwd(), './key.pem')
110
159
 
111
160
  if (!fs.existsSync(certPath) || !fs.existsSync(keyPath)) {
112
161
  throw new Error('Certificate files not found. Run ./generate-certs.sh first.')
@@ -125,7 +174,10 @@ export default async function (routes, port = params.port || 3000, ip = params.i
125
174
  server.listen(port || 3000, ip || '')
126
175
 
127
176
  const protocol = params.secure ? 'https' : 'http'
128
- console.log(`started on: ${protocol}://${(process.env.ip || ip)}:${(process.env.port || port)}, public: ${publicDir}, ${Object.keys(routes).length > 0 ? `using routes: ${Object.keys(routes)}` : 'not using routes'}`)
177
+ const routesInfo = Object.keys(routes).length > 0
178
+ ? (routesFilePath ? `using routes: ${Object.keys(routes)} (${routesFilePath})` : `using routes: ${Object.keys(routes)}`)
179
+ : 'not using routes'
180
+ console.log(`started on: ${protocol}://${(process.env.ip || ip)}:${(process.env.port || port)}, public: ${publicDir}, ${routesInfo}`)
129
181
 
130
182
  return {
131
183
  routes: routes,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instaserve",
3
- "version": "1.1.3",
3
+ "version": "1.1.6",
4
4
  "description": "Instant web stack",
5
5
  "main": "module.mjs",
6
6
  "bin": "./instaserve",
package/test.js ADDED
@@ -0,0 +1,28 @@
1
+ export default {
2
+ // Middleware functions (prefixed with _) run on every request
3
+ // Return false to continue processing, or a value to use as response
4
+
5
+ // Example: Log all requests
6
+ _log: (req, res, data) => {
7
+ console.log(`${req.method} ${req.url}`)
8
+ return false // Continue to next middleware or route
9
+ },
10
+
11
+ // Example: Basic authentication (commented out)
12
+ // _auth: (req, res, data) => {
13
+ // if (!data.token) {
14
+ // res.writeHead(401)
15
+ // return 'Unauthorized'
16
+ // }
17
+ // return false // Continue if authorized
18
+ // },
19
+
20
+ // Regular route handlers
21
+ hello: (req, res, data) => {
22
+ return { message: 'Hello World' }
23
+ },
24
+
25
+ api: (req, res, data) => {
26
+ return { message: 'API endpoint', data }
27
+ }
28
+ }