@stacksjs/router 0.58.47 → 0.58.49

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/router",
3
3
  "type": "module",
4
- "version": "0.58.47",
4
+ "version": "0.58.49",
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",
@@ -47,7 +48,7 @@
47
48
  "prepublishOnly": "bun --bun run build"
48
49
  },
49
50
  "peerDependencies": {
50
- "@stacksjs/config": "workspace:*",
51
+ "@stacksjs/config": "latest",
51
52
  "unplugin-vue-router": "^0.7.0",
52
53
  "vue-router": "^4.2.5"
53
54
  },
@@ -57,6 +58,6 @@
57
58
  "vue-router": "^4.2.5"
58
59
  },
59
60
  "devDependencies": {
60
- "@stacksjs/development": "workspace:*"
61
+ "@stacksjs/development": "latest"
61
62
  }
62
63
  }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './middleware'
2
+ export * from './request'
3
+ export * from './server'
4
+ export * from './router'
@@ -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
+ }