@stacksjs/router 0.64.6 → 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/dist/index.js +76 -101
- package/dist/index.js.map +155 -324
- 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 +43 -30
- package/src/server.ts +93 -43
- package/src/utils.ts +9 -21
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
|
|
|
@@ -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)
|
|
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
|
-
}
|
|
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
|
-
|
|
265
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
}
|
|
431
|
+
}
|
|
432
|
+
catch (error: any) {
|
|
386
433
|
return error
|
|
387
434
|
}
|
|
388
|
-
}
|
|
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
|
-
}
|
|
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 {
|
|
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
|
-
|
|
79
|
-
if (pathExists) {
|
|
80
|
-
const requestInstance = await import(filePath)
|
|
80
|
+
const reqInstance = await import(filePath)
|
|
81
81
|
|
|
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
|
|
82
|
+
return reqInstance.request
|
|
95
83
|
}
|
|
96
84
|
|
|
97
|
-
export async function extractDefaultRequest(
|
|
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
|
|