@stacksjs/router 0.58.48 → 0.58.50
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/package.json +3 -2
- package/src/index.ts +4 -0
- package/src/middleware.ts +34 -0
- package/src/request.ts +44 -0
- package/src/router.ts +162 -0
- package/src/server.ts +154 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacksjs/router",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.58.
|
|
4
|
+
"version": "0.58.50",
|
|
5
5
|
"description": "The Stacks framework router.",
|
|
6
6
|
"author": "Chris Breuer",
|
|
7
7
|
"license": "MIT",
|
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
],
|
|
40
40
|
"files": [
|
|
41
41
|
"README.md",
|
|
42
|
-
"dist"
|
|
42
|
+
"dist",
|
|
43
|
+
"src"
|
|
43
44
|
],
|
|
44
45
|
"scripts": {
|
|
45
46
|
"build": "bun --bun build.ts",
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { appPath } from '@stacksjs/path'
|
|
2
|
+
import type { MiddlewareType } from '@stacksjs/types'
|
|
3
|
+
|
|
4
|
+
export class Middleware implements MiddlewareType {
|
|
5
|
+
name: string
|
|
6
|
+
priority: number
|
|
7
|
+
|
|
8
|
+
handle: Function
|
|
9
|
+
|
|
10
|
+
constructor(data: MiddlewareType) {
|
|
11
|
+
this.name = data.name
|
|
12
|
+
this.priority = data.priority
|
|
13
|
+
this.handle = data.handle
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// const readdir = promisify(fs.readdir)
|
|
18
|
+
|
|
19
|
+
async function importMiddlewares(directory: string) {
|
|
20
|
+
// const middlewares = []
|
|
21
|
+
// TODO: somehow this breaks ./buddy dev
|
|
22
|
+
// const files = await readdir(directory)
|
|
23
|
+
|
|
24
|
+
// for (const file of files) {
|
|
25
|
+
// // Dynamically import the middleware
|
|
26
|
+
// const imported = await import(path.join(directory, file))
|
|
27
|
+
// middlewares.push(imported.default)
|
|
28
|
+
// }
|
|
29
|
+
|
|
30
|
+
// return middlewares
|
|
31
|
+
return [directory] // fix this: return array of middlewares
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const middlewares = await importMiddlewares(appPath('middleware'))
|
package/src/request.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
interface RequestData {
|
|
2
|
+
[key: string]: string
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
type RouteParams = { [key: string]: string } | null
|
|
6
|
+
|
|
7
|
+
export class Request {
|
|
8
|
+
private query: RequestData = {}
|
|
9
|
+
private params: RouteParams = null
|
|
10
|
+
|
|
11
|
+
public addQuery(url: URL): void {
|
|
12
|
+
this.query = Object.fromEntries(url.searchParams)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public get(element: string): string | number | undefined {
|
|
16
|
+
return this.query[element]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public all(): RequestData {
|
|
20
|
+
return this.query
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public has(element: string): boolean {
|
|
24
|
+
return element in this.query
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public isEmpty(): boolean {
|
|
28
|
+
return Object.keys(this.query).length === 0
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public extractParamsFromRoute(routePattern: string, pathname: string): void {
|
|
32
|
+
const pattern = new RegExp(`^${routePattern.replace(/:(\w+)/g, (match, paramName) => `(?<${paramName}>\\w+)`)}$`)
|
|
33
|
+
const match = pattern.exec(pathname)
|
|
34
|
+
|
|
35
|
+
if (match?.groups)
|
|
36
|
+
this.params = match?.groups
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public getParams(key: string): number | string | null {
|
|
40
|
+
return this.params ? (this.params[key] || null) : null
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const request = new Request()
|
package/src/router.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { RedirectCode, Route, RouteGroupOptions, StatusCode } from '@stacksjs/types'
|
|
2
|
+
import { projectPath } from '@stacksjs/path'
|
|
3
|
+
|
|
4
|
+
export interface RouterInterface {
|
|
5
|
+
get(url: Route['url'], callback: Route['callback']): this
|
|
6
|
+
post(url: Route['url'], callback: Route['callback']): this
|
|
7
|
+
view(url: Route['url'], callback: Route['callback']): this
|
|
8
|
+
redirect(url: Route['url'], callback: Route['callback'], status?: RedirectCode): this
|
|
9
|
+
delete(url: Route['url'], callback: Route['callback']): this
|
|
10
|
+
patch(url: Route['url'], callback: Route['callback']): this
|
|
11
|
+
put(url: Route['url'], callback: Route['callback']): this
|
|
12
|
+
group(options: RouteGroupOptions, callback: () => void): this
|
|
13
|
+
name(name: string): this
|
|
14
|
+
middleware(middleware: Route['middleware']): this
|
|
15
|
+
getRoutes(): Promise<Route[]>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class Router implements RouterInterface {
|
|
19
|
+
private routes: Route[] = []
|
|
20
|
+
|
|
21
|
+
private addRoute(method: Route['method'], uri: string, callback: Route['callback'] | string | object, statusCode: StatusCode): void {
|
|
22
|
+
const name = uri.replace(/\//g, '.').replace(/:/g, '') // we can improve this
|
|
23
|
+
const pattern = new RegExp(`^${uri.replace(/:[a-zA-Z]+/g, (_match) => {
|
|
24
|
+
return '([a-zA-Z0-9-]+)'
|
|
25
|
+
})}$`)
|
|
26
|
+
|
|
27
|
+
let routeCallback: Route['callback']
|
|
28
|
+
|
|
29
|
+
if (typeof callback === 'string' || typeof callback === 'object') {
|
|
30
|
+
// Convert string or object to RouteCallback
|
|
31
|
+
routeCallback = () => callback
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
routeCallback = callback
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.routes.push({
|
|
38
|
+
name,
|
|
39
|
+
method,
|
|
40
|
+
url: uri,
|
|
41
|
+
uri,
|
|
42
|
+
callback: routeCallback,
|
|
43
|
+
pattern,
|
|
44
|
+
statusCode,
|
|
45
|
+
paramNames: [],
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public get(path: Route['url'], callback: Route['callback']): this {
|
|
50
|
+
this.addRoute('GET', path, callback, 200)
|
|
51
|
+
return this
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public post(path: Route['url'], callback: Route['callback']): this {
|
|
55
|
+
this.addRoute('POST', path, callback, 201)
|
|
56
|
+
return this
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public view(path: Route['url'], callback: Route['callback']): this {
|
|
60
|
+
this.addRoute('GET', path, callback, 200)
|
|
61
|
+
return this
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public redirect(path: Route['url'], callback: Route['callback'], _status?: RedirectCode): this {
|
|
65
|
+
this.addRoute('GET', path, callback, 302)
|
|
66
|
+
return this
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public delete(path: Route['url'], callback: Route['callback']): this {
|
|
70
|
+
this.addRoute('DELETE', path, callback, 204)
|
|
71
|
+
return this
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public patch(path: Route['url'], callback: Route['callback']): this {
|
|
75
|
+
this.addRoute('PATCH', path, callback, 202)
|
|
76
|
+
return this
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public put(path: Route['url'], callback: Route['callback']): this {
|
|
80
|
+
this.addRoute('PUT', path, callback, 202)
|
|
81
|
+
return this
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public group(options: RouteGroupOptions | (() => void), callback?: () => void): this {
|
|
85
|
+
let cb: () => void
|
|
86
|
+
|
|
87
|
+
if (typeof options === 'function') {
|
|
88
|
+
cb = options
|
|
89
|
+
options = {}
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
if (!callback)
|
|
93
|
+
throw new Error('Missing callback function for route group.')
|
|
94
|
+
cb = callback
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const { prefix = '', middleware = [] } = options
|
|
98
|
+
|
|
99
|
+
// Save a reference to the original routes array.
|
|
100
|
+
const originalRoutes = this.routes
|
|
101
|
+
|
|
102
|
+
// Create a new routes array for the duration of the callback.
|
|
103
|
+
this.routes = []
|
|
104
|
+
|
|
105
|
+
// Execute the callback. This will add routes to the new this.routes array.
|
|
106
|
+
cb()
|
|
107
|
+
|
|
108
|
+
// For each route added by the callback, adjust the URI and add to the original routes array.
|
|
109
|
+
this.routes.forEach((r) => {
|
|
110
|
+
r.uri = `${prefix}${r.uri}`
|
|
111
|
+
|
|
112
|
+
if (middleware.length)
|
|
113
|
+
r.middleware = middleware
|
|
114
|
+
// Assuming you have a middleware property for each route.
|
|
115
|
+
|
|
116
|
+
originalRoutes.push(r)
|
|
117
|
+
return this
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
// Restore the original routes array.
|
|
121
|
+
this.routes = originalRoutes
|
|
122
|
+
|
|
123
|
+
return this
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
public name(name: string): this {
|
|
127
|
+
// @ts-expect-error - this is fine for now
|
|
128
|
+
this.routes[this.routes.length - 1].name = name
|
|
129
|
+
|
|
130
|
+
return this
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public middleware(middleware: Route['middleware']): this {
|
|
134
|
+
// @ts-expect-error - this is fine for now
|
|
135
|
+
this.routes[this.routes.length - 1].middleware = middleware
|
|
136
|
+
|
|
137
|
+
return this
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
public prefix(prefix: string): this {
|
|
141
|
+
// @ts-expect-error - this is fine for now
|
|
142
|
+
this.routes[this.routes.length - 1].prefix = prefix
|
|
143
|
+
|
|
144
|
+
return this
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
public async getRoutes(): Promise<Route[]> {
|
|
148
|
+
// const routeFileData = (await readTextFile(projectPath('routes/web.ts'))).data
|
|
149
|
+
|
|
150
|
+
await import(projectPath('routes/api.ts'))
|
|
151
|
+
|
|
152
|
+
// run routes/web.ts
|
|
153
|
+
// const webRoutesPath = projectPath('routes/web.ts')
|
|
154
|
+
// await runCommand(`bun ${webRoutesPath}`)
|
|
155
|
+
|
|
156
|
+
// set this.routes to a mapped array of routes that matches the pattern
|
|
157
|
+
|
|
158
|
+
return this.routes
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export const route = new Router()
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { extname } from 'node:path'
|
|
2
|
+
import { URL } from 'node:url'
|
|
3
|
+
import type { MiddlewareType, Route, StatusCode } from '@stacksjs/types'
|
|
4
|
+
import { localUrl } from '@stacksjs/config'
|
|
5
|
+
import { middlewares } from './middleware'
|
|
6
|
+
import { request } from './request'
|
|
7
|
+
import { route } from '.'
|
|
8
|
+
|
|
9
|
+
interface ServeOptions {
|
|
10
|
+
host?: string
|
|
11
|
+
port?: number
|
|
12
|
+
tunnel?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function serve(options: ServeOptions = {}) {
|
|
16
|
+
const hostname = options.host || options.tunnel ? await localUrl({ type: 'backend' }) : '127.0.0.1'
|
|
17
|
+
const port = options.port || 3000
|
|
18
|
+
|
|
19
|
+
Bun.serve({
|
|
20
|
+
hostname,
|
|
21
|
+
port,
|
|
22
|
+
|
|
23
|
+
fetch(req: Request) {
|
|
24
|
+
// eslint-disable-next-line no-console
|
|
25
|
+
console.log(req)
|
|
26
|
+
return serverResponse(req)
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function serverResponse(req: Request) {
|
|
32
|
+
// eslint-disable-next-line no-console
|
|
33
|
+
console.log('serverResponse', req)
|
|
34
|
+
const routesList: Route[] = await route.getRoutes()
|
|
35
|
+
const url = new URL(req.url)
|
|
36
|
+
|
|
37
|
+
const foundRoute: Route | undefined = routesList.find((route: Route) => {
|
|
38
|
+
const pattern = new RegExp(`^${route.uri.replace(/:\w+/g, '\\w+')}$`)
|
|
39
|
+
|
|
40
|
+
return pattern.test(url.pathname)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// if (url.pathname === '/favicon.ico')
|
|
44
|
+
// return new Response('')
|
|
45
|
+
|
|
46
|
+
if (!foundRoute)
|
|
47
|
+
return new Response('Not found', { status: 404 }) // TODO: create a pretty 404 page
|
|
48
|
+
|
|
49
|
+
addRouteParamsAndQuery(url, foundRoute)
|
|
50
|
+
executeMiddleware(foundRoute)
|
|
51
|
+
|
|
52
|
+
return execute(foundRoute, req, { statusCode: foundRoute?.statusCode })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function addRouteParamsAndQuery(url: URL, route: Route): void {
|
|
56
|
+
if (!isObjectNotEmpty(url.searchParams))
|
|
57
|
+
request.addQuery(url)
|
|
58
|
+
|
|
59
|
+
request.extractParamsFromRoute(route.uri, url.pathname)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function executeMiddleware(route: Route): void {
|
|
63
|
+
const { middleware = null } = route
|
|
64
|
+
|
|
65
|
+
if (middleware && middlewares && isObjectNotEmpty(middlewares)) {
|
|
66
|
+
if (isString(middleware)) {
|
|
67
|
+
const middlewareItem: MiddlewareType = middlewares.find((middlewareItem: MiddlewareType) => {
|
|
68
|
+
return middlewareItem.name === middleware
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
if (middlewareItem)
|
|
72
|
+
middlewareItem.handle() // Invoke only if it exists and is not undefined.
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
middleware.forEach((m) => {
|
|
76
|
+
const middlewareItem: MiddlewareType = middlewares.find((middlewareItem: MiddlewareType) => {
|
|
77
|
+
return middlewareItem.name === m
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
if (middlewareItem)
|
|
81
|
+
middlewareItem.handle() // Again, invoke only if it exists.
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface Options {
|
|
88
|
+
statusCode?: StatusCode
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function execute(route: Route, request: Request, { statusCode }: Options) {
|
|
92
|
+
if (!statusCode)
|
|
93
|
+
statusCode = 200
|
|
94
|
+
|
|
95
|
+
if (route?.method === 'GET' && (statusCode === 301 || statusCode === 302)) {
|
|
96
|
+
const callback = String(route.callback)
|
|
97
|
+
const response = Response.redirect(callback, statusCode)
|
|
98
|
+
|
|
99
|
+
return noCache(response)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (route?.method !== request.method)
|
|
103
|
+
return new Response('Method not allowed', { status: 405 })
|
|
104
|
+
|
|
105
|
+
// Check if it's a path to an HTM L file
|
|
106
|
+
if (isString(route.callback) && extname(route.callback) === '.html') {
|
|
107
|
+
try {
|
|
108
|
+
const fileContent = Bun.file(route.callback)
|
|
109
|
+
|
|
110
|
+
return new Response(fileContent, { headers: { 'Content-Type': 'text/html' } })
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
return new Response('Error reading the HTML file', { status: 500 })
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (isString(route.callback))
|
|
118
|
+
return new Response(route.callback)
|
|
119
|
+
|
|
120
|
+
if (isFunction(route.callback)) {
|
|
121
|
+
const result = (route.callback)()
|
|
122
|
+
return new Response(JSON.stringify(result))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (isObject(route.callback))
|
|
126
|
+
return new Response(JSON.stringify(route.callback))
|
|
127
|
+
|
|
128
|
+
// If no known type matched, return a generic error.
|
|
129
|
+
return new Response('Unknown callback type.', { status: 500 })
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function noCache(response: Response) {
|
|
133
|
+
response.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate')
|
|
134
|
+
response.headers.set('Pragma', 'no-cache')
|
|
135
|
+
response.headers.set('Expires', '0')
|
|
136
|
+
|
|
137
|
+
return response
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function isString(val: unknown): val is string {
|
|
141
|
+
return typeof val === 'string'
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isObjectNotEmpty(obj: object): boolean {
|
|
145
|
+
return Object.keys(obj).length > 0
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function isFunction(val: unknown): val is Function {
|
|
149
|
+
return typeof val === 'function'
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function isObject(val: unknown): val is object {
|
|
153
|
+
return val !== null && typeof val === 'object' && !Array.isArray(val)
|
|
154
|
+
}
|