@stacksjs/router 0.64.5 → 0.65.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
 
@@ -133,12 +137,21 @@ type CallbackWithStatus = Route['callback'] & { status: number }
133
137
  async function execute(foundRoute: Route, req: Request, { statusCode }: Options) {
134
138
  const foundCallback: CallbackWithStatus = await route.resolveCallback(foundRoute.callback)
135
139
 
140
+ // return new Response(`<html><body><h1>Error</h1><p>${foundCallback}</p><pre></pre></body></html>`, {
141
+ // headers: {
142
+ // 'Content-Type': 'text/html',
143
+ // 'Access-Control-Allow-Origin': '*',
144
+ // 'Access-Control-Allow-Headers': '*',
145
+ // },
146
+ // status: 500,
147
+ // })
148
+
136
149
  const middlewarePayload = await executeMiddleware(foundRoute)
137
150
 
138
151
  if (
139
- middlewarePayload !== null &&
140
- typeof middlewarePayload === 'object' &&
141
- Object.keys(middlewarePayload).length > 0
152
+ middlewarePayload !== null
153
+ && typeof middlewarePayload === 'object'
154
+ && Object.keys(middlewarePayload).length > 0
142
155
  ) {
143
156
  const middlewareStatus = middlewarePayload.status
144
157
 
@@ -146,7 +159,7 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
146
159
 
147
160
  return new Response(JSON.stringify(payloadWithoutStatus), {
148
161
  headers: {
149
- 'Content-Type': 'json',
162
+ 'Content-Type': 'application/json',
150
163
  'Access-Control-Allow-Origin': '*',
151
164
  'Access-Control-Allow-Headers': '*',
152
165
  },
@@ -154,7 +167,8 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
154
167
  })
155
168
  }
156
169
 
157
- if (!statusCode) statusCode = 200
170
+ if (!statusCode)
171
+ statusCode = 200
158
172
 
159
173
  if (foundRoute?.method === 'GET' && (statusCode === 301 || statusCode === 302)) {
160
174
  const callback = String(foundCallback)
@@ -185,7 +199,9 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
185
199
  'Access-Control-Allow-Headers': '*',
186
200
  },
187
201
  })
188
- } catch (error) {
202
+ }
203
+ catch (error) {
204
+ handleError('Error reading the HTML file', error)
189
205
  return new Response('Error reading the HTML file', {
190
206
  status: 500,
191
207
  headers: {
@@ -196,15 +212,27 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
196
212
  }
197
213
  }
198
214
 
199
- if (isString(foundCallback))
215
+ if (isString(foundCallback)) {
200
216
  return new Response(foundCallback, {
201
217
  headers: {
202
- 'Content-Type': 'json',
218
+ 'Content-Type': 'application/json',
203
219
  'Access-Control-Allow-Origin': '*',
204
220
  'Access-Control-Allow-Headers': '*',
205
221
  },
206
222
  status: 200,
207
223
  })
224
+ }
225
+
226
+ if (foundCallback === undefined || foundCallback === null) {
227
+ return new Response('', {
228
+ headers: {
229
+ 'Content-Type': 'application/json',
230
+ 'Access-Control-Allow-Origin': '*',
231
+ 'Access-Control-Allow-Headers': '*',
232
+ },
233
+ status: 204,
234
+ })
235
+ }
208
236
 
209
237
  if (isFunction(foundCallback)) {
210
238
  const result = foundCallback()
@@ -220,11 +248,11 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
220
248
 
221
249
  if (isObject(foundCallback) && foundCallback.status) {
222
250
  if (foundCallback.status === 401) {
223
- const { status, ...rest } = foundCallback
251
+ const { status, ...rest } = await foundCallback
224
252
 
225
253
  return new Response(JSON.stringify(rest), {
226
254
  headers: {
227
- 'Content-Type': 'json',
255
+ 'Content-Type': 'application/json',
228
256
  'Access-Control-Allow-Origin': '*',
229
257
  'Access-Control-Allow-Headers': '*',
230
258
  },
@@ -232,12 +260,26 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
232
260
  })
233
261
  }
234
262
 
263
+ if (foundCallback.status === 404) {
264
+ const { status, ...rest } = await foundCallback
265
+
266
+ const { errors } = rest
267
+ return new Response(JSON.stringify(errors), {
268
+ headers: {
269
+ 'Content-Type': 'application/json',
270
+ 'Access-Control-Allow-Origin': '*',
271
+ 'Access-Control-Allow-Headers': '*',
272
+ },
273
+ status: 404,
274
+ })
275
+ }
276
+
235
277
  if (foundCallback.status === 403) {
236
- const { status, ...rest } = foundCallback
278
+ const { status, ...rest } = await foundCallback
237
279
 
238
280
  return new Response(JSON.stringify(rest), {
239
281
  headers: {
240
- 'Content-Type': 'json',
282
+ 'Content-Type': 'application/json',
241
283
  'Access-Control-Allow-Origin': '*',
242
284
  'Access-Control-Allow-Headers': '*',
243
285
  },
@@ -246,11 +288,11 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
246
288
  }
247
289
 
248
290
  if (foundCallback.status === 422) {
249
- const { status, ...rest } = foundCallback
291
+ const { status, ...rest } = await foundCallback
250
292
 
251
293
  return new Response(JSON.stringify(rest), {
252
294
  headers: {
253
- 'Content-Type': 'json',
295
+ 'Content-Type': 'application/json',
254
296
  'Access-Control-Allow-Origin': '*',
255
297
  'Access-Control-Allow-Headers': '*',
256
298
  },
@@ -259,10 +301,15 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
259
301
  }
260
302
 
261
303
  if (foundCallback.status === 500) {
262
- const { status, ...rest } = foundCallback
304
+ const { status, ...rest } = await foundCallback
263
305
 
264
- return new Response(JSON.stringify(rest), {
265
- headers: { 'Content-Type': 'json', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': '*' },
306
+ const { errors } = rest
307
+ return new Response(`<html><body><p>${errors}</p><pre></pre></body></html>`, {
308
+ headers: {
309
+ 'Content-Type': 'text/html',
310
+ 'Access-Control-Allow-Origin': '*',
311
+ 'Access-Control-Allow-Headers': '*',
312
+ },
266
313
  status: 500,
267
314
  })
268
315
  }
@@ -271,7 +318,7 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
271
318
  if (isObject(foundCallback)) {
272
319
  return new Response(JSON.stringify(foundCallback), {
273
320
  headers: {
274
- 'Content-Type': 'json',
321
+ 'Content-Type': 'application/json',
275
322
  'Access-Control-Allow-Origin': '*',
276
323
  'Access-Control-Allow-Headers': '*',
277
324
  },
@@ -282,7 +329,7 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
282
329
  // If no known type matched, return a generic error.
283
330
  return new Response('Unknown callback type.', {
284
331
  headers: {
285
- 'Content-Type': 'json',
332
+ 'Content-Type': 'application/json',
286
333
  'Access-Control-Allow-Origin': '*',
287
334
  'Access-Control-Allow-Headers': '*',
288
335
  },
@@ -290,7 +337,7 @@ async function execute(foundRoute: Route, req: Request, { statusCode }: Options)
290
337
  })
291
338
  }
292
339
 
293
- function noCache(response: Response) {
340
+ function noCache(response: Response): Response {
294
341
  response.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate')
295
342
  response.headers.set('Pragma', 'no-cache')
296
343
  response.headers.set('Expires', '0')
@@ -298,9 +345,8 @@ function noCache(response: Response) {
298
345
  return response
299
346
  }
300
347
 
301
- async function addRouteQuery(url: URL) {
302
- const modelFiles = glob.sync(path.userModelsPath('*.ts'))
303
-
348
+ async function addRouteQuery(url: URL): Promise<void> {
349
+ const modelFiles = globSync([path.userModelsPath('*.ts')], { absolute: true })
304
350
  for (const modelFile of modelFiles) {
305
351
  const model = (await import(modelFile)).default
306
352
  const modelName = getModelName(model, modelFile)
@@ -316,8 +362,8 @@ async function addRouteQuery(url: URL) {
316
362
  RequestParam.addQuery(url)
317
363
  }
318
364
 
319
- async function addBody(params: any) {
320
- const modelFiles = glob.sync(path.userModelsPath('*.ts'))
365
+ async function addBody(params: any): Promise<void> {
366
+ const modelFiles = globSync([path.userModelsPath('*.ts')], { absolute: true })
321
367
 
322
368
  for (const modelFile of modelFiles) {
323
369
  const model = (await import(modelFile)).default
@@ -335,7 +381,7 @@ async function addBody(params: any) {
335
381
  }
336
382
 
337
383
  async function addRouteParam(param: RouteParam): Promise<void> {
338
- const modelFiles = glob.sync(path.userModelsPath('*.ts'))
384
+ const modelFiles = globSync([path.userModelsPath('*.ts')], { absolute: true })
339
385
 
340
386
  for (const modelFile of modelFiles) {
341
387
  const model = (await import(modelFile)).default as Model
@@ -353,7 +399,7 @@ async function addRouteParam(param: RouteParam): Promise<void> {
353
399
  }
354
400
 
355
401
  async function addHeaders(headers: Headers): Promise<void> {
356
- const modelFiles = glob.sync(path.userModelsPath('*.ts'))
402
+ const modelFiles = globSync([path.userModelsPath('*.ts')], { absolute: true })
357
403
 
358
404
  for (const modelFile of modelFiles) {
359
405
  const model = (await import(modelFile)).default as Model
@@ -382,10 +428,12 @@ async function executeMiddleware(route: Route): Promise<any> {
382
428
 
383
429
  try {
384
430
  await middlewareInstance.handle()
385
- } catch (error: any) {
431
+ }
432
+ catch (error: any) {
386
433
  return error
387
434
  }
388
- } else {
435
+ }
436
+ else {
389
437
  for (const middlewareElement of middleware) {
390
438
  const middlewarePath = path.userMiddlewarePath(`${middlewareElement}.ts`)
391
439
 
@@ -393,7 +441,8 @@ async function executeMiddleware(route: Route): Promise<any> {
393
441
 
394
442
  try {
395
443
  await middlewareInstance.handle()
396
- } catch (error: any) {
444
+ }
445
+ catch (error: any) {
397
446
  return error
398
447
  }
399
448
  }
@@ -408,6 +457,7 @@ function isObjectNotEmpty(obj: object): boolean {
408
457
  return Object.keys(obj).length > 0
409
458
  }
410
459
 
460
+ // eslint-disable-next-line ts/no-unsafe-function-type
411
461
  function isFunction(val: unknown): val is Function {
412
462
  return typeof val === 'function'
413
463
  }
package/src/utils.ts CHANGED
@@ -1,12 +1,14 @@
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
4
  import { existsSync } from '@stacksjs/storage'
4
5
  import { camelCase } from '@stacksjs/strings'
5
6
  import { route } from './router'
6
7
 
7
- export async function listRoutes() {
8
+ export async function listRoutes(): Promise<Ok<string, any>> {
8
9
  const routeLists = await route.getRoutes()
9
10
 
11
+ // eslint-disable-next-line no-console
10
12
  console.table(routeLists)
11
13
 
12
14
  return ok('Successfully listed routes!')
@@ -60,7 +62,7 @@ export function extractDynamicAction(action: string): string | undefined {
60
62
  return match ? match[1] : ''
61
63
  }
62
64
 
63
- export async function extractModelRequest(action: string) {
65
+ export async function extractModelRequest(action: string): Promise<RequestInstance | null> {
64
66
  const extractedModel = extractModelFromAction(action)
65
67
  const lowerCaseModel = camelCase(extractedModel)
66
68
  const requestPath = path.frameworkPath(`requests/${extractedModel}Request.ts`)
@@ -70,31 +72,17 @@ export async function extractModelRequest(action: string) {
70
72
  return requestInstance[requestIndex]
71
73
  }
72
74
 
73
- export async function findRequestInstance(requestInstance: string) {
75
+ export async function findRequestInstance(requestInstance: string): Promise<ModelRequest> {
74
76
  const frameworkDirectory = path.storagePath('framework/requests')
75
77
  const filePath = path.join(frameworkDirectory, `${requestInstance}.ts`)
76
78
  const pathExists = await existsSync(filePath)
77
79
 
78
- // Check if the directory exists
79
- if (pathExists) {
80
- const requestInstance = await import(filePath)
80
+ const reqInstance = await import(filePath)
81
81
 
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
82
+ return reqInstance.request
95
83
  }
96
84
 
97
- export async function extractDefaultRequest(action: string) {
85
+ export async function extractDefaultRequest(): Promise<RequestInstance> {
98
86
  const requestPath = path.frameworkPath(`core/router/src/request.ts`)
99
87
  const requestInstance = await import(requestPath)
100
88