@stacksjs/router 0.64.6 → 0.66.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/src/server.ts CHANGED
@@ -1,9 +1,10 @@
1
+ import type { Model, Route, RouteParam, StatusCode } from '@stacksjs/types'
1
2
  import process from 'node:process'
3
+ import { handleError } from '@stacksjs/error-handling'
2
4
  import { log } from '@stacksjs/logging'
3
5
  import { getModelName } from '@stacksjs/orm'
4
- import { path, extname } from '@stacksjs/path'
5
- import { glob } from '@stacksjs/storage'
6
- import type { Model, Route, RouteParam, StatusCode } from '@stacksjs/types'
6
+ import { extname, path } from '@stacksjs/path'
7
+ import { globSync } from '@stacksjs/storage'
7
8
  import { route } from '.'
8
9
  import { middlewares } from './middleware'
9
10
  import { request as RequestParam } from './request'
@@ -19,12 +20,13 @@ interface Options {
19
20
  statusCode?: StatusCode
20
21
  }
21
22
 
22
- export async function serve(options: ServeOptions = {}) {
23
+ export async function serve(options: ServeOptions = {}): Promise<void> {
23
24
  const hostname = options.host || 'localhost'
24
25
  const port = options.port || 3000
25
26
  const development = options.debug ? true : process.env.APP_ENV !== 'production' && process.env.APP_ENV !== 'prod'
26
27
 
27
- if (options.timezone) process.env.TZ = options.timezone
28
+ if (options.timezone)
29
+ process.env.TZ = options.timezone
28
30
 
29
31
  Bun.serve({
30
32
  hostname,
@@ -39,7 +41,7 @@ export async function serve(options: ServeOptions = {}) {
39
41
  })
40
42
  }
41
43
 
42
- export async function serverResponse(req: Request, body: string) {
44
+ export async function serverResponse(req: Request, body: string): Promise<Response> {
43
45
  log.debug(`Incoming Request: ${req.method} ${req.url}`)
44
46
  log.debug(`Headers: ${JSON.stringify(req.headers)}`)
45
47
  log.debug(`Body: ${JSON.stringify(req.body)}`)
@@ -73,12 +75,12 @@ export async function serverResponse(req: Request, body: string) {
73
75
 
74
76
  if (!foundRoute) {
75
77
  // TODO: create a pretty 404 page
76
- return new Response('Pretty 404 page coming soon', {
78
+ return new Response('<html><body><h1>Page not found!</h1<pre></pre></body></html>', {
77
79
  status: 404,
78
80
  headers: {
79
81
  'Access-Control-Allow-Origin': '*',
80
82
  'Access-Control-Allow-Headers': '*',
81
- 'Content-Type': 'json',
83
+ 'Content-Type': 'application/json',
82
84
  },
83
85
  })
84
86
  }
@@ -87,7 +89,8 @@ export async function serverResponse(req: Request, body: string) {
87
89
 
88
90
  if (!body) {
89
91
  await addRouteQuery(url)
90
- } else {
92
+ }
93
+ else {
91
94
  await addBody(body)
92
95
  }
93
96
 
@@ -97,7 +100,7 @@ export async function serverResponse(req: Request, body: string) {
97
100
  return await execute(foundRoute, req, { statusCode: foundRoute?.statusCode })
98
101
  }
99
102
 
100
- function handleOptions(req: Request) {
103
+ function handleOptions() {
101
104
  return new Response(null, {
102
105
  status: 204,
103
106
  headers: {
@@ -117,14 +120,15 @@ function extractDynamicSegments(routePattern: string, path: string): RouteParam
117
120
  if (!match) {
118
121
  return null
119
122
  }
120
- const dynamicSegmentNames = [...routePattern.matchAll(/\{(\w+)\}/g)].map((m) => m[1])
123
+ const dynamicSegmentNames = [...routePattern.matchAll(/\{(\w+)\}/g)].map(m => m[1])
121
124
  const dynamicSegmentValues = match.slice(1) // First match is the whole string, so we slice it off
122
125
 
123
126
  const dynamicSegments: { [key: string]: string } = {}
124
127
  dynamicSegmentNames.forEach((name, index) => {
125
- dynamicSegments[name] = dynamicSegmentValues[index]
128
+ if (name && dynamicSegmentValues[index] !== undefined) {
129
+ dynamicSegments[name] = dynamicSegmentValues[index] // Ensure value is defined
130
+ }
126
131
  })
127
-
128
132
  return dynamicSegments
129
133
  }
130
134
 
@@ -136,9 +140,9 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
136
140
  const middlewarePayload = await executeMiddleware(foundRoute)
137
141
 
138
142
  if (
139
- middlewarePayload !== null &&
140
- typeof middlewarePayload === 'object' &&
141
- Object.keys(middlewarePayload).length > 0
143
+ middlewarePayload !== null
144
+ && typeof middlewarePayload === 'object'
145
+ && Object.keys(middlewarePayload).length > 0
142
146
  ) {
143
147
  const middlewareStatus = middlewarePayload.status
144
148
 
@@ -146,7 +150,7 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
146
150
 
147
151
  return new Response(JSON.stringify(payloadWithoutStatus), {
148
152
  headers: {
149
- 'Content-Type': 'json',
153
+ 'Content-Type': 'application/json',
150
154
  'Access-Control-Allow-Origin': '*',
151
155
  'Access-Control-Allow-Headers': '*',
152
156
  },
@@ -154,7 +158,8 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
154
158
  })
155
159
  }
156
160
 
157
- if (!statusCode) statusCode = 200
161
+ if (!statusCode)
162
+ statusCode = 200
158
163
 
159
164
  if (foundRoute?.method === 'GET' && (statusCode === 301 || statusCode === 302)) {
160
165
  const callback = String(foundCallback)
@@ -185,7 +190,9 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
185
190
  'Access-Control-Allow-Headers': '*',
186
191
  },
187
192
  })
188
- } catch (error) {
193
+ }
194
+ catch (error) {
195
+ handleError('Error reading the HTML file', error)
189
196
  return new Response('Error reading the HTML file', {
190
197
  status: 500,
191
198
  headers: {
@@ -196,15 +203,27 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
196
203
  }
197
204
  }
198
205
 
199
- if (isString(foundCallback))
206
+ if (isString(foundCallback)) {
200
207
  return new Response(foundCallback, {
201
208
  headers: {
202
- 'Content-Type': 'json',
209
+ 'Content-Type': 'application/json',
203
210
  'Access-Control-Allow-Origin': '*',
204
211
  'Access-Control-Allow-Headers': '*',
205
212
  },
206
213
  status: 200,
207
214
  })
215
+ }
216
+
217
+ if (foundCallback === undefined || foundCallback === null) {
218
+ return new Response('', {
219
+ headers: {
220
+ 'Content-Type': 'application/json',
221
+ 'Access-Control-Allow-Origin': '*',
222
+ 'Access-Control-Allow-Headers': '*',
223
+ },
224
+ status: 204,
225
+ })
226
+ }
208
227
 
209
228
  if (isFunction(foundCallback)) {
210
229
  const result = foundCallback()
@@ -220,11 +239,11 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
220
239
 
221
240
  if (isObject(foundCallback) && foundCallback.status) {
222
241
  if (foundCallback.status === 401) {
223
- const { status, ...rest } = foundCallback
242
+ const { status, ...rest } = await foundCallback
224
243
 
225
244
  return new Response(JSON.stringify(rest), {
226
245
  headers: {
227
- 'Content-Type': 'json',
246
+ 'Content-Type': 'application/json',
228
247
  'Access-Control-Allow-Origin': '*',
229
248
  'Access-Control-Allow-Headers': '*',
230
249
  },
@@ -232,12 +251,27 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
232
251
  })
233
252
  }
234
253
 
235
- if (foundCallback.status === 403) {
236
- const { status, ...rest } = foundCallback
254
+ if (foundCallback.status === 404) {
255
+ const { status, ...rest } = await foundCallback
237
256
 
238
- return new Response(JSON.stringify(rest), {
257
+ const { errors } = rest
258
+
259
+ return new Response(JSON.stringify(errors), {
239
260
  headers: {
240
- 'Content-Type': 'json',
261
+ 'Content-Type': 'application/json',
262
+ 'Access-Control-Allow-Origin': '*',
263
+ 'Access-Control-Allow-Headers': '*',
264
+ },
265
+ status: 404,
266
+ })
267
+ }
268
+
269
+ if (foundCallback.status === 403) {
270
+ const { status, ...rest } = await foundCallback
271
+ const { errors } = rest
272
+ return new Response(JSON.stringify(errors), {
273
+ headers: {
274
+ 'Content-Type': 'application/json',
241
275
  'Access-Control-Allow-Origin': '*',
242
276
  'Access-Control-Allow-Headers': '*',
243
277
  },
@@ -246,11 +280,12 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
246
280
  }
247
281
 
248
282
  if (foundCallback.status === 422) {
249
- const { status, ...rest } = foundCallback
283
+ const { status, ...rest } = await foundCallback
250
284
 
251
- return new Response(JSON.stringify(rest), {
285
+ const { errors } = rest
286
+ return new Response(JSON.stringify(errors), {
252
287
  headers: {
253
- 'Content-Type': 'json',
288
+ 'Content-Type': 'application/json',
254
289
  'Access-Control-Allow-Origin': '*',
255
290
  'Access-Control-Allow-Headers': '*',
256
291
  },
@@ -259,11 +294,24 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
259
294
  }
260
295
 
261
296
  if (foundCallback.status === 500) {
262
- const { status, ...rest } = foundCallback
297
+ const { status, ...rest } = await foundCallback
263
298
 
264
- return new Response(JSON.stringify(rest), {
265
- headers: { 'Content-Type': 'json', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': '*' },
266
- status: 500,
299
+ const { errors } = rest
300
+
301
+ const file = Bun.file(path.corePath('error-handling/src/views/500.html'))
302
+
303
+ return file.text().then((htmlContent) => {
304
+ // Replace the placeholder with the actual error message
305
+ const modifiedHtml = htmlContent.replace('{{ERROR_MESSAGE}}', errors)
306
+
307
+ return new Response(modifiedHtml, {
308
+ headers: {
309
+ 'Content-Type': 'text/html',
310
+ 'Access-Control-Allow-Origin': '*',
311
+ 'Access-Control-Allow-Headers': '*',
312
+ },
313
+ status: 500,
314
+ })
267
315
  })
268
316
  }
269
317
  }
@@ -271,7 +319,7 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
271
319
  if (isObject(foundCallback)) {
272
320
  return new Response(JSON.stringify(foundCallback), {
273
321
  headers: {
274
- 'Content-Type': 'json',
322
+ 'Content-Type': 'application/json',
275
323
  'Access-Control-Allow-Origin': '*',
276
324
  'Access-Control-Allow-Headers': '*',
277
325
  },
@@ -282,7 +330,7 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
282
330
  // If no known type matched, return a generic error.
283
331
  return new Response('Unknown callback type.', {
284
332
  headers: {
285
- 'Content-Type': 'json',
333
+ 'Content-Type': 'application/json',
286
334
  'Access-Control-Allow-Origin': '*',
287
335
  'Access-Control-Allow-Headers': '*',
288
336
  },
@@ -290,7 +338,7 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
290
338
  })
291
339
  }
292
340
 
293
- function noCache(response: Response) {
341
+ function noCache(response: Response): Response {
294
342
  response.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate')
295
343
  response.headers.set('Pragma', 'no-cache')
296
344
  response.headers.set('Expires', '0')
@@ -298,9 +346,8 @@ function noCache(response: Response) {
298
346
  return response
299
347
  }
300
348
 
301
- async function addRouteQuery(url: URL) {
302
- const modelFiles = glob.sync(path.userModelsPath('*.ts'))
303
-
349
+ async function addRouteQuery(url: URL): Promise<void> {
350
+ const modelFiles = globSync([path.userModelsPath('*.ts')], { absolute: true })
304
351
  for (const modelFile of modelFiles) {
305
352
  const model = (await import(modelFile)).default
306
353
  const modelName = getModelName(model, modelFile)
@@ -316,8 +363,8 @@ async function addRouteQuery(url: URL) {
316
363
  RequestParam.addQuery(url)
317
364
  }
318
365
 
319
- async function addBody(params: any) {
320
- const modelFiles = glob.sync(path.userModelsPath('*.ts'))
366
+ async function addBody(params: any): Promise<void> {
367
+ const modelFiles = globSync([path.userModelsPath('*.ts')], { absolute: true })
321
368
 
322
369
  for (const modelFile of modelFiles) {
323
370
  const model = (await import(modelFile)).default
@@ -335,7 +382,7 @@ async function addBody(params: any) {
335
382
  }
336
383
 
337
384
  async function addRouteParam(param: RouteParam): Promise<void> {
338
- const modelFiles = glob.sync(path.userModelsPath('*.ts'))
385
+ const modelFiles = globSync([path.userModelsPath('*.ts')], { absolute: true })
339
386
 
340
387
  for (const modelFile of modelFiles) {
341
388
  const model = (await import(modelFile)).default as Model
@@ -353,7 +400,7 @@ async function addRouteParam(param: RouteParam): Promise<void> {
353
400
  }
354
401
 
355
402
  async function addHeaders(headers: Headers): Promise<void> {
356
- const modelFiles = glob.sync(path.userModelsPath('*.ts'))
403
+ const modelFiles = globSync([path.userModelsPath('*.ts')], { absolute: true })
357
404
 
358
405
  for (const modelFile of modelFiles) {
359
406
  const model = (await import(modelFile)).default as Model
@@ -382,10 +429,12 @@ async function executeMiddleware(route: Route): Promise<any> {
382
429
 
383
430
  try {
384
431
  await middlewareInstance.handle()
385
- } catch (error: any) {
432
+ }
433
+ catch (error: any) {
386
434
  return error
387
435
  }
388
- } else {
436
+ }
437
+ else {
389
438
  for (const middlewareElement of middleware) {
390
439
  const middlewarePath = path.userMiddlewarePath(`${middlewareElement}.ts`)
391
440
 
@@ -393,7 +442,8 @@ async function executeMiddleware(route: Route): Promise<any> {
393
442
 
394
443
  try {
395
444
  await middlewareInstance.handle()
396
- } catch (error: any) {
445
+ }
446
+ catch (error: any) {
397
447
  return error
398
448
  }
399
449
  }
@@ -408,6 +458,7 @@ function isObjectNotEmpty(obj: object): boolean {
408
458
  return Object.keys(obj).length > 0
409
459
  }
410
460
 
461
+ // eslint-disable-next-line ts/no-unsafe-function-type
411
462
  function isFunction(val: unknown): val is Function {
412
463
  return typeof val === 'function'
413
464
  }
package/src/utils.ts CHANGED
@@ -1,12 +1,13 @@
1
- import { ok } from '@stacksjs/error-handling'
1
+ import type { ModelRequest, RequestInstance } from '@stacksjs/types'
2
+ import { type Ok, ok } from '@stacksjs/error-handling'
2
3
  import { path } from '@stacksjs/path'
3
- import { existsSync } from '@stacksjs/storage'
4
4
  import { camelCase } from '@stacksjs/strings'
5
5
  import { route } from './router'
6
6
 
7
- export async function listRoutes() {
7
+ export async function listRoutes(): Promise<Ok<string, any>> {
8
8
  const routeLists = await route.getRoutes()
9
9
 
10
+ // eslint-disable-next-line no-console
10
11
  console.table(routeLists)
11
12
 
12
13
  return ok('Successfully listed routes!')
@@ -60,7 +61,7 @@ export function extractDynamicAction(action: string): string | undefined {
60
61
  return match ? match[1] : ''
61
62
  }
62
63
 
63
- export async function extractModelRequest(action: string) {
64
+ export async function extractModelRequest(action: string): Promise<RequestInstance | null> {
64
65
  const extractedModel = extractModelFromAction(action)
65
66
  const lowerCaseModel = camelCase(extractedModel)
66
67
  const requestPath = path.frameworkPath(`requests/${extractedModel}Request.ts`)
@@ -70,31 +71,16 @@ export async function extractModelRequest(action: string) {
70
71
  return requestInstance[requestIndex]
71
72
  }
72
73
 
73
- export async function findRequestInstance(requestInstance: string) {
74
+ export async function findRequestInstance(requestInstance: string): Promise<ModelRequest> {
74
75
  const frameworkDirectory = path.storagePath('framework/requests')
75
76
  const filePath = path.join(frameworkDirectory, `${requestInstance}.ts`)
76
- const pathExists = await existsSync(filePath)
77
77
 
78
- // Check if the directory exists
79
- if (pathExists) {
80
- const requestInstance = await import(filePath)
78
+ const reqInstance = await import(filePath)
81
79
 
82
- return requestInstance.request
83
- }
84
-
85
- const defaultRequestPath = path.storagePath('framework/core/router/src/request.ts')
86
- const fileExists = await existsSync(defaultRequestPath)
87
-
88
- if (fileExists) {
89
- const requestInstance = await import(defaultRequestPath)
90
-
91
- return requestInstance.request
92
- }
93
-
94
- return null
80
+ return reqInstance.request
95
81
  }
96
82
 
97
- export async function extractDefaultRequest(action: string) {
83
+ export async function extractDefaultRequest(): Promise<RequestInstance> {
98
84
  const requestPath = path.frameworkPath(`core/router/src/request.ts`)
99
85
  const requestInstance = await import(requestPath)
100
86