@stacksjs/router 0.66.0 → 0.68.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 DELETED
@@ -1,468 +0,0 @@
1
- import type { Model, Route, RouteParam, StatusCode } from '@stacksjs/types'
2
- import process from 'node:process'
3
- import { handleError } from '@stacksjs/error-handling'
4
- import { log } from '@stacksjs/logging'
5
- import { getModelName } from '@stacksjs/orm'
6
- import { extname, path } from '@stacksjs/path'
7
- import { globSync } from '@stacksjs/storage'
8
- import { route } from '.'
9
- import { middlewares } from './middleware'
10
- import { request as RequestParam } from './request'
11
-
12
- interface ServeOptions {
13
- host?: string
14
- port?: number
15
- debug?: boolean
16
- timezone?: string
17
- }
18
-
19
- interface Options {
20
- statusCode?: StatusCode
21
- }
22
-
23
- export async function serve(options: ServeOptions = {}): Promise<void> {
24
- const hostname = options.host || 'localhost'
25
- const port = options.port || 3000
26
- const development = options.debug ? true : process.env.APP_ENV !== 'production' && process.env.APP_ENV !== 'prod'
27
-
28
- if (options.timezone)
29
- process.env.TZ = options.timezone
30
-
31
- Bun.serve({
32
- hostname,
33
- port,
34
- development,
35
-
36
- async fetch(req: Request) {
37
- const reqBody = await req.text()
38
-
39
- return await serverResponse(req, reqBody)
40
- },
41
- })
42
- }
43
-
44
- export async function serverResponse(req: Request, body: string): Promise<Response> {
45
- log.debug(`Incoming Request: ${req.method} ${req.url}`)
46
- log.debug(`Headers: ${JSON.stringify(req.headers)}`)
47
- log.debug(`Body: ${JSON.stringify(req.body)}`)
48
- // log.debug(`Query: ${JSON.stringify(req.query)}`)
49
- // log.debug(`Params: ${JSON.stringify(req.params)}`)
50
- // log.debug(`Cookies: ${JSON.stringify(req.cookies)}`)
51
-
52
- // Trim trailing slash from the URL if it's not the root '/'
53
- // This automatically allows for route definitions, like
54
- // '/about' and '/about/' to be treated as the same
55
- const trimmedUrl = req.url.endsWith('/') && req.url.length > 1 ? req.url.slice(0, -1) : req.url
56
- const url = new URL(trimmedUrl)
57
- const routesList: Route[] = await route.getRoutes()
58
-
59
- log.info(`Routes List: ${JSON.stringify(routesList)}`)
60
- log.info(`URL: ${JSON.stringify(url)}`)
61
-
62
- if (req.method === 'OPTIONS') {
63
- return handleOptions(req)
64
- }
65
-
66
- const foundRoute: Route | undefined = routesList
67
- .filter((route: Route) => {
68
- const pattern = new RegExp(`^${route.uri.replace(/\{(\w+)\}/g, '(\\w+)')}$`)
69
-
70
- return pattern.test(url.pathname)
71
- })
72
- .find((route: Route) => route.method === req.method)
73
-
74
- log.info(`Found Route: ${JSON.stringify(foundRoute)}`)
75
-
76
- if (!foundRoute) {
77
- // TODO: create a pretty 404 page
78
- return new Response('<html><body><h1>Page not found!</h1<pre></pre></body></html>', {
79
- status: 404,
80
- headers: {
81
- 'Access-Control-Allow-Origin': '*',
82
- 'Access-Control-Allow-Headers': '*',
83
- 'Content-Type': 'application/json',
84
- },
85
- })
86
- }
87
-
88
- const routeParams = extractDynamicSegments(foundRoute.uri, url.pathname)
89
-
90
- if (!body) {
91
- await addRouteQuery(url)
92
- }
93
- else {
94
- await addBody(body)
95
- }
96
-
97
- await addRouteParam(routeParams)
98
- await addHeaders(req.headers)
99
-
100
- return await execute(foundRoute, req, { statusCode: foundRoute?.statusCode })
101
- }
102
-
103
- function handleOptions() {
104
- return new Response(null, {
105
- status: 204,
106
- headers: {
107
- 'Access-Control-Allow-Origin': '*',
108
- 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
109
- 'Access-Control-Allow-Headers':
110
- 'Content-Type, Authorization, Access-Control-Allow-Headers, Access-Control-Allow-Origin, Accept',
111
- 'Access-Control-Max-Age': '86400', // Cache the preflight response for a day
112
- },
113
- })
114
- }
115
-
116
- function extractDynamicSegments(routePattern: string, path: string): RouteParam {
117
- const regexPattern = new RegExp(`^${routePattern.replace(/\{(\w+)\}/g, '(\\w+)')}$`)
118
- const match = path.match(regexPattern)
119
-
120
- if (!match) {
121
- return null
122
- }
123
- const dynamicSegmentNames = [...routePattern.matchAll(/\{(\w+)\}/g)].map(m => m[1])
124
- const dynamicSegmentValues = match.slice(1) // First match is the whole string, so we slice it off
125
-
126
- const dynamicSegments: { [key: string]: string } = {}
127
- dynamicSegmentNames.forEach((name, index) => {
128
- if (name && dynamicSegmentValues[index] !== undefined) {
129
- dynamicSegments[name] = dynamicSegmentValues[index] // Ensure value is defined
130
- }
131
- })
132
- return dynamicSegments
133
- }
134
-
135
- type CallbackWithStatus = Route['callback'] & { status: number }
136
-
137
- async function execute(foundRoute: Route, req: Request, { statusCode }: Options) {
138
- const foundCallback: CallbackWithStatus = await route.resolveCallback(foundRoute.callback)
139
-
140
- const middlewarePayload = await executeMiddleware(foundRoute)
141
-
142
- if (
143
- middlewarePayload !== null
144
- && typeof middlewarePayload === 'object'
145
- && Object.keys(middlewarePayload).length > 0
146
- ) {
147
- const middlewareStatus = middlewarePayload.status
148
-
149
- const { status, ...payloadWithoutStatus } = middlewarePayload
150
-
151
- return new Response(JSON.stringify(payloadWithoutStatus), {
152
- headers: {
153
- 'Content-Type': 'application/json',
154
- 'Access-Control-Allow-Origin': '*',
155
- 'Access-Control-Allow-Headers': '*',
156
- },
157
- status: middlewareStatus || 401,
158
- })
159
- }
160
-
161
- if (!statusCode)
162
- statusCode = 200
163
-
164
- if (foundRoute?.method === 'GET' && (statusCode === 301 || statusCode === 302)) {
165
- const callback = String(foundCallback)
166
- const response = Response.redirect(callback, statusCode)
167
-
168
- return noCache(response)
169
- }
170
-
171
- if (foundRoute?.method !== req.method) {
172
- return new Response('Method not allowed', {
173
- status: 405,
174
- headers: {
175
- 'Access-Control-Allow-Origin': '*',
176
- 'Access-Control-Allow-Headers': '*',
177
- },
178
- })
179
- }
180
-
181
- // Check if it's a path to an HTML file
182
- if (isString(foundCallback) && extname(foundCallback) === '.html') {
183
- try {
184
- const fileContent = Bun.file(foundCallback)
185
-
186
- return new Response(fileContent, {
187
- headers: {
188
- 'Content-Type': 'text/html',
189
- 'Access-Control-Allow-Origin': '*',
190
- 'Access-Control-Allow-Headers': '*',
191
- },
192
- })
193
- }
194
- catch (error) {
195
- handleError('Error reading the HTML file', error)
196
- return new Response('Error reading the HTML file', {
197
- status: 500,
198
- headers: {
199
- 'Access-Control-Allow-Origin': '*',
200
- 'Access-Control-Allow-Headers': '*',
201
- },
202
- })
203
- }
204
- }
205
-
206
- if (isString(foundCallback)) {
207
- return new Response(foundCallback, {
208
- headers: {
209
- 'Content-Type': 'application/json',
210
- 'Access-Control-Allow-Origin': '*',
211
- 'Access-Control-Allow-Headers': '*',
212
- },
213
- status: 200,
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
- }
227
-
228
- if (isFunction(foundCallback)) {
229
- const result = foundCallback()
230
-
231
- return new Response(JSON.stringify(result), {
232
- status: 200,
233
- headers: {
234
- 'Access-Control-Allow-Origin': '*',
235
- 'Access-Control-Allow-Headers': '*',
236
- },
237
- })
238
- }
239
-
240
- if (isObject(foundCallback) && foundCallback.status) {
241
- if (foundCallback.status === 401) {
242
- const { status, ...rest } = await foundCallback
243
-
244
- return new Response(JSON.stringify(rest), {
245
- headers: {
246
- 'Content-Type': 'application/json',
247
- 'Access-Control-Allow-Origin': '*',
248
- 'Access-Control-Allow-Headers': '*',
249
- },
250
- status: 401,
251
- })
252
- }
253
-
254
- if (foundCallback.status === 404) {
255
- const { status, ...rest } = await foundCallback
256
-
257
- const { errors } = rest
258
-
259
- return new Response(JSON.stringify(errors), {
260
- headers: {
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',
275
- 'Access-Control-Allow-Origin': '*',
276
- 'Access-Control-Allow-Headers': '*',
277
- },
278
- status: 403,
279
- })
280
- }
281
-
282
- if (foundCallback.status === 422) {
283
- const { status, ...rest } = await foundCallback
284
-
285
- const { errors } = rest
286
- return new Response(JSON.stringify(errors), {
287
- headers: {
288
- 'Content-Type': 'application/json',
289
- 'Access-Control-Allow-Origin': '*',
290
- 'Access-Control-Allow-Headers': '*',
291
- },
292
- status: 422,
293
- })
294
- }
295
-
296
- if (foundCallback.status === 500) {
297
- const { status, ...rest } = await foundCallback
298
-
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
- })
315
- })
316
- }
317
- }
318
-
319
- if (isObject(foundCallback)) {
320
- return new Response(JSON.stringify(foundCallback), {
321
- headers: {
322
- 'Content-Type': 'application/json',
323
- 'Access-Control-Allow-Origin': '*',
324
- 'Access-Control-Allow-Headers': '*',
325
- },
326
- status: 200,
327
- })
328
- }
329
-
330
- // If no known type matched, return a generic error.
331
- return new Response('Unknown callback type.', {
332
- headers: {
333
- 'Content-Type': 'application/json',
334
- 'Access-Control-Allow-Origin': '*',
335
- 'Access-Control-Allow-Headers': '*',
336
- },
337
- status: 500,
338
- })
339
- }
340
-
341
- function noCache(response: Response): Response {
342
- response.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate')
343
- response.headers.set('Pragma', 'no-cache')
344
- response.headers.set('Expires', '0')
345
-
346
- return response
347
- }
348
-
349
- async function addRouteQuery(url: URL): Promise<void> {
350
- const modelFiles = globSync([path.userModelsPath('*.ts')], { absolute: true })
351
- for (const modelFile of modelFiles) {
352
- const model = (await import(modelFile)).default
353
- const modelName = getModelName(model, modelFile)
354
- const requestPath = path.frameworkPath(`requests/${modelName}Request.ts`)
355
- const requestImport = await import(requestPath)
356
- const requestInstance = requestImport.request
357
-
358
- if (requestInstance) {
359
- requestInstance.addQuery(url)
360
- }
361
- }
362
-
363
- RequestParam.addQuery(url)
364
- }
365
-
366
- async function addBody(params: any): Promise<void> {
367
- const modelFiles = globSync([path.userModelsPath('*.ts')], { absolute: true })
368
-
369
- for (const modelFile of modelFiles) {
370
- const model = (await import(modelFile)).default
371
- const modelName = getModelName(model, modelFile)
372
- const requestPath = path.frameworkPath(`requests/${modelName}Request.ts`)
373
- const requestImport = await import(requestPath)
374
- const requestInstance = requestImport.request
375
-
376
- if (requestInstance) {
377
- requestInstance.addBodies(JSON.parse(params))
378
- }
379
- }
380
-
381
- RequestParam.addBodies(JSON.parse(params))
382
- }
383
-
384
- async function addRouteParam(param: RouteParam): Promise<void> {
385
- const modelFiles = globSync([path.userModelsPath('*.ts')], { absolute: true })
386
-
387
- for (const modelFile of modelFiles) {
388
- const model = (await import(modelFile)).default as Model
389
- const modelName = getModelName(model, modelFile)
390
- const requestPath = path.frameworkPath(`requests/${modelName}Request.ts`)
391
- const requestImport = await import(requestPath)
392
- const requestInstance = requestImport.request
393
-
394
- if (requestInstance) {
395
- requestInstance.addParam(param)
396
- }
397
- }
398
-
399
- RequestParam.addParam(param)
400
- }
401
-
402
- async function addHeaders(headers: Headers): Promise<void> {
403
- const modelFiles = globSync([path.userModelsPath('*.ts')], { absolute: true })
404
-
405
- for (const modelFile of modelFiles) {
406
- const model = (await import(modelFile)).default as Model
407
- const modelName = getModelName(model, modelFile)
408
- const requestPath = path.frameworkPath(`requests/${modelName}Request.ts`)
409
- const requestImport = await import(requestPath)
410
- const requestInstance = requestImport.request
411
-
412
- if (requestInstance) {
413
- requestInstance.addHeaders(headers)
414
- }
415
- }
416
-
417
- RequestParam.addHeaders(headers)
418
- }
419
-
420
- async function executeMiddleware(route: Route): Promise<any> {
421
- const { middleware = null } = route
422
-
423
- if (middleware && middlewares && isObjectNotEmpty(middlewares)) {
424
- // let middlewareItem: MiddlewareOptions
425
- if (isString(middleware)) {
426
- const middlewarePath = path.userMiddlewarePath(`${middleware}.ts`)
427
-
428
- const middlewareInstance = (await import(middlewarePath)).default
429
-
430
- try {
431
- await middlewareInstance.handle()
432
- }
433
- catch (error: any) {
434
- return error
435
- }
436
- }
437
- else {
438
- for (const middlewareElement of middleware) {
439
- const middlewarePath = path.userMiddlewarePath(`${middlewareElement}.ts`)
440
-
441
- const middlewareInstance = (await import(middlewarePath)).default
442
-
443
- try {
444
- await middlewareInstance.handle()
445
- }
446
- catch (error: any) {
447
- return error
448
- }
449
- }
450
- }
451
- }
452
- }
453
- function isString(val: unknown): val is string {
454
- return typeof val === 'string'
455
- }
456
-
457
- function isObjectNotEmpty(obj: object): boolean {
458
- return Object.keys(obj).length > 0
459
- }
460
-
461
- // eslint-disable-next-line ts/no-unsafe-function-type
462
- function isFunction(val: unknown): val is Function {
463
- return typeof val === 'function'
464
- }
465
-
466
- function isObject(val: unknown): val is object {
467
- return typeof val === 'object'
468
- }
package/src/utils.ts DELETED
@@ -1,88 +0,0 @@
1
- import type { ModelRequest, RequestInstance } from '@stacksjs/types'
2
- import { type Ok, ok } from '@stacksjs/error-handling'
3
- import { path } from '@stacksjs/path'
4
- import { camelCase } from '@stacksjs/strings'
5
- import { route } from './router'
6
-
7
- export async function listRoutes(): Promise<Ok<string, any>> {
8
- const routeLists = await route.getRoutes()
9
-
10
- // eslint-disable-next-line no-console
11
- console.table(routeLists)
12
-
13
- return ok('Successfully listed routes!')
14
- }
15
-
16
- export function extractModelFromAction(action: string): string {
17
- let model = ''
18
-
19
- if (action.includes('IndexOrmAction')) {
20
- const match = action.match(/\/([A-Z][a-z]+)IndexOrmAction/)
21
- const modelString = match ? match[1] : ''
22
-
23
- model = modelString as string
24
- }
25
-
26
- if (action.includes('StoreOrmAction')) {
27
- const match = action.match(/\/([A-Z][a-z]+)StoreOrmAction/)
28
- const modelString = match ? match[1] : ''
29
-
30
- model = modelString as string
31
- }
32
-
33
- if (action.includes('ShowOrmAction')) {
34
- const match = action.match(/\/([A-Z][a-z]+)ShowOrmAction/)
35
- const modelString = match ? match[1] : ''
36
-
37
- model = modelString as string
38
- }
39
-
40
- if (action.includes('UpdateOrmAction')) {
41
- const match = action.match(/\/([A-Z][a-z]+)UpdateOrmAction/)
42
- const modelString = match ? match[1] : ''
43
-
44
- model = modelString as string
45
- }
46
-
47
- if (action.includes('DestroyOrmAction')) {
48
- const match = action.match(/\/([A-Z][a-z]+)DestroyOrmAction/)
49
- const modelString = match ? match[1] : ''
50
-
51
- model = modelString as string
52
- }
53
-
54
- return model
55
- }
56
-
57
- export function extractDynamicAction(action: string): string | undefined {
58
- const regex = /Actions\/(.*?)Action/
59
- const match = action.match(regex)
60
-
61
- return match ? match[1] : ''
62
- }
63
-
64
- export async function extractModelRequest(action: string): Promise<RequestInstance | null> {
65
- const extractedModel = extractModelFromAction(action)
66
- const lowerCaseModel = camelCase(extractedModel)
67
- const requestPath = path.frameworkPath(`requests/${extractedModel}Request.ts`)
68
- const requestInstance = await import(requestPath)
69
- const requestIndex = `${lowerCaseModel}Request`
70
-
71
- return requestInstance[requestIndex]
72
- }
73
-
74
- export async function findRequestInstance(requestInstance: string): Promise<ModelRequest> {
75
- const frameworkDirectory = path.storagePath('framework/requests')
76
- const filePath = path.join(frameworkDirectory, `${requestInstance}.ts`)
77
-
78
- const reqInstance = await import(filePath)
79
-
80
- return reqInstance.request
81
- }
82
-
83
- export async function extractDefaultRequest(): Promise<RequestInstance> {
84
- const requestPath = path.frameworkPath(`core/router/src/request.ts`)
85
- const requestInstance = await import(requestPath)
86
-
87
- return requestInstance.request
88
- }