@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 +106 -33
- package/package.json +2 -1
- package/src/router.ts +135 -39
- package/src/server.ts +26 -19
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
|
|
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 =
|
|
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
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
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
|
-
|
|
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) &&
|
|
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
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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,
|
|
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):
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
66
|
+
const uri = this.prepareUri(this.path)
|
|
67
|
+
log.debug(`Prepared URI: ${uri}`)
|
|
64
68
|
|
|
65
|
-
this.addRoute('GET',
|
|
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.
|
|
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:
|
|
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
|
-
|
|
156
|
+
if (!callback)
|
|
157
|
+
throw new Error('Missing callback function for your route group.')
|
|
158
|
+
|
|
159
|
+
cb = callback
|
|
134
160
|
|
|
135
|
-
|
|
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
|
-
|
|
211
|
+
await import(routesPath('api.ts'))
|
|
212
|
+
|
|
213
|
+
return this.routes
|
|
214
|
+
}
|
|
185
215
|
|
|
186
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
// await runCommand(`bun ${webRoutesPath}`)
|
|
226
|
+
this.groupPrefix = effectiveOptions.prefix ?? prefix ?? ''
|
|
227
|
+
}
|
|
191
228
|
|
|
192
|
-
|
|
229
|
+
private prepareGroupPrefix(options: string | RouteGroupOptions): void {
|
|
230
|
+
if (this.groupPrefix !== '' && typeof options !== 'string')
|
|
231
|
+
return this.setGroupPrefix(this.groupPrefix, options)
|
|
193
232
|
|
|
194
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
|
|
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
|
-
|
|
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
|
|