@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/dist/index.js +87 -100
- package/dist/index.js.map +343 -358
- package/package.json +20 -25
- package/src/index.ts +1 -1
- package/src/middleware.ts +4 -4
- package/src/request.ts +11 -8
- package/src/router.ts +45 -30
- package/src/server.ts +98 -47
- package/src/utils.ts +9 -23
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 {
|
|
5
|
-
import {
|
|
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)
|
|
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('
|
|
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
|
-
}
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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)
|
|
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
|
-
}
|
|
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 ===
|
|
236
|
-
const { status, ...rest } = foundCallback
|
|
254
|
+
if (foundCallback.status === 404) {
|
|
255
|
+
const { status, ...rest } = await foundCallback
|
|
237
256
|
|
|
238
|
-
|
|
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
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
}
|
|
432
|
+
}
|
|
433
|
+
catch (error: any) {
|
|
386
434
|
return error
|
|
387
435
|
}
|
|
388
|
-
}
|
|
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
|
-
}
|
|
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 {
|
|
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
|
-
|
|
79
|
-
if (pathExists) {
|
|
80
|
-
const requestInstance = await import(filePath)
|
|
78
|
+
const reqInstance = await import(filePath)
|
|
81
79
|
|
|
82
|
-
|
|
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(
|
|
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
|
|