@stacksjs/router 0.65.0 → 0.67.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,467 +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
- // 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
-
149
- const middlewarePayload = await executeMiddleware(foundRoute)
150
-
151
- if (
152
- middlewarePayload !== null
153
- && typeof middlewarePayload === 'object'
154
- && Object.keys(middlewarePayload).length > 0
155
- ) {
156
- const middlewareStatus = middlewarePayload.status
157
-
158
- const { status, ...payloadWithoutStatus } = middlewarePayload
159
-
160
- return new Response(JSON.stringify(payloadWithoutStatus), {
161
- headers: {
162
- 'Content-Type': 'application/json',
163
- 'Access-Control-Allow-Origin': '*',
164
- 'Access-Control-Allow-Headers': '*',
165
- },
166
- status: middlewareStatus || 401,
167
- })
168
- }
169
-
170
- if (!statusCode)
171
- statusCode = 200
172
-
173
- if (foundRoute?.method === 'GET' && (statusCode === 301 || statusCode === 302)) {
174
- const callback = String(foundCallback)
175
- const response = Response.redirect(callback, statusCode)
176
-
177
- return noCache(response)
178
- }
179
-
180
- if (foundRoute?.method !== req.method) {
181
- return new Response('Method not allowed', {
182
- status: 405,
183
- headers: {
184
- 'Access-Control-Allow-Origin': '*',
185
- 'Access-Control-Allow-Headers': '*',
186
- },
187
- })
188
- }
189
-
190
- // Check if it's a path to an HTML file
191
- if (isString(foundCallback) && extname(foundCallback) === '.html') {
192
- try {
193
- const fileContent = Bun.file(foundCallback)
194
-
195
- return new Response(fileContent, {
196
- headers: {
197
- 'Content-Type': 'text/html',
198
- 'Access-Control-Allow-Origin': '*',
199
- 'Access-Control-Allow-Headers': '*',
200
- },
201
- })
202
- }
203
- catch (error) {
204
- handleError('Error reading the HTML file', error)
205
- return new Response('Error reading the HTML file', {
206
- status: 500,
207
- headers: {
208
- 'Access-Control-Allow-Origin': '*',
209
- 'Access-Control-Allow-Headers': '*',
210
- },
211
- })
212
- }
213
- }
214
-
215
- if (isString(foundCallback)) {
216
- return new Response(foundCallback, {
217
- headers: {
218
- 'Content-Type': 'application/json',
219
- 'Access-Control-Allow-Origin': '*',
220
- 'Access-Control-Allow-Headers': '*',
221
- },
222
- status: 200,
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
- }
236
-
237
- if (isFunction(foundCallback)) {
238
- const result = foundCallback()
239
-
240
- return new Response(JSON.stringify(result), {
241
- status: 200,
242
- headers: {
243
- 'Access-Control-Allow-Origin': '*',
244
- 'Access-Control-Allow-Headers': '*',
245
- },
246
- })
247
- }
248
-
249
- if (isObject(foundCallback) && foundCallback.status) {
250
- if (foundCallback.status === 401) {
251
- const { status, ...rest } = await foundCallback
252
-
253
- return new Response(JSON.stringify(rest), {
254
- headers: {
255
- 'Content-Type': 'application/json',
256
- 'Access-Control-Allow-Origin': '*',
257
- 'Access-Control-Allow-Headers': '*',
258
- },
259
- status: 401,
260
- })
261
- }
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
-
277
- if (foundCallback.status === 403) {
278
- const { status, ...rest } = await foundCallback
279
-
280
- return new Response(JSON.stringify(rest), {
281
- headers: {
282
- 'Content-Type': 'application/json',
283
- 'Access-Control-Allow-Origin': '*',
284
- 'Access-Control-Allow-Headers': '*',
285
- },
286
- status: 403,
287
- })
288
- }
289
-
290
- if (foundCallback.status === 422) {
291
- const { status, ...rest } = await foundCallback
292
-
293
- return new Response(JSON.stringify(rest), {
294
- headers: {
295
- 'Content-Type': 'application/json',
296
- 'Access-Control-Allow-Origin': '*',
297
- 'Access-Control-Allow-Headers': '*',
298
- },
299
- status: 422,
300
- })
301
- }
302
-
303
- if (foundCallback.status === 500) {
304
- const { status, ...rest } = await foundCallback
305
-
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
- },
313
- status: 500,
314
- })
315
- }
316
- }
317
-
318
- if (isObject(foundCallback)) {
319
- return new Response(JSON.stringify(foundCallback), {
320
- headers: {
321
- 'Content-Type': 'application/json',
322
- 'Access-Control-Allow-Origin': '*',
323
- 'Access-Control-Allow-Headers': '*',
324
- },
325
- status: 200,
326
- })
327
- }
328
-
329
- // If no known type matched, return a generic error.
330
- return new Response('Unknown callback type.', {
331
- headers: {
332
- 'Content-Type': 'application/json',
333
- 'Access-Control-Allow-Origin': '*',
334
- 'Access-Control-Allow-Headers': '*',
335
- },
336
- status: 500,
337
- })
338
- }
339
-
340
- function noCache(response: Response): Response {
341
- response.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate')
342
- response.headers.set('Pragma', 'no-cache')
343
- response.headers.set('Expires', '0')
344
-
345
- return response
346
- }
347
-
348
- async function addRouteQuery(url: URL): Promise<void> {
349
- const modelFiles = globSync([path.userModelsPath('*.ts')], { absolute: true })
350
- for (const modelFile of modelFiles) {
351
- const model = (await import(modelFile)).default
352
- const modelName = getModelName(model, modelFile)
353
- const requestPath = path.frameworkPath(`requests/${modelName}Request.ts`)
354
- const requestImport = await import(requestPath)
355
- const requestInstance = requestImport.request
356
-
357
- if (requestInstance) {
358
- requestInstance.addQuery(url)
359
- }
360
- }
361
-
362
- RequestParam.addQuery(url)
363
- }
364
-
365
- async function addBody(params: any): Promise<void> {
366
- const modelFiles = globSync([path.userModelsPath('*.ts')], { absolute: true })
367
-
368
- for (const modelFile of modelFiles) {
369
- const model = (await import(modelFile)).default
370
- const modelName = getModelName(model, modelFile)
371
- const requestPath = path.frameworkPath(`requests/${modelName}Request.ts`)
372
- const requestImport = await import(requestPath)
373
- const requestInstance = requestImport.request
374
-
375
- if (requestInstance) {
376
- requestInstance.addBodies(JSON.parse(params))
377
- }
378
- }
379
-
380
- RequestParam.addBodies(JSON.parse(params))
381
- }
382
-
383
- async function addRouteParam(param: RouteParam): Promise<void> {
384
- const modelFiles = globSync([path.userModelsPath('*.ts')], { absolute: true })
385
-
386
- for (const modelFile of modelFiles) {
387
- const model = (await import(modelFile)).default as Model
388
- const modelName = getModelName(model, modelFile)
389
- const requestPath = path.frameworkPath(`requests/${modelName}Request.ts`)
390
- const requestImport = await import(requestPath)
391
- const requestInstance = requestImport.request
392
-
393
- if (requestInstance) {
394
- requestInstance.addParam(param)
395
- }
396
- }
397
-
398
- RequestParam.addParam(param)
399
- }
400
-
401
- async function addHeaders(headers: Headers): Promise<void> {
402
- const modelFiles = globSync([path.userModelsPath('*.ts')], { absolute: true })
403
-
404
- for (const modelFile of modelFiles) {
405
- const model = (await import(modelFile)).default as Model
406
- const modelName = getModelName(model, modelFile)
407
- const requestPath = path.frameworkPath(`requests/${modelName}Request.ts`)
408
- const requestImport = await import(requestPath)
409
- const requestInstance = requestImport.request
410
-
411
- if (requestInstance) {
412
- requestInstance.addHeaders(headers)
413
- }
414
- }
415
-
416
- RequestParam.addHeaders(headers)
417
- }
418
-
419
- async function executeMiddleware(route: Route): Promise<any> {
420
- const { middleware = null } = route
421
-
422
- if (middleware && middlewares && isObjectNotEmpty(middlewares)) {
423
- // let middlewareItem: MiddlewareOptions
424
- if (isString(middleware)) {
425
- const middlewarePath = path.userMiddlewarePath(`${middleware}.ts`)
426
-
427
- const middlewareInstance = (await import(middlewarePath)).default
428
-
429
- try {
430
- await middlewareInstance.handle()
431
- }
432
- catch (error: any) {
433
- return error
434
- }
435
- }
436
- else {
437
- for (const middlewareElement of middleware) {
438
- const middlewarePath = path.userMiddlewarePath(`${middlewareElement}.ts`)
439
-
440
- const middlewareInstance = (await import(middlewarePath)).default
441
-
442
- try {
443
- await middlewareInstance.handle()
444
- }
445
- catch (error: any) {
446
- return error
447
- }
448
- }
449
- }
450
- }
451
- }
452
- function isString(val: unknown): val is string {
453
- return typeof val === 'string'
454
- }
455
-
456
- function isObjectNotEmpty(obj: object): boolean {
457
- return Object.keys(obj).length > 0
458
- }
459
-
460
- // eslint-disable-next-line ts/no-unsafe-function-type
461
- function isFunction(val: unknown): val is Function {
462
- return typeof val === 'function'
463
- }
464
-
465
- function isObject(val: unknown): val is object {
466
- return typeof val === 'object'
467
- }
package/src/utils.ts DELETED
@@ -1,90 +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 { existsSync } from '@stacksjs/storage'
5
- import { camelCase } from '@stacksjs/strings'
6
- import { route } from './router'
7
-
8
- export async function listRoutes(): Promise<Ok<string, any>> {
9
- const routeLists = await route.getRoutes()
10
-
11
- // eslint-disable-next-line no-console
12
- console.table(routeLists)
13
-
14
- return ok('Successfully listed routes!')
15
- }
16
-
17
- export function extractModelFromAction(action: string): string {
18
- let model = ''
19
-
20
- if (action.includes('IndexOrmAction')) {
21
- const match = action.match(/\/([A-Z][a-z]+)IndexOrmAction/)
22
- const modelString = match ? match[1] : ''
23
-
24
- model = modelString as string
25
- }
26
-
27
- if (action.includes('StoreOrmAction')) {
28
- const match = action.match(/\/([A-Z][a-z]+)StoreOrmAction/)
29
- const modelString = match ? match[1] : ''
30
-
31
- model = modelString as string
32
- }
33
-
34
- if (action.includes('ShowOrmAction')) {
35
- const match = action.match(/\/([A-Z][a-z]+)ShowOrmAction/)
36
- const modelString = match ? match[1] : ''
37
-
38
- model = modelString as string
39
- }
40
-
41
- if (action.includes('UpdateOrmAction')) {
42
- const match = action.match(/\/([A-Z][a-z]+)UpdateOrmAction/)
43
- const modelString = match ? match[1] : ''
44
-
45
- model = modelString as string
46
- }
47
-
48
- if (action.includes('DestroyOrmAction')) {
49
- const match = action.match(/\/([A-Z][a-z]+)DestroyOrmAction/)
50
- const modelString = match ? match[1] : ''
51
-
52
- model = modelString as string
53
- }
54
-
55
- return model
56
- }
57
-
58
- export function extractDynamicAction(action: string): string | undefined {
59
- const regex = /Actions\/(.*?)Action/
60
- const match = action.match(regex)
61
-
62
- return match ? match[1] : ''
63
- }
64
-
65
- export async function extractModelRequest(action: string): Promise<RequestInstance | null> {
66
- const extractedModel = extractModelFromAction(action)
67
- const lowerCaseModel = camelCase(extractedModel)
68
- const requestPath = path.frameworkPath(`requests/${extractedModel}Request.ts`)
69
- const requestInstance = await import(requestPath)
70
- const requestIndex = `${lowerCaseModel}Request`
71
-
72
- return requestInstance[requestIndex]
73
- }
74
-
75
- export async function findRequestInstance(requestInstance: string): Promise<ModelRequest> {
76
- const frameworkDirectory = path.storagePath('framework/requests')
77
- const filePath = path.join(frameworkDirectory, `${requestInstance}.ts`)
78
- const pathExists = await existsSync(filePath)
79
-
80
- const reqInstance = await import(filePath)
81
-
82
- return reqInstance.request
83
- }
84
-
85
- export async function extractDefaultRequest(): Promise<RequestInstance> {
86
- const requestPath = path.frameworkPath(`core/router/src/request.ts`)
87
- const requestInstance = await import(requestPath)
88
-
89
- return requestInstance.request
90
- }