@stacksjs/router 0.58.71 → 0.58.73

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
@@ -17,7 +17,7 @@ var __toESM = (mod, isNodeMode, target) => {
17
17
  };
18
18
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
19
19
 
20
- // /home/runner/work/stacks/stacks/node_modules/slugify/slugify.js
20
+ // ../../../../node_modules/slugify/slugify.js
21
21
  var require_slugify = __commonJS((exports, module) => {
22
22
  (function(name, root, factory) {
23
23
  if (typeof exports === "object") {
@@ -68,7 +68,7 @@ var require_slugify = __commonJS((exports, module) => {
68
68
  });
69
69
  });
70
70
 
71
- // /home/runner/work/stacks/stacks/node_modules/pluralize/pluralize.js
71
+ // ../../../../node_modules/pluralize/pluralize.js
72
72
  var require_pluralize = __commonJS((exports, module) => {
73
73
  (function(root, pluralize) {
74
74
  if (typeof import.meta.require === "function" && typeof exports === "object" && typeof module === "object") {
@@ -395,10 +395,11 @@ var require_pluralize = __commonJS((exports, module) => {
395
395
  });
396
396
  });
397
397
 
398
- // /home/runner/work/stacks/stacks/storage/framework/core/path/src/index.ts
398
+ // ../path/src/index.ts
399
399
  import process2 from "process";
400
+ import os from "os";
400
401
 
401
- // /home/runner/work/stacks/stacks/node_modules/pathe/dist/shared/pathe.ff20891b.mjs
402
+ // ../../../../node_modules/pathe/dist/shared/pathe.ff20891b.mjs
402
403
  var normalizeWindowsPath = function(input = "") {
403
404
  if (!input) {
404
405
  return input;
@@ -552,7 +553,7 @@ var toNamespacedPath = function(p) {
552
553
  return normalizeWindowsPath(p);
553
554
  };
554
555
  var _EXTNAME_RE = /.(\.[^./]+)$/;
555
- var extname = function(p) {
556
+ var extname2 = function(p) {
556
557
  const match = _EXTNAME_RE.exec(normalizeWindowsPath(p));
557
558
  return match && match[1] || "";
558
559
  };
@@ -590,7 +591,7 @@ var basename = function(p, extension) {
590
591
  var parse = function(p) {
591
592
  const root = normalizeWindowsPath(p).split("/").shift() || "/";
592
593
  const base = basename(p);
593
- const extension = extname(base);
594
+ const extension = extname2(base);
594
595
  return {
595
596
  root,
596
597
  dir: dirname(p),
@@ -599,7 +600,7 @@ var parse = function(p) {
599
600
  name: base.slice(0, base.length - extension.length)
600
601
  };
601
602
  };
602
- // /home/runner/work/stacks/stacks/storage/framework/core/path/src/index.ts
603
+ // ../path/src/index.ts
603
604
  function actionsPath(path2) {
604
605
  return corePath(`actions/src/${path2 || ""}`);
605
606
  }
@@ -838,7 +839,10 @@ function queuePath(path2) {
838
839
  function realtimePath(path2) {
839
840
  return corePath(`realtime/${path2 || ""}`);
840
841
  }
841
- function resourcesPath(path2) {
842
+ function resourcesPath(path2, options) {
843
+ const absolutePath = projectStoragePath(`resources/${path2 || ""}`);
844
+ if (options?.relative)
845
+ return relative(process2.cwd(), absolutePath);
842
846
  return projectPath(`resources/${path2 || ""}`);
843
847
  }
844
848
  function replPath(path2) {
@@ -847,7 +851,10 @@ function replPath(path2) {
847
851
  function routerPath(path2) {
848
852
  return corePath(`router/${path2 || ""}`);
849
853
  }
850
- function routesPath(path2) {
854
+ function routesPath(path2, options) {
855
+ const absolutePath = resourcesPath(`routes/${path2 || ""}`);
856
+ if (options?.relative)
857
+ return relative(process2.cwd(), absolutePath);
851
858
  return projectPath(`routes/${path2 || ""}`);
852
859
  }
853
860
  function searchEnginePath(path2) {
@@ -889,6 +896,9 @@ function serverlessPath(path2) {
889
896
  function stacksPath(path2) {
890
897
  return frameworkPath(`src/${path2 || ""}`);
891
898
  }
899
+ function shellPath(path2) {
900
+ return corePath(`shell/${path2 || ""}`);
901
+ }
892
902
  function stringsPath(path2) {
893
903
  return corePath(`strings/${path2 || ""}`);
894
904
  }
@@ -919,6 +929,9 @@ function vitePath(path2) {
919
929
  function xRayPath(path2) {
920
930
  return frameworkPath(`stacks/x-ray/${path2 || ""}`);
921
931
  }
932
+ function homeDir(path2) {
933
+ return os.homedir() + (path2 ? (path2.startsWith("/") ? "" : "/") + path2 : "~");
934
+ }
922
935
  var path2 = {
923
936
  actionsPath,
924
937
  userActionsPath,
@@ -1005,6 +1018,7 @@ var path2 = {
1005
1018
  serverlessPath,
1006
1019
  stacksPath,
1007
1020
  stringsPath,
1021
+ shellPath,
1008
1022
  storesPath,
1009
1023
  testingPath,
1010
1024
  testsPath,
@@ -1021,10 +1035,11 @@ var path2 = {
1021
1035
  validationPath,
1022
1036
  vitePath,
1023
1037
  xRayPath,
1038
+ homeDir,
1024
1039
  basename,
1025
1040
  delimiter,
1026
1041
  dirname,
1027
- extname,
1042
+ extname: extname2,
1028
1043
  format,
1029
1044
  isAbsolute,
1030
1045
  join,
@@ -1084,33 +1099,38 @@ class Request {
1084
1099
  }
1085
1100
  var request = new Request;
1086
1101
  // src/server.ts
1087
- import {extname as extname2} from "path";
1088
1102
  import {URL} from "url";
1103
+ import process3 from "process";
1104
+ import {log} from "@stacksjs/logging";
1089
1105
  async function serve(options = {}) {
1090
1106
  const hostname = options.host || "localhost";
1091
1107
  const port = options.port || 3000;
1108
+ const development = options.debug ? true : process3.env.APP_ENV !== "production" && process3.env.APP_ENV !== "prod";
1109
+ if (options.timezone)
1110
+ process3.env.TZ = options.timezone;
1092
1111
  Bun.serve({
1093
1112
  hostname,
1094
1113
  port,
1114
+ development,
1095
1115
  fetch(req) {
1096
1116
  return serverResponse(req);
1097
1117
  }
1098
1118
  });
1099
1119
  }
1100
1120
  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)}`);
1121
+ log.info(`Incoming Request: ${req.method} ${req.url}`);
1122
+ log.info(`Headers: ${JSON.stringify(req.headers)}`);
1123
+ log.info(`Body: ${JSON.stringify(req.body)}`);
1124
+ const trimmedUrl = req.url.endsWith("/") && req.url.length > 1 ? req.url.slice(0, -1) : req.url;
1108
1125
  const routesList = await route.getRoutes();
1109
- const url = new URL(req.url);
1126
+ log.info(`Routes List: ${JSON.stringify(routesList)}`);
1127
+ const url = new URL(trimmedUrl);
1128
+ log.info(`URL: ${JSON.stringify(url)}`);
1110
1129
  const foundRoute = routesList.find((route2) => {
1111
1130
  const pattern = new RegExp(`^${route2.uri.replace(/:\w+/g, "\\w+")}\$`);
1112
1131
  return pattern.test(url.pathname);
1113
1132
  });
1133
+ log.info(`Found Route: ${JSON.stringify(foundRoute)}`);
1114
1134
  if (!foundRoute)
1115
1135
  return new Response("Pretty 404 page coming soon", { status: 404 });
1116
1136
  addRouteParamsAndQuery(url, foundRoute);
@@ -1152,7 +1172,7 @@ var execute = function(route2, request3, { statusCode }) {
1152
1172
  }
1153
1173
  if (route2?.method !== request3.method)
1154
1174
  return new Response("Method not allowed", { status: 405 });
1155
- if (isString(route2.callback) && extname2(route2.callback) === ".html") {
1175
+ if (isString(route2.callback) && extname(route2.callback) === ".html") {
1156
1176
  try {
1157
1177
  const fileContent = Bun.file(route2.callback);
1158
1178
  return new Response(fileContent, { headers: { "Content-Type": "text/html" } });
@@ -1188,9 +1208,12 @@ var isFunction = function(val) {
1188
1208
  var isObject = function(val) {
1189
1209
  return val !== null && typeof val === "object" && !Array.isArray(val);
1190
1210
  };
1191
- // /home/runner/work/stacks/stacks/storage/framework/core/strings/src/utils.ts
1211
+ // src/router.ts
1212
+ import {log as log2} from "@stacksjs/logging";
1213
+
1214
+ // ../strings/src/utils.ts
1192
1215
  var import_slugify = __toESM(require_slugify(), 1);
1193
- // /home/runner/work/stacks/stacks/node_modules/change-case/dist/index.js
1216
+ // ../../../../node_modules/change-case/dist/index.js
1194
1217
  function split(value) {
1195
1218
  let result = value.trim();
1196
1219
  result = result.replace(SPLIT_LOWER_UPPER_RE, SPLIT_REPLACE_VALUE).replace(SPLIT_UPPER_UPPER_RE, SPLIT_REPLACE_VALUE);
@@ -1271,7 +1294,7 @@ var SPLIT_SEPARATE_NUMBER_RE = /(\d)\p{Ll}|(\p{L})\d/u;
1271
1294
  var DEFAULT_STRIP_REGEXP = /[^\p{L}\d]+/giu;
1272
1295
  var SPLIT_REPLACE_VALUE = "$1\0$2";
1273
1296
  var DEFAULT_PREFIX_SUFFIX_CHARACTERS = "";
1274
- // /home/runner/work/stacks/stacks/node_modules/title-case/dist/index.js
1297
+ // ../../../../node_modules/title-case/dist/index.js
1275
1298
  var WORD_SEPARATORS = new Set(["\u2014", "\u2013", "-", "\u2015", "/"]);
1276
1299
  var SENTENCE_TERMINATORS = new Set([".", "!", "?"]);
1277
1300
  var TITLE_TERMINATORS = new Set([
@@ -1319,11 +1342,14 @@ var SMALL_WORDS = new Set([
1319
1342
  "without",
1320
1343
  "yet"
1321
1344
  ]);
1322
- // /home/runner/work/stacks/stacks/storage/framework/core/strings/src/pluralize.ts
1345
+ // ../strings/src/pluralize.ts
1323
1346
  var pluralize = __toESM(require_pluralize(), 1);
1324
1347
  // src/router.ts
1325
1348
  class Router {
1326
1349
  routes = [];
1350
+ apiPrefix = "/api";
1351
+ groupPrefix = "";
1352
+ path = "";
1327
1353
  addRoute(method, uri, callback, statusCode) {
1328
1354
  const name = uri.replace(/\//g, ".").replace(/:/g, "");
1329
1355
  const pattern = new RegExp(`^${uri.replace(/:[a-zA-Z]+/g, (_match) => {
@@ -1335,6 +1361,7 @@ class Router {
1335
1361
  } else {
1336
1362
  routeCallback = callback;
1337
1363
  }
1364
+ log2.debug(`Adding route: ${method} ${uri} with name ${name}`);
1338
1365
  this.routes.push({
1339
1366
  name,
1340
1367
  method,
@@ -1345,35 +1372,49 @@ class Router {
1345
1372
  statusCode,
1346
1373
  paramNames: []
1347
1374
  });
1375
+ return this;
1348
1376
  }
1349
1377
  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);
1378
+ this.path = this.normalizePath(path5);
1379
+ log2.debug(`Normalized Path: ${this.path}`);
1380
+ callback = await this.resolveCallback(callback);
1381
+ const uri = this.prepareUri(this.path);
1382
+ log2.debug(`Prepared URI: ${uri}`);
1383
+ return this.addRoute("GET", uri, callback, 200);
1384
+ }
1385
+ async email(path5) {
1386
+ path5 = pascalCase(path5);
1387
+ const emailModule = await import(path2.userNotificationsPath(`${path5}.ts`));
1388
+ const callback = emailModule.default.handle;
1389
+ const uri = this.prepareUri(path5);
1390
+ this.addRoute("GET", uri, callback, 200);
1359
1391
  return this;
1360
1392
  }
1361
1393
  async health() {
1362
1394
  const healthModule = await import(path2.userActionsPath("HealthAction.ts"));
1363
1395
  const callback = healthModule.default.handle;
1364
- this.addRoute("GET", "/api/health", callback, 200);
1396
+ const path5 = healthModule.default.path ?? `${this.apiPrefix}/health`;
1397
+ this.addRoute("GET", path5, callback, 200);
1365
1398
  return this;
1366
1399
  }
1367
1400
  async job(path5) {
1368
- if (path5.includes("api/"))
1369
- path5 = path5.replace(/^\/api\//, "");
1370
1401
  path5 = pascalCase(path5);
1371
1402
  const jobModule = await import(path2.userJobsPath(`${path5}Job.ts`.replace(/JobJob/, "Job")));
1372
1403
  const callback = jobModule.default.handle;
1404
+ path5 = this.prepareUri(path5);
1405
+ this.addRoute("GET", path5, callback, 200);
1406
+ return this;
1407
+ }
1408
+ async action(path5) {
1409
+ path5 = pascalCase(path5);
1410
+ const actionModule = await import(path2.userActionsPath(`${path5}Action.ts`.replace(/ActionAction/, "Action")));
1411
+ const callback = actionModule.default.handle;
1412
+ path5 = this.prepareUri(path5);
1373
1413
  this.addRoute("GET", path5, callback, 200);
1374
1414
  return this;
1375
1415
  }
1376
1416
  post(path5, callback) {
1417
+ path5 = this.prepareUri(path5);
1377
1418
  this.addRoute("POST", path5, callback, 201);
1378
1419
  return this;
1379
1420
  }
@@ -1398,16 +1439,18 @@ class Router {
1398
1439
  return this;
1399
1440
  }
1400
1441
  group(options, callback) {
1442
+ if (typeof options === "string")
1443
+ options = options.startsWith("/") ? options.slice(1) : options;
1401
1444
  let cb;
1445
+ this.prepareGroupPrefix(options);
1402
1446
  if (typeof options === "function") {
1403
1447
  cb = options;
1404
1448
  options = {};
1405
- } else {
1406
- if (!callback)
1407
- throw new Error("Missing callback function for route group.");
1408
- cb = callback;
1409
1449
  }
1410
- const { prefix = "", middleware: middleware2 = [] } = options;
1450
+ if (!callback)
1451
+ throw new Error("Missing callback function for your route group.");
1452
+ cb = callback;
1453
+ const { prefix, middleware: middleware2 = [] } = options;
1411
1454
  const originalRoutes = this.routes;
1412
1455
  this.routes = [];
1413
1456
  cb();
@@ -1434,9 +1477,59 @@ class Router {
1434
1477
  return this;
1435
1478
  }
1436
1479
  async getRoutes() {
1437
- await import(projectPath("routes/api.ts"));
1480
+ await import(routesPath("api.ts"));
1438
1481
  return this.routes;
1439
1482
  }
1483
+ setGroupPrefix(prefix, options = {}) {
1484
+ if (prefix !== "") {
1485
+ prefix = `/${this.groupPrefix}/${prefix}`.replace(/\/\//g, "/");
1486
+ this.groupPrefix = prefix;
1487
+ return;
1488
+ }
1489
+ const effectiveOptions = typeof options === "object" ? options : {};
1490
+ this.groupPrefix = effectiveOptions.prefix ?? prefix ?? "";
1491
+ }
1492
+ prepareGroupPrefix(options) {
1493
+ if (this.groupPrefix !== "" && typeof options !== "string")
1494
+ return this.setGroupPrefix(this.groupPrefix, options);
1495
+ if (typeof options === "string")
1496
+ return this.setGroupPrefix(options);
1497
+ return this.setGroupPrefix("", options);
1498
+ }
1499
+ async resolveCallback(callback) {
1500
+ if (callback instanceof Promise) {
1501
+ const actionModule = await callback;
1502
+ return actionModule.default;
1503
+ }
1504
+ if (typeof callback === "string")
1505
+ return this.importCallbackFromPath(callback, this.path);
1506
+ return callback;
1507
+ }
1508
+ async importCallbackFromPath(callbackPath, originalPath) {
1509
+ let modulePath = callbackPath;
1510
+ let importPathFunction = path2.appPath;
1511
+ if (callbackPath.startsWith("../"))
1512
+ importPathFunction = path2.routesPath;
1513
+ modulePath = modulePath.endsWith(".ts") ? modulePath.slice(0, -3) : modulePath;
1514
+ const actionModule = await import(importPathFunction(`${modulePath}.ts`));
1515
+ const newPath = actionModule.default.path ?? originalPath;
1516
+ this.updatePathIfNeeded(newPath, originalPath);
1517
+ return actionModule.default.handle;
1518
+ }
1519
+ normalizePath(path5) {
1520
+ return path5.endsWith("/") ? path5.slice(0, -1) : path5;
1521
+ }
1522
+ prepareUri(path5) {
1523
+ if (path5.startsWith("/"))
1524
+ path5 = path5.slice(1);
1525
+ path5 = `${this.apiPrefix}${this.groupPrefix}/${path5}`;
1526
+ return path5.endsWith("/") ? path5.slice(0, -1) : path5;
1527
+ }
1528
+ updatePathIfNeeded(newPath, originalPath) {
1529
+ if (newPath !== originalPath) {
1530
+ this.path = newPath;
1531
+ }
1532
+ }
1440
1533
  }
1441
1534
  var route = new Router;
1442
1535
  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.73",
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,11 @@ 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
+ email: (url: Route['url']) => Promise<this>
17
+ health: () => Promise<this>
18
+ job: (url: Route['url']) => Promise<this>
19
+ action: (url: Route['url']) => Promise<this>
20
+ group: (options: Prefix | RouteGroupOptions, callback: () => void) => this
14
21
  name: (name: string) => this
15
22
  middleware: (middleware: Route['middleware']) => this
16
23
  getRoutes: () => Promise<Route[]>
@@ -18,8 +25,11 @@ export interface RouterInterface {
18
25
 
19
26
  export class Router implements RouterInterface {
20
27
  private routes: Route[] = []
28
+ private apiPrefix = '/api'
29
+ private groupPrefix = ''
30
+ private path = ''
21
31
 
22
- private addRoute(method: Route['method'], uri: string, callback: Route['callback'] | string | object, statusCode: StatusCode): void {
32
+ private addRoute(method: Route['method'], uri: string, callback: Route['callback'] | string | object, statusCode: StatusCode): this {
23
33
  const name = uri.replace(/\//g, '.').replace(/:/g, '') // we can improve this
24
34
  const pattern = new RegExp(`^${uri.replace(/:[a-zA-Z]+/g, (_match) => {
25
35
  return '([a-zA-Z0-9-]+)'
@@ -35,6 +45,8 @@ export class Router implements RouterInterface {
35
45
  routeCallback = callback
36
46
  }
37
47
 
48
+ log.debug(`Adding route: ${method} ${uri} with name ${name}`)
49
+
38
50
  this.routes.push({
39
51
  name,
40
52
  method,
@@ -45,24 +57,31 @@ export class Router implements RouterInterface {
45
57
  statusCode,
46
58
  paramNames: [],
47
59
  })
60
+
61
+ return this
48
62
  }
49
63
 
50
64
  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
- }
65
+ this.path = this.normalizePath(path)
66
+ log.debug(`Normalized Path: ${this.path}`)
56
67
 
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`))
68
+ callback = await this.resolveCallback(callback)
60
69
 
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
- }
70
+ const uri = this.prepareUri(this.path)
71
+ log.debug(`Prepared URI: ${uri}`)
72
+
73
+ return this.addRoute('GET', uri, callback, 200)
74
+ }
75
+
76
+ public async email(path: Route['url']): Promise<this> {
77
+ path = pascalCase(path)
78
+
79
+ const emailModule = await import(p.userNotificationsPath(`${path}.ts`))
80
+ const callback = emailModule.default.handle
81
+
82
+ const uri = this.prepareUri(path)
83
+ this.addRoute('GET', uri, callback, 200)
64
84
 
65
- this.addRoute('GET', path, callback, 200)
66
85
  return this
67
86
  }
68
87
 
@@ -70,35 +89,55 @@ export class Router implements RouterInterface {
70
89
  const healthModule = await import(p.userActionsPath('HealthAction.ts'))
71
90
  const callback = healthModule.default.handle
72
91
 
73
- this.addRoute('GET', '/api/health', callback, 200)
92
+ const path = healthModule.default.path ?? `${this.apiPrefix}/health`
93
+
94
+ this.addRoute('GET', path, callback, 200)
95
+
74
96
  return this
75
97
  }
76
98
 
77
99
  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
100
  path = pascalCase(path)
101
+
82
102
  // removes the potential `JobJob` suffix in case the user does not choose to use the Job suffix in their file name
83
103
  const jobModule = await import(p.userJobsPath(`${path}Job.ts`.replace(/JobJob/, 'Job')))
84
104
  const callback = jobModule.default.handle
85
105
 
106
+ path = this.prepareUri(path)
86
107
  this.addRoute('GET', path, callback, 200)
108
+
109
+ return this
110
+ }
111
+
112
+ public async action(path: Route['url']): Promise<this> {
113
+ path = pascalCase(path) // actions are PascalCase
114
+
115
+ // removes the potential `ActionAction` suffix in case the user does not choose to use the Job suffix in their file name
116
+ const actionModule = await import(p.userActionsPath(`${path}Action.ts`.replace(/ActionAction/, 'Action')))
117
+ const callback = actionModule.default.handle
118
+
119
+ path = this.prepareUri(path)
120
+ this.addRoute('GET', path, callback, 200)
121
+
87
122
  return this
88
123
  }
89
124
 
90
125
  public post(path: Route['url'], callback: Route['callback']): this {
126
+ path = this.prepareUri(path)
91
127
  this.addRoute('POST', path, callback, 201)
128
+
92
129
  return this
93
130
  }
94
131
 
95
132
  public view(path: Route['url'], callback: Route['callback']): this {
96
133
  this.addRoute('GET', path, callback, 200)
134
+
97
135
  return this
98
136
  }
99
137
 
100
138
  public redirect(path: Route['url'], callback: Route['callback'], _status?: RedirectCode): this {
101
139
  this.addRoute('GET', path, callback, 302)
140
+
102
141
  return this
103
142
  }
104
143
 
@@ -117,37 +156,41 @@ export class Router implements RouterInterface {
117
156
  return this
118
157
  }
119
158
 
120
- public group(options: RouteGroupOptions | (() => void), callback?: () => void): this {
159
+ public group(options: string | RouteGroupOptions, callback?: () => void): this {
160
+ if (typeof options === 'string')
161
+ options = options.startsWith('/') ? options.slice(1) : options
162
+
121
163
  let cb: () => void
122
164
 
165
+ this.prepareGroupPrefix(options)
166
+
123
167
  if (typeof options === 'function') {
124
168
  cb = options
125
169
  options = {}
126
170
  }
127
- else {
128
- if (!callback)
129
- throw new Error('Missing callback function for route group.')
130
- cb = callback
131
- }
132
171
 
133
- const { prefix = '', middleware = [] } = options
172
+ if (!callback)
173
+ throw new Error('Missing callback function for your route group.')
174
+
175
+ cb = callback
176
+
177
+ const { prefix, middleware = [] } = options as RouteGroupOptions
134
178
 
135
- // Save a reference to the original routes array.
179
+ // Save a reference to the original routes array
136
180
  const originalRoutes = this.routes
137
181
 
138
- // Create a new routes array for the duration of the callback.
182
+ // Create a new routes array for the duration of the callback
139
183
  this.routes = []
140
184
 
141
- // Execute the callback. This will add routes to the new this.routes array.
185
+ // Execute the callback. This will add routes to the new this.routes array
142
186
  cb()
143
187
 
144
- // For each route added by the callback, adjust the URI and add to the original routes array.
188
+ // For each route added by the callback, adjust the URI and add to the original routes array
145
189
  this.routes.forEach((r) => {
146
190
  r.uri = `${prefix}${r.uri}`
147
191
 
148
192
  if (middleware.length)
149
193
  r.middleware = middleware
150
- // Assuming you have a middleware property for each route.
151
194
 
152
195
  originalRoutes.push(r)
153
196
  return this
@@ -181,17 +224,86 @@ export class Router implements RouterInterface {
181
224
  }
182
225
 
183
226
  public async getRoutes(): Promise<Route[]> {
184
- // const routeFileData = (await readTextFile(projectPath('routes/web.ts'))).data
227
+ await import(routesPath('api.ts'))
185
228
 
186
- await import(projectPath('routes/api.ts'))
229
+ return this.routes
230
+ }
187
231
 
188
- // run routes/web.ts
189
- // const webRoutesPath = projectPath('routes/web.ts')
190
- // await runCommand(`bun ${webRoutesPath}`)
232
+ private setGroupPrefix(prefix: string, options: RouteGroupOptions = {}) {
233
+ if (prefix !== '') {
234
+ prefix = `/${this.groupPrefix}/${prefix}`.replace(/\/\//g, '/') // remove double slashes in case there are any
235
+ this.groupPrefix = prefix
236
+ return
237
+ }
191
238
 
192
- // set this.routes to a mapped array of routes that matches the pattern
239
+ // Ensure options is always treated as an object, even if it's undefined or a function
240
+ const effectiveOptions = typeof options === 'object' ? options : {}
193
241
 
194
- return this.routes
242
+ this.groupPrefix = effectiveOptions.prefix ?? prefix ?? ''
243
+ }
244
+
245
+ private prepareGroupPrefix(options: string | RouteGroupOptions): void {
246
+ if (this.groupPrefix !== '' && typeof options !== 'string')
247
+ return this.setGroupPrefix(this.groupPrefix, options)
248
+
249
+ if (typeof options === 'string')
250
+ return this.setGroupPrefix(options)
251
+
252
+ return this.setGroupPrefix('', options)
253
+ }
254
+
255
+ private async resolveCallback(callback: Route['callback']): Promise<Route['callback']> {
256
+ if (callback instanceof Promise) {
257
+ const actionModule = await callback
258
+ return actionModule.default
259
+ }
260
+
261
+ if (typeof callback === 'string')
262
+ return this.importCallbackFromPath(callback, this.path)
263
+
264
+ // in this case, the callback ends up being a function
265
+ return callback
266
+ }
267
+
268
+ private async importCallbackFromPath(callbackPath: string, originalPath: string): Promise<Route['callback']> {
269
+ let modulePath = callbackPath
270
+ let importPathFunction = p.appPath // Default import path function
271
+
272
+ if (callbackPath.startsWith('../'))
273
+ importPathFunction = p.routesPath
274
+
275
+ // Remove trailing .ts if present
276
+ modulePath = modulePath.endsWith('.ts') ? modulePath.slice(0, -3) : modulePath
277
+ const actionModule = await import(importPathFunction(`${modulePath}.ts`))
278
+
279
+ // Use custom path from action module if available
280
+ const newPath = actionModule.default.path ?? originalPath
281
+ this.updatePathIfNeeded(newPath, originalPath)
282
+
283
+ return actionModule.default.handle
284
+ }
285
+
286
+ private normalizePath(path: string): string {
287
+ return path.endsWith('/') ? path.slice(0, -1) : path
288
+ }
289
+
290
+ public prepareUri(path: string) {
291
+ // if string starts with / then remove it because we are adding it back in the next line
292
+ if (path.startsWith('/'))
293
+ path = path.slice(1)
294
+
295
+ path = `${this.apiPrefix}${this.groupPrefix}/${path}`
296
+
297
+ // if path ends in "/", then remove it
298
+ // e.g. triggered when route is "/"
299
+ return path.endsWith('/') ? path.slice(0, -1) : path
300
+ }
301
+
302
+ private updatePathIfNeeded(newPath: string, originalPath: string): void {
303
+ if (newPath !== originalPath) {
304
+ // Logic to update the path if needed, based on the action module's custom path
305
+ this.path = newPath
306
+ }
195
307
  }
196
308
  }
197
309
 
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