@stacksjs/router 0.58.71 → 0.58.72

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 CHANGED
@@ -552,7 +552,7 @@ var toNamespacedPath = function(p) {
552
552
  return normalizeWindowsPath(p);
553
553
  };
554
554
  var _EXTNAME_RE = /.(\.[^./]+)$/;
555
- var extname = function(p) {
555
+ var extname2 = function(p) {
556
556
  const match = _EXTNAME_RE.exec(normalizeWindowsPath(p));
557
557
  return match && match[1] || "";
558
558
  };
@@ -590,7 +590,7 @@ var basename = function(p, extension) {
590
590
  var parse = function(p) {
591
591
  const root = normalizeWindowsPath(p).split("/").shift() || "/";
592
592
  const base = basename(p);
593
- const extension = extname(base);
593
+ const extension = extname2(base);
594
594
  return {
595
595
  root,
596
596
  dir: dirname(p),
@@ -847,7 +847,10 @@ function replPath(path2) {
847
847
  function routerPath(path2) {
848
848
  return corePath(`router/${path2 || ""}`);
849
849
  }
850
- function routesPath(path2) {
850
+ function routesPath(path2, options) {
851
+ const absolutePath = resourcesPath(`routes/${path2 || ""}`);
852
+ if (options?.relative)
853
+ return relative(process2.cwd(), absolutePath);
851
854
  return projectPath(`routes/${path2 || ""}`);
852
855
  }
853
856
  function searchEnginePath(path2) {
@@ -1024,7 +1027,7 @@ var path2 = {
1024
1027
  basename,
1025
1028
  delimiter,
1026
1029
  dirname,
1027
- extname,
1030
+ extname: extname2,
1028
1031
  format,
1029
1032
  isAbsolute,
1030
1033
  join,
@@ -1084,33 +1087,38 @@ class Request {
1084
1087
  }
1085
1088
  var request = new Request;
1086
1089
  // src/server.ts
1087
- import {extname as extname2} from "path";
1088
1090
  import {URL} from "url";
1091
+ import process3 from "process";
1092
+ import {log} from "@stacksjs/logging";
1089
1093
  async function serve(options = {}) {
1090
1094
  const hostname = options.host || "localhost";
1091
1095
  const port = options.port || 3000;
1096
+ const development = options.debug ? true : process3.env.APP_ENV !== "production" && process3.env.APP_ENV !== "prod";
1097
+ if (options.timezone)
1098
+ process3.env.TZ = options.timezone;
1092
1099
  Bun.serve({
1093
1100
  hostname,
1094
1101
  port,
1102
+ development,
1095
1103
  fetch(req) {
1096
1104
  return serverResponse(req);
1097
1105
  }
1098
1106
  });
1099
1107
  }
1100
1108
  async function serverResponse(req) {
1101
- const timestamp = new Date().toISOString();
1102
- console.log(`${timestamp} - Incoming request: ${req.method} ${req.url}`);
1103
- console.log(`${timestamp} - Headers: ${JSON.stringify(req.headers)}`);
1104
- console.log(`${timestamp} - Body: ${JSON.stringify(req.body)}`);
1105
- console.log(`${timestamp} - Query: ${JSON.stringify(req.query)}`);
1106
- console.log(`${timestamp} - Params: ${JSON.stringify(req.params)}`);
1107
- console.log(`${timestamp} - Cookies: ${JSON.stringify(req.cookies)}`);
1109
+ log.info(`Incoming Request: ${req.method} ${req.url}`);
1110
+ log.info(`Headers: ${JSON.stringify(req.headers)}`);
1111
+ log.info(`Body: ${JSON.stringify(req.body)}`);
1112
+ const trimmedUrl = req.url.endsWith("/") && req.url.length > 1 ? req.url.slice(0, -1) : req.url;
1108
1113
  const routesList = await route.getRoutes();
1109
- const url = new URL(req.url);
1114
+ log.info(`Routes List: ${JSON.stringify(routesList)}`);
1115
+ const url = new URL(trimmedUrl);
1116
+ log.info(`URL: ${JSON.stringify(url)}`);
1110
1117
  const foundRoute = routesList.find((route2) => {
1111
1118
  const pattern = new RegExp(`^${route2.uri.replace(/:\w+/g, "\\w+")}\$`);
1112
1119
  return pattern.test(url.pathname);
1113
1120
  });
1121
+ log.info(`Found Route: ${JSON.stringify(foundRoute)}`);
1114
1122
  if (!foundRoute)
1115
1123
  return new Response("Pretty 404 page coming soon", { status: 404 });
1116
1124
  addRouteParamsAndQuery(url, foundRoute);
@@ -1152,7 +1160,7 @@ var execute = function(route2, request3, { statusCode }) {
1152
1160
  }
1153
1161
  if (route2?.method !== request3.method)
1154
1162
  return new Response("Method not allowed", { status: 405 });
1155
- if (isString(route2.callback) && extname2(route2.callback) === ".html") {
1163
+ if (isString(route2.callback) && extname(route2.callback) === ".html") {
1156
1164
  try {
1157
1165
  const fileContent = Bun.file(route2.callback);
1158
1166
  return new Response(fileContent, { headers: { "Content-Type": "text/html" } });
@@ -1188,6 +1196,9 @@ var isFunction = function(val) {
1188
1196
  var isObject = function(val) {
1189
1197
  return val !== null && typeof val === "object" && !Array.isArray(val);
1190
1198
  };
1199
+ // src/router.ts
1200
+ import {log as log2} from "@stacksjs/logging";
1201
+
1191
1202
  // /home/runner/work/stacks/stacks/storage/framework/core/strings/src/utils.ts
1192
1203
  var import_slugify = __toESM(require_slugify(), 1);
1193
1204
  // /home/runner/work/stacks/stacks/node_modules/change-case/dist/index.js
@@ -1324,6 +1335,9 @@ var pluralize = __toESM(require_pluralize(), 1);
1324
1335
  // src/router.ts
1325
1336
  class Router {
1326
1337
  routes = [];
1338
+ apiPrefix = "/api";
1339
+ groupPrefix = "";
1340
+ path = "";
1327
1341
  addRoute(method, uri, callback, statusCode) {
1328
1342
  const name = uri.replace(/\//g, ".").replace(/:/g, "");
1329
1343
  const pattern = new RegExp(`^${uri.replace(/:[a-zA-Z]+/g, (_match) => {
@@ -1335,6 +1349,7 @@ class Router {
1335
1349
  } else {
1336
1350
  routeCallback = callback;
1337
1351
  }
1352
+ log2.debug(`Adding route: ${method} ${uri} with name ${name}`);
1338
1353
  this.routes.push({
1339
1354
  name,
1340
1355
  method,
@@ -1345,35 +1360,41 @@ class Router {
1345
1360
  statusCode,
1346
1361
  paramNames: []
1347
1362
  });
1363
+ return this;
1348
1364
  }
1349
1365
  async get(path5, callback) {
1350
- if (callback instanceof Promise) {
1351
- const actionModule = await callback;
1352
- callback = actionModule.default;
1353
- } else if (typeof callback === "string") {
1354
- const actionModule = await import(path2.userActionsPath(`${callback}.ts`));
1355
- path5 = actionModule.default.path ?? path5;
1356
- callback = actionModule.default.handle;
1357
- }
1358
- this.addRoute("GET", path5, callback, 200);
1359
- return this;
1366
+ this.path = this.normalizePath(path5);
1367
+ log2.debug(`Normalized Path: ${this.path}`);
1368
+ callback = await this.resolveCallback(callback);
1369
+ const uri = this.prepareUri(this.path);
1370
+ log2.debug(`Prepared URI: ${uri}`);
1371
+ return this.addRoute("GET", uri, callback, 200);
1360
1372
  }
1361
1373
  async health() {
1362
1374
  const healthModule = await import(path2.userActionsPath("HealthAction.ts"));
1363
1375
  const callback = healthModule.default.handle;
1364
- this.addRoute("GET", "/api/health", callback, 200);
1376
+ const path5 = healthModule.default.path ?? `${this.apiPrefix}/health`;
1377
+ this.addRoute("GET", path5, callback, 200);
1365
1378
  return this;
1366
1379
  }
1367
1380
  async job(path5) {
1368
- if (path5.includes("api/"))
1369
- path5 = path5.replace(/^\/api\//, "");
1370
1381
  path5 = pascalCase(path5);
1371
1382
  const jobModule = await import(path2.userJobsPath(`${path5}Job.ts`.replace(/JobJob/, "Job")));
1372
1383
  const callback = jobModule.default.handle;
1384
+ path5 = this.prepareUri(path5);
1385
+ this.addRoute("GET", path5, callback, 200);
1386
+ return this;
1387
+ }
1388
+ async action(path5) {
1389
+ path5 = pascalCase(path5);
1390
+ const actionModule = await import(path2.userActionsPath(`${path5}Action.ts`.replace(/ActionAction/, "Action")));
1391
+ const callback = actionModule.default.handle;
1392
+ path5 = this.prepareUri(path5);
1373
1393
  this.addRoute("GET", path5, callback, 200);
1374
1394
  return this;
1375
1395
  }
1376
1396
  post(path5, callback) {
1397
+ path5 = this.prepareUri(path5);
1377
1398
  this.addRoute("POST", path5, callback, 201);
1378
1399
  return this;
1379
1400
  }
@@ -1398,16 +1419,18 @@ class Router {
1398
1419
  return this;
1399
1420
  }
1400
1421
  group(options, callback) {
1422
+ if (typeof options === "string")
1423
+ options = options.startsWith("/") ? options.slice(1) : options;
1401
1424
  let cb;
1425
+ this.prepareGroupPrefix(options);
1402
1426
  if (typeof options === "function") {
1403
1427
  cb = options;
1404
1428
  options = {};
1405
- } else {
1406
- if (!callback)
1407
- throw new Error("Missing callback function for route group.");
1408
- cb = callback;
1409
1429
  }
1410
- const { prefix = "", middleware: middleware2 = [] } = options;
1430
+ if (!callback)
1431
+ throw new Error("Missing callback function for your route group.");
1432
+ cb = callback;
1433
+ const { prefix, middleware: middleware2 = [] } = options;
1411
1434
  const originalRoutes = this.routes;
1412
1435
  this.routes = [];
1413
1436
  cb();
@@ -1434,9 +1457,59 @@ class Router {
1434
1457
  return this;
1435
1458
  }
1436
1459
  async getRoutes() {
1437
- await import(projectPath("routes/api.ts"));
1460
+ await import(routesPath("api.ts"));
1438
1461
  return this.routes;
1439
1462
  }
1463
+ setGroupPrefix(prefix, options = {}) {
1464
+ if (prefix !== "") {
1465
+ prefix = `/${this.groupPrefix}/${prefix}`.replace(/\/\//g, "/");
1466
+ this.groupPrefix = prefix;
1467
+ return;
1468
+ }
1469
+ const effectiveOptions = typeof options === "object" ? options : {};
1470
+ this.groupPrefix = effectiveOptions.prefix ?? prefix ?? "";
1471
+ }
1472
+ prepareGroupPrefix(options) {
1473
+ if (this.groupPrefix !== "" && typeof options !== "string")
1474
+ return this.setGroupPrefix(this.groupPrefix, options);
1475
+ if (typeof options === "string")
1476
+ return this.setGroupPrefix(options);
1477
+ return this.setGroupPrefix("", options);
1478
+ }
1479
+ async resolveCallback(callback) {
1480
+ if (callback instanceof Promise) {
1481
+ const actionModule = await callback;
1482
+ return actionModule.default;
1483
+ }
1484
+ if (typeof callback === "string")
1485
+ return this.importCallbackFromPath(callback, this.path);
1486
+ return callback;
1487
+ }
1488
+ async importCallbackFromPath(callbackPath, originalPath) {
1489
+ let modulePath = callbackPath;
1490
+ let importPathFunction = path2.appPath;
1491
+ if (callbackPath.startsWith("../"))
1492
+ importPathFunction = path2.routesPath;
1493
+ modulePath = modulePath.endsWith(".ts") ? modulePath.slice(0, -3) : modulePath;
1494
+ const actionModule = await import(importPathFunction(`${modulePath}.ts`));
1495
+ const newPath = actionModule.default.path ?? originalPath;
1496
+ this.updatePathIfNeeded(newPath, originalPath);
1497
+ return actionModule.default.handle;
1498
+ }
1499
+ normalizePath(path5) {
1500
+ return path5.endsWith("/") ? path5.slice(0, -1) : path5;
1501
+ }
1502
+ prepareUri(path5) {
1503
+ if (path5.startsWith("/"))
1504
+ path5 = path5.slice(1);
1505
+ path5 = `${this.apiPrefix}${this.groupPrefix}/${path5}`;
1506
+ return path5.endsWith("/") ? path5.slice(0, -1) : path5;
1507
+ }
1508
+ updatePathIfNeeded(newPath, originalPath) {
1509
+ if (newPath !== originalPath) {
1510
+ this.path = newPath;
1511
+ }
1512
+ }
1440
1513
  }
1441
1514
  var route = new Router;
1442
1515
  export {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/router",
3
3
  "type": "module",
4
- "version": "0.58.71",
4
+ "version": "0.58.72",
5
5
  "description": "The Stacks framework router.",
6
6
  "author": "Chris Breuer",
7
7
  "license": "MIT",
@@ -54,6 +54,7 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "@stacksjs/config": "latest",
57
+ "@stacksjs/logging": "latest",
57
58
  "unplugin-vue-router": "^0.7.0",
58
59
  "vue-router": "^4.2.5"
59
60
  },
package/src/router.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  import type { RedirectCode, Route, RouteGroupOptions, StatusCode } from '@stacksjs/types'
2
- import { path as p, projectPath } from '@stacksjs/path'
2
+ import { path as p, routesPath } from '@stacksjs/path'
3
+ import { log } from '@stacksjs/logging'
3
4
  import { pascalCase } from '@stacksjs/strings'
4
5
 
6
+ type Prefix = string
7
+
5
8
  export interface RouterInterface {
6
9
  get: (url: Route['url'], callback: Route['callback']) => Promise<this>
7
10
  post: (url: Route['url'], callback: Route['callback']) => this
@@ -10,7 +13,7 @@ export interface RouterInterface {
10
13
  delete: (url: Route['url'], callback: Route['callback']) => this
11
14
  patch: (url: Route['url'], callback: Route['callback']) => this
12
15
  put: (url: Route['url'], callback: Route['callback']) => this
13
- group: (options: RouteGroupOptions, callback: () => void) => this
16
+ group: (options: Prefix | RouteGroupOptions, callback: () => void) => this
14
17
  name: (name: string) => this
15
18
  middleware: (middleware: Route['middleware']) => this
16
19
  getRoutes: () => Promise<Route[]>
@@ -18,8 +21,11 @@ export interface RouterInterface {
18
21
 
19
22
  export class Router implements RouterInterface {
20
23
  private routes: Route[] = []
24
+ private apiPrefix = '/api'
25
+ private groupPrefix = ''
26
+ private path = ''
21
27
 
22
- private addRoute(method: Route['method'], uri: string, callback: Route['callback'] | string | object, statusCode: StatusCode): void {
28
+ private addRoute(method: Route['method'], uri: string, callback: Route['callback'] | string | object, statusCode: StatusCode): this {
23
29
  const name = uri.replace(/\//g, '.').replace(/:/g, '') // we can improve this
24
30
  const pattern = new RegExp(`^${uri.replace(/:[a-zA-Z]+/g, (_match) => {
25
31
  return '([a-zA-Z0-9-]+)'
@@ -35,6 +41,8 @@ export class Router implements RouterInterface {
35
41
  routeCallback = callback
36
42
  }
37
43
 
44
+ log.debug(`Adding route: ${method} ${uri} with name ${name}`)
45
+
38
46
  this.routes.push({
39
47
  name,
40
48
  method,
@@ -45,60 +53,75 @@ export class Router implements RouterInterface {
45
53
  statusCode,
46
54
  paramNames: [],
47
55
  })
56
+
57
+ return this
48
58
  }
49
59
 
50
60
  public async get(path: Route['url'], callback: Route['callback']): Promise<this> {
51
- // check if callback is a string and if it is, then import that module path and use the default.handle function as the callback
52
- if (callback instanceof Promise) {
53
- const actionModule = await callback
54
- callback = actionModule.default
55
- }
61
+ this.path = this.normalizePath(path)
62
+ log.debug(`Normalized Path: ${this.path}`)
56
63
 
57
- else if (typeof callback === 'string') {
58
- // import the module and use the default.handle function as the callback
59
- const actionModule = await import(p.userActionsPath(`${callback}.ts`))
64
+ callback = await this.resolveCallback(callback)
60
65
 
61
- path = actionModule.default.path ?? path // in case a custom path is defined inside the Action, use that instead of the path passed by the router
62
- callback = actionModule.default.handle
63
- }
66
+ const uri = this.prepareUri(this.path)
67
+ log.debug(`Prepared URI: ${uri}`)
64
68
 
65
- this.addRoute('GET', path, callback, 200)
66
- return this
69
+ return this.addRoute('GET', uri, callback, 200)
67
70
  }
68
71
 
69
72
  public async health(): Promise<this> {
70
73
  const healthModule = await import(p.userActionsPath('HealthAction.ts'))
71
74
  const callback = healthModule.default.handle
72
75
 
73
- this.addRoute('GET', '/api/health', callback, 200)
76
+ const path = healthModule.default.path ?? `${this.apiPrefix}/health`
77
+
78
+ this.addRoute('GET', path, callback, 200)
79
+
74
80
  return this
75
81
  }
76
82
 
77
83
  public async job(path: Route['url']): Promise<this> {
78
- if (path.includes('api/')) // TODO: once the api prefix is removed from the router/api, remove this
79
- path = path.replace(/^\/api\//, '')
80
-
81
84
  path = pascalCase(path)
85
+
82
86
  // removes the potential `JobJob` suffix in case the user does not choose to use the Job suffix in their file name
83
87
  const jobModule = await import(p.userJobsPath(`${path}Job.ts`.replace(/JobJob/, 'Job')))
84
88
  const callback = jobModule.default.handle
85
89
 
90
+ path = this.prepareUri(path)
86
91
  this.addRoute('GET', path, callback, 200)
92
+
93
+ return this
94
+ }
95
+
96
+ public async action(path: Route['url']): Promise<this> {
97
+ path = pascalCase(path) // actions are PascalCase
98
+
99
+ // removes the potential `ActionAction` suffix in case the user does not choose to use the Job suffix in their file name
100
+ const actionModule = await import(p.userActionsPath(`${path}Action.ts`.replace(/ActionAction/, 'Action')))
101
+ const callback = actionModule.default.handle
102
+
103
+ path = this.prepareUri(path)
104
+ this.addRoute('GET', path, callback, 200)
105
+
87
106
  return this
88
107
  }
89
108
 
90
109
  public post(path: Route['url'], callback: Route['callback']): this {
110
+ path = this.prepareUri(path)
91
111
  this.addRoute('POST', path, callback, 201)
112
+
92
113
  return this
93
114
  }
94
115
 
95
116
  public view(path: Route['url'], callback: Route['callback']): this {
96
117
  this.addRoute('GET', path, callback, 200)
118
+
97
119
  return this
98
120
  }
99
121
 
100
122
  public redirect(path: Route['url'], callback: Route['callback'], _status?: RedirectCode): this {
101
123
  this.addRoute('GET', path, callback, 302)
124
+
102
125
  return this
103
126
  }
104
127
 
@@ -117,37 +140,41 @@ export class Router implements RouterInterface {
117
140
  return this
118
141
  }
119
142
 
120
- public group(options: RouteGroupOptions | (() => void), callback?: () => void): this {
143
+ public group(options: string | RouteGroupOptions, callback?: () => void): this {
144
+ if (typeof options === 'string')
145
+ options = options.startsWith('/') ? options.slice(1) : options
146
+
121
147
  let cb: () => void
122
148
 
149
+ this.prepareGroupPrefix(options)
150
+
123
151
  if (typeof options === 'function') {
124
152
  cb = options
125
153
  options = {}
126
154
  }
127
- else {
128
- if (!callback)
129
- throw new Error('Missing callback function for route group.')
130
- cb = callback
131
- }
132
155
 
133
- const { prefix = '', middleware = [] } = options
156
+ if (!callback)
157
+ throw new Error('Missing callback function for your route group.')
158
+
159
+ cb = callback
134
160
 
135
- // Save a reference to the original routes array.
161
+ const { prefix, middleware = [] } = options as RouteGroupOptions
162
+
163
+ // Save a reference to the original routes array
136
164
  const originalRoutes = this.routes
137
165
 
138
- // Create a new routes array for the duration of the callback.
166
+ // Create a new routes array for the duration of the callback
139
167
  this.routes = []
140
168
 
141
- // Execute the callback. This will add routes to the new this.routes array.
169
+ // Execute the callback. This will add routes to the new this.routes array
142
170
  cb()
143
171
 
144
- // For each route added by the callback, adjust the URI and add to the original routes array.
172
+ // For each route added by the callback, adjust the URI and add to the original routes array
145
173
  this.routes.forEach((r) => {
146
174
  r.uri = `${prefix}${r.uri}`
147
175
 
148
176
  if (middleware.length)
149
177
  r.middleware = middleware
150
- // Assuming you have a middleware property for each route.
151
178
 
152
179
  originalRoutes.push(r)
153
180
  return this
@@ -181,17 +208,86 @@ export class Router implements RouterInterface {
181
208
  }
182
209
 
183
210
  public async getRoutes(): Promise<Route[]> {
184
- // const routeFileData = (await readTextFile(projectPath('routes/web.ts'))).data
211
+ await import(routesPath('api.ts'))
212
+
213
+ return this.routes
214
+ }
185
215
 
186
- await import(projectPath('routes/api.ts'))
216
+ private setGroupPrefix(prefix: string, options: RouteGroupOptions = {}) {
217
+ if (prefix !== '') {
218
+ prefix = `/${this.groupPrefix}/${prefix}`.replace(/\/\//g, '/') // remove double slashes in case there are any
219
+ this.groupPrefix = prefix
220
+ return
221
+ }
222
+
223
+ // Ensure options is always treated as an object, even if it's undefined or a function
224
+ const effectiveOptions = typeof options === 'object' ? options : {}
187
225
 
188
- // run routes/web.ts
189
- // const webRoutesPath = projectPath('routes/web.ts')
190
- // await runCommand(`bun ${webRoutesPath}`)
226
+ this.groupPrefix = effectiveOptions.prefix ?? prefix ?? ''
227
+ }
191
228
 
192
- // set this.routes to a mapped array of routes that matches the pattern
229
+ private prepareGroupPrefix(options: string | RouteGroupOptions): void {
230
+ if (this.groupPrefix !== '' && typeof options !== 'string')
231
+ return this.setGroupPrefix(this.groupPrefix, options)
193
232
 
194
- return this.routes
233
+ if (typeof options === 'string')
234
+ return this.setGroupPrefix(options)
235
+
236
+ return this.setGroupPrefix('', options)
237
+ }
238
+
239
+ private async resolveCallback(callback: Route['callback']): Promise<Route['callback']> {
240
+ if (callback instanceof Promise) {
241
+ const actionModule = await callback
242
+ return actionModule.default
243
+ }
244
+
245
+ if (typeof callback === 'string')
246
+ return this.importCallbackFromPath(callback, this.path)
247
+
248
+ // in this case, the callback ends up being a function
249
+ return callback
250
+ }
251
+
252
+ private async importCallbackFromPath(callbackPath: string, originalPath: string): Promise<Route['callback']> {
253
+ let modulePath = callbackPath
254
+ let importPathFunction = p.appPath // Default import path function
255
+
256
+ if (callbackPath.startsWith('../'))
257
+ importPathFunction = p.routesPath
258
+
259
+ // Remove trailing .ts if present
260
+ modulePath = modulePath.endsWith('.ts') ? modulePath.slice(0, -3) : modulePath
261
+ const actionModule = await import(importPathFunction(`${modulePath}.ts`))
262
+
263
+ // Use custom path from action module if available
264
+ const newPath = actionModule.default.path ?? originalPath
265
+ this.updatePathIfNeeded(newPath, originalPath)
266
+
267
+ return actionModule.default.handle
268
+ }
269
+
270
+ private normalizePath(path: string): string {
271
+ return path.endsWith('/') ? path.slice(0, -1) : path
272
+ }
273
+
274
+ public prepareUri(path: string) {
275
+ // if string starts with / then remove it because we are adding it back in the next line
276
+ if (path.startsWith('/'))
277
+ path = path.slice(1)
278
+
279
+ path = `${this.apiPrefix}${this.groupPrefix}/${path}`
280
+
281
+ // if path ends in "/", then remove it
282
+ // e.g. triggered when route is "/"
283
+ return path.endsWith('/') ? path.slice(0, -1) : path
284
+ }
285
+
286
+ private updatePathIfNeeded(newPath: string, originalPath: string): void {
287
+ if (newPath !== originalPath) {
288
+ // Logic to update the path if needed, based on the action module's custom path
289
+ this.path = newPath
290
+ }
195
291
  }
196
292
  }
197
293
 
package/src/server.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { extname } from 'node:path'
2
1
  import { URL } from 'node:url'
2
+ import process from 'node:process'
3
+ import { log } from '@stacksjs/logging'
3
4
  import type { MiddlewareType, Route, StatusCode } from '@stacksjs/types'
4
5
  import { middlewares } from './middleware'
5
6
  import { request } from './request'
@@ -8,17 +9,22 @@ import { route } from '.'
8
9
  interface ServeOptions {
9
10
  host?: string
10
11
  port?: number
11
- tunnel?: boolean
12
+ debug?: boolean
13
+ timezone?: string
12
14
  }
13
15
 
14
16
  export async function serve(options: ServeOptions = {}) {
15
- // maybe make use of localUrl({ type: 'backend' }) here & options.tunnel
16
17
  const hostname = options.host || 'localhost'
17
18
  const port = options.port || 3000
19
+ const development = options.debug ? true : process.env.APP_ENV !== 'production' && process.env.APP_ENV !== 'prod'
20
+
21
+ if (options.timezone)
22
+ process.env.TZ = options.timezone
18
23
 
19
24
  Bun.serve({
20
25
  hostname,
21
26
  port,
27
+ development,
22
28
 
23
29
  fetch(req: Request) {
24
30
  return serverResponse(req)
@@ -27,24 +33,23 @@ export async function serve(options: ServeOptions = {}) {
27
33
  }
28
34
 
29
35
  export async function serverResponse(req: Request) {
30
- const timestamp = new Date().toISOString()
31
-
32
- // log the request in a structured way like Laravel does in the console
33
- // eslint-disable-next-line no-console
34
- console.log(`${timestamp} - Incoming request: ${req.method} ${req.url}`)
35
- // eslint-disable-next-line no-console
36
- console.log(`${timestamp} - Headers: ${JSON.stringify(req.headers)}`)
37
- // eslint-disable-next-line no-console
38
- console.log(`${timestamp} - Body: ${JSON.stringify(req.body)}`)
39
- // eslint-disable-next-line no-console
40
- console.log(`${timestamp} - Query: ${JSON.stringify(req.query)}`)
41
- // eslint-disable-next-line no-console
42
- console.log(`${timestamp} - Params: ${JSON.stringify(req.params)}`)
43
- // eslint-disable-next-line no-console
44
- console.log(`${timestamp} - Cookies: ${JSON.stringify(req.cookies)}`)
36
+ log.info(`Incoming Request: ${req.method} ${req.url}`)
37
+ log.info(`Headers: ${JSON.stringify(req.headers)}`)
38
+ log.info(`Body: ${JSON.stringify(req.body)}`)
39
+ // log.info(`Query: ${JSON.stringify(req.query)}`)
40
+ // log.info(`Params: ${JSON.stringify(req.params)}`)
41
+ // log.info(`Cookies: ${JSON.stringify(req.cookies)}`)
42
+
43
+ // Trim trailing slash from the URL if it's not the root '/'
44
+ // This automatically allows for route definitions, like
45
+ // '/about' and '/about/' to be treated as the same
46
+ const trimmedUrl = req.url.endsWith('/') && req.url.length > 1 ? req.url.slice(0, -1) : req.url
45
47
 
46
48
  const routesList: Route[] = await route.getRoutes()
47
- const url = new URL(req.url)
49
+ log.info(`Routes List: ${JSON.stringify(routesList)}`)
50
+
51
+ const url = new URL(trimmedUrl)
52
+ log.info(`URL: ${JSON.stringify(url)}`)
48
53
 
49
54
  const foundRoute: Route | undefined = routesList.find((route: Route) => {
50
55
  const pattern = new RegExp(`^${route.uri.replace(/:\w+/g, '\\w+')}$`)
@@ -52,6 +57,8 @@ export async function serverResponse(req: Request) {
52
57
  return pattern.test(url.pathname)
53
58
  })
54
59
 
60
+ log.info(`Found Route: ${JSON.stringify(foundRoute)}`)
61
+
55
62
  // if (url.pathname === '/favicon.ico')
56
63
  // return new Response('')
57
64