@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 +135 -42
- package/package.json +2 -1
- package/src/router.ts +150 -38
- package/src/server.ts +26 -19
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
398
|
+
// ../path/src/index.ts
|
|
399
399
|
import process2 from "process";
|
|
400
|
+
import os from "os";
|
|
400
401
|
|
|
401
|
-
//
|
|
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
|
|
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 =
|
|
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
|
-
//
|
|
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
|
-
|
|
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)}`);
|
|
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
|
-
|
|
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) &&
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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,
|
|
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
|
-
|
|
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):
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
227
|
+
await import(routesPath('api.ts'))
|
|
185
228
|
|
|
186
|
-
|
|
229
|
+
return this.routes
|
|
230
|
+
}
|
|
187
231
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|