@stacksjs/router 0.61.17 → 0.61.19
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 +107 -78
- package/package.json +3 -3
- package/src/index.ts +1 -0
- package/src/request.ts +11 -1
- package/src/router.ts +76 -59
- package/src/server.ts +77 -48
- package/src/utils.ts +9 -0
package/dist/index.js
CHANGED
|
@@ -30,6 +30,9 @@ class Request {
|
|
|
30
30
|
addQuery(url) {
|
|
31
31
|
this.query = Object.fromEntries(url.searchParams);
|
|
32
32
|
}
|
|
33
|
+
addParam(param) {
|
|
34
|
+
this.params = param;
|
|
35
|
+
}
|
|
33
36
|
get(element) {
|
|
34
37
|
return this.query[element];
|
|
35
38
|
}
|
|
@@ -48,15 +51,18 @@ class Request {
|
|
|
48
51
|
if (match?.groups)
|
|
49
52
|
this.params = match.groups;
|
|
50
53
|
}
|
|
51
|
-
|
|
54
|
+
getParam(key) {
|
|
52
55
|
return this.params ? this.params[key] || null : null;
|
|
53
56
|
}
|
|
57
|
+
getParams() {
|
|
58
|
+
return this.params;
|
|
59
|
+
}
|
|
54
60
|
getParamAsInt(key) {
|
|
55
61
|
const value = this.params ? this.params[key] || null : null;
|
|
56
62
|
return value ? Number.parseInt(value) : null;
|
|
57
63
|
}
|
|
58
64
|
}
|
|
59
|
-
var
|
|
65
|
+
var request2 = new Request;
|
|
60
66
|
// src/server.ts
|
|
61
67
|
import process from "process";
|
|
62
68
|
import {log} from "@stacksjs/logging";
|
|
@@ -82,47 +88,50 @@ async function serverResponse(req) {
|
|
|
82
88
|
log.info(`Body: ${JSON.stringify(req.body)}`);
|
|
83
89
|
const trimmedUrl = req.url.endsWith("/") && req.url.length > 1 ? req.url.slice(0, -1) : req.url;
|
|
84
90
|
const url = new URL(trimmedUrl);
|
|
85
|
-
addRouteParamsAndQuery(url);
|
|
86
91
|
const routesList = await route.getRoutes();
|
|
87
92
|
log.info(`Routes List: ${JSON.stringify(routesList)}`);
|
|
88
93
|
log.info(`URL: ${JSON.stringify(url)}`);
|
|
89
|
-
const foundRoute = routesList.
|
|
90
|
-
const pattern = new RegExp(`^${route2.uri.replace(
|
|
94
|
+
const foundRoute = routesList.filter((route2) => {
|
|
95
|
+
const pattern = new RegExp(`^${route2.uri.replace(/\{(\w+)\}/g, "(\\w+)")}\$`);
|
|
91
96
|
return pattern.test(url.pathname);
|
|
92
|
-
});
|
|
97
|
+
}).find((route2) => route2.method === req.method);
|
|
93
98
|
log.info(`Found Route: ${JSON.stringify(foundRoute)}`);
|
|
94
99
|
if (!foundRoute)
|
|
95
100
|
return new Response("Pretty 404 page coming soon", { status: 404 });
|
|
101
|
+
const routeParams = extractDynamicSegments(foundRoute.uri, url.pathname);
|
|
102
|
+
addRouteQuery(url);
|
|
103
|
+
addRouteParam(routeParams);
|
|
96
104
|
await executeMiddleware(foundRoute);
|
|
97
105
|
return await execute(foundRoute, req, { statusCode: foundRoute?.statusCode });
|
|
98
106
|
}
|
|
99
|
-
var
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
107
|
+
var extractDynamicSegments = function(routePattern, path3) {
|
|
108
|
+
const regexPattern = new RegExp(`^${routePattern.replace(/\{(\w+)\}/g, "(\\w+)")}\$`);
|
|
109
|
+
const match = path3.match(regexPattern);
|
|
110
|
+
if (!match) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const dynamicSegmentNames = [...routePattern.matchAll(/\{(\w+)\}/g)].map((m) => m[1]);
|
|
114
|
+
const dynamicSegmentValues = match.slice(1);
|
|
115
|
+
const dynamicSegments = {};
|
|
116
|
+
dynamicSegmentNames.forEach((name, index) => {
|
|
117
|
+
dynamicSegments[name] = dynamicSegmentValues[index];
|
|
118
|
+
});
|
|
119
|
+
return dynamicSegments;
|
|
112
120
|
};
|
|
113
|
-
async function execute(
|
|
121
|
+
async function execute(foundRoute, req, { statusCode }) {
|
|
122
|
+
const foundCallback = await route.resolveCallback(foundRoute.callback);
|
|
114
123
|
if (!statusCode)
|
|
115
124
|
statusCode = 200;
|
|
116
|
-
if (
|
|
117
|
-
const callback = String(
|
|
125
|
+
if (foundRoute?.method === "GET" && (statusCode === 301 || statusCode === 302)) {
|
|
126
|
+
const callback = String(foundCallback);
|
|
118
127
|
const response = Response.redirect(callback, statusCode);
|
|
119
128
|
return await noCache(response);
|
|
120
129
|
}
|
|
121
|
-
if (
|
|
130
|
+
if (foundRoute?.method !== req.method)
|
|
122
131
|
return new Response("Method not allowed", { status: 405 });
|
|
123
|
-
if (isString(
|
|
132
|
+
if (isString(foundCallback) && extname(foundCallback) === ".html") {
|
|
124
133
|
try {
|
|
125
|
-
const fileContent = Bun.file(
|
|
134
|
+
const fileContent = Bun.file(foundCallback);
|
|
126
135
|
return await new Response(fileContent, {
|
|
127
136
|
headers: { "Content-Type": "text/html" }
|
|
128
137
|
});
|
|
@@ -130,14 +139,14 @@ async function execute(route2, req, { statusCode }) {
|
|
|
130
139
|
return await new Response("Error reading the HTML file", { status: 500 });
|
|
131
140
|
}
|
|
132
141
|
}
|
|
133
|
-
if (isString(
|
|
134
|
-
return await new Response(
|
|
135
|
-
if (isFunction(
|
|
136
|
-
const result =
|
|
142
|
+
if (isString(foundCallback))
|
|
143
|
+
return await new Response(foundCallback);
|
|
144
|
+
if (isFunction(foundCallback)) {
|
|
145
|
+
const result = foundCallback();
|
|
137
146
|
return await new Response(JSON.stringify(result));
|
|
138
147
|
}
|
|
139
|
-
if (isObject(
|
|
140
|
-
return await new Response(JSON.stringify(
|
|
148
|
+
if (isObject(foundCallback))
|
|
149
|
+
return await new Response(JSON.stringify(foundCallback));
|
|
141
150
|
return await new Response("Unknown callback type.", { status: 500 });
|
|
142
151
|
}
|
|
143
152
|
var noCache = function(response) {
|
|
@@ -146,6 +155,23 @@ var noCache = function(response) {
|
|
|
146
155
|
response.headers.set("Expires", "0");
|
|
147
156
|
return response;
|
|
148
157
|
};
|
|
158
|
+
var addRouteQuery = function(url) {
|
|
159
|
+
if (!isObjectNotEmpty(url.searchParams))
|
|
160
|
+
request2.addQuery(url);
|
|
161
|
+
};
|
|
162
|
+
var addRouteParam = function(param) {
|
|
163
|
+
request2.addParam(param);
|
|
164
|
+
};
|
|
165
|
+
var executeMiddleware = function(route2) {
|
|
166
|
+
const { middleware: middleware2 = null } = route2;
|
|
167
|
+
if (middleware2 && middlewares && isObjectNotEmpty(middlewares)) {
|
|
168
|
+
if (isString(middleware2)) {
|
|
169
|
+
} else {
|
|
170
|
+
middleware2.forEach(() => {
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
149
175
|
var isString = function(val) {
|
|
150
176
|
return typeof val === "string";
|
|
151
177
|
};
|
|
@@ -156,12 +182,12 @@ var isFunction = function(val) {
|
|
|
156
182
|
return typeof val === "function";
|
|
157
183
|
};
|
|
158
184
|
var isObject = function(val) {
|
|
159
|
-
return
|
|
185
|
+
return typeof val === "object";
|
|
160
186
|
};
|
|
161
187
|
// src/router.ts
|
|
162
188
|
import {log as log2} from "@stacksjs/logging";
|
|
163
189
|
import {path as p, projectStoragePath, routesPath} from "@stacksjs/path";
|
|
164
|
-
import {pascalCase} from "@stacksjs/strings";
|
|
190
|
+
import {kebabCase, pascalCase} from "@stacksjs/strings";
|
|
165
191
|
|
|
166
192
|
class Router {
|
|
167
193
|
routes = [];
|
|
@@ -173,99 +199,94 @@ class Router {
|
|
|
173
199
|
const pattern = new RegExp(`^${uri.replace(/:[a-zA-Z]+/g, (_match) => {
|
|
174
200
|
return "([a-zA-Z0-9-]+)";
|
|
175
201
|
})}\$`);
|
|
176
|
-
let routeCallback;
|
|
177
|
-
if (typeof callback === "string" || typeof callback === "object") {
|
|
178
|
-
routeCallback = () => callback;
|
|
179
|
-
} else {
|
|
180
|
-
routeCallback = callback;
|
|
181
|
-
}
|
|
182
202
|
log2.debug(`Adding route: ${method} ${uri} with name ${name}`);
|
|
183
203
|
this.routes.push({
|
|
184
204
|
name,
|
|
185
205
|
method,
|
|
186
206
|
url: uri,
|
|
187
207
|
uri,
|
|
188
|
-
callback
|
|
208
|
+
callback,
|
|
189
209
|
pattern,
|
|
190
210
|
statusCode,
|
|
191
211
|
paramNames: []
|
|
192
212
|
});
|
|
193
213
|
return this;
|
|
194
214
|
}
|
|
195
|
-
|
|
215
|
+
get(path4, callback) {
|
|
196
216
|
this.path = this.normalizePath(path4);
|
|
197
217
|
log2.debug(`Normalized Path: ${this.path}`);
|
|
198
|
-
callback = await this.resolveCallback(callback);
|
|
199
218
|
const uri = this.prepareUri(this.path);
|
|
200
219
|
log2.debug(`Prepared URI: ${uri}`);
|
|
201
220
|
return this.addRoute("GET", uri, callback, 200);
|
|
202
221
|
}
|
|
203
222
|
async email(path4) {
|
|
204
223
|
path4 = pascalCase(path4);
|
|
205
|
-
const emailModule = await import(p.userNotificationsPath(`${path4}.ts`));
|
|
206
|
-
const callback = emailModule.
|
|
224
|
+
const emailModule = (await import(p.userNotificationsPath(`${path4}.ts`))).default;
|
|
225
|
+
const callback = emailModule.handle;
|
|
207
226
|
const uri = this.prepareUri(path4);
|
|
208
227
|
this.addRoute("GET", uri, callback, 200);
|
|
209
228
|
return this;
|
|
210
229
|
}
|
|
211
230
|
async health() {
|
|
212
|
-
const healthModule = await import(p.userActionsPath("HealthAction.ts"));
|
|
213
|
-
const callback = healthModule.
|
|
214
|
-
const path4 = healthModule.
|
|
231
|
+
const healthModule = (await import(p.userActionsPath("HealthAction.ts"))).default;
|
|
232
|
+
const callback = healthModule.handle;
|
|
233
|
+
const path4 = healthModule.path ?? `${this.apiPrefix}/health`;
|
|
215
234
|
this.addRoute("GET", path4, callback, 200);
|
|
216
235
|
return this;
|
|
217
236
|
}
|
|
218
237
|
async job(path4) {
|
|
219
238
|
path4 = pascalCase(path4);
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
path4 = this.prepareUri(path4);
|
|
223
|
-
this.addRoute("GET", path4, callback, 200);
|
|
224
|
-
return this;
|
|
239
|
+
const job = (await import(p.userJobsPath(`${path4}.ts`))).default;
|
|
240
|
+
return this.addRoute("GET", this.prepareUri(path4), job.handle, 200);
|
|
225
241
|
}
|
|
226
242
|
async action(path4) {
|
|
243
|
+
if (path4?.endsWith(".ts")) {
|
|
244
|
+
const action = (await import(p.userActionsPath(path4))).default;
|
|
245
|
+
path4 = action.path ?? kebabCase(path4);
|
|
246
|
+
return this.addRoute(action.method ?? "GET", path4, action.handle, 200);
|
|
247
|
+
}
|
|
227
248
|
path4 = pascalCase(path4);
|
|
228
|
-
|
|
249
|
+
const userActionsPath = p.userActionsPath(`${path4}.ts`);
|
|
229
250
|
try {
|
|
230
|
-
const
|
|
231
|
-
|
|
251
|
+
const action = (await import(userActionsPath)).default;
|
|
252
|
+
return this.addRoute(action.method ?? "GET", this.prepareUri(path4), action.handle, 200);
|
|
232
253
|
} catch (error) {
|
|
233
254
|
try {
|
|
234
|
-
const
|
|
235
|
-
|
|
255
|
+
const action = (await import(p.userActionsPath(`${path4}.ts`))).default;
|
|
256
|
+
return this.addRoute(action.method ?? "GET", this.prepareUri(path4), action.handle, 200);
|
|
236
257
|
} catch (error2) {
|
|
237
|
-
log2.error(`Could not find
|
|
258
|
+
log2.error(`Could not find Action for path: ${path4}`);
|
|
238
259
|
return this;
|
|
239
260
|
}
|
|
240
261
|
}
|
|
241
|
-
path4 = this.prepareUri(path4);
|
|
242
|
-
this.addRoute("GET", path4, callback, 200);
|
|
243
|
-
return this;
|
|
244
262
|
}
|
|
245
263
|
post(path4, callback) {
|
|
246
|
-
|
|
247
|
-
this.
|
|
248
|
-
return this;
|
|
264
|
+
this.path = this.normalizePath(path4);
|
|
265
|
+
const uri = this.prepareUri(this.path);
|
|
266
|
+
return this.addRoute("POST", uri, callback, 201);
|
|
249
267
|
}
|
|
250
268
|
view(path4, callback) {
|
|
251
|
-
this.
|
|
252
|
-
|
|
269
|
+
this.path = this.normalizePath(path4);
|
|
270
|
+
const uri = this.prepareUri(this.path);
|
|
271
|
+
return this.addRoute("GET", uri, callback, 200);
|
|
253
272
|
}
|
|
254
273
|
redirect(path4, callback, _status) {
|
|
255
|
-
this.addRoute("GET", path4, callback, 302);
|
|
256
|
-
return this;
|
|
274
|
+
return this.addRoute("GET", path4, callback, 302);
|
|
257
275
|
}
|
|
258
276
|
delete(path4, callback) {
|
|
259
|
-
this.addRoute("DELETE", path4, callback, 204);
|
|
260
|
-
return this;
|
|
277
|
+
return this.addRoute("DELETE", this.prepareUri(path4), callback, 204);
|
|
261
278
|
}
|
|
262
279
|
patch(path4, callback) {
|
|
263
|
-
this.
|
|
264
|
-
|
|
280
|
+
this.path = this.normalizePath(path4);
|
|
281
|
+
log2.debug(`Normalized Path: ${this.path}`);
|
|
282
|
+
const uri = this.prepareUri(this.path);
|
|
283
|
+
log2.debug(`Prepared URI: ${uri}`);
|
|
284
|
+
return this.addRoute("PATCH", uri, callback, 202);
|
|
265
285
|
}
|
|
266
286
|
put(path4, callback) {
|
|
267
|
-
this.
|
|
268
|
-
|
|
287
|
+
this.path = this.normalizePath(path4);
|
|
288
|
+
const uri = this.prepareUri(this.path);
|
|
289
|
+
return this.addRoute("PUT", uri, callback, 202);
|
|
269
290
|
}
|
|
270
291
|
group(options, callback) {
|
|
271
292
|
if (typeof options === "string")
|
|
@@ -338,7 +359,7 @@ class Router {
|
|
|
338
359
|
}
|
|
339
360
|
if (typeof callback === "string")
|
|
340
361
|
return await this.importCallbackFromPath(callback, this.path);
|
|
341
|
-
return
|
|
362
|
+
return callback;
|
|
342
363
|
}
|
|
343
364
|
async importCallbackFromPath(callbackPath, originalPath) {
|
|
344
365
|
let modulePath = callbackPath;
|
|
@@ -356,7 +377,9 @@ class Router {
|
|
|
356
377
|
}
|
|
357
378
|
const newPath = actionModule.default.path ?? originalPath;
|
|
358
379
|
this.updatePathIfNeeded(newPath, originalPath);
|
|
359
|
-
|
|
380
|
+
if (condition)
|
|
381
|
+
return await actionModule.default.handle();
|
|
382
|
+
return await actionModule.default.handle(request);
|
|
360
383
|
}
|
|
361
384
|
normalizePath(path4) {
|
|
362
385
|
return path4.endsWith("/") ? path4.slice(0, -1) : path4;
|
|
@@ -365,7 +388,6 @@ class Router {
|
|
|
365
388
|
if (path4.startsWith("/"))
|
|
366
389
|
path4 = path4.slice(1);
|
|
367
390
|
path4 = `${this.apiPrefix}${this.groupPrefix}/${path4}`;
|
|
368
|
-
console.log(path4);
|
|
369
391
|
return path4.endsWith("/") ? path4.slice(0, -1) : path4;
|
|
370
392
|
}
|
|
371
393
|
updatePathIfNeeded(newPath, originalPath) {
|
|
@@ -375,12 +397,19 @@ class Router {
|
|
|
375
397
|
}
|
|
376
398
|
}
|
|
377
399
|
var route = new Router;
|
|
400
|
+
// src/utils.ts
|
|
401
|
+
async function listRoutes() {
|
|
402
|
+
const routeLists = await route.getRoutes();
|
|
403
|
+
console.table(routeLists);
|
|
404
|
+
return ok("Successfully listed routes!");
|
|
405
|
+
}
|
|
378
406
|
export {
|
|
379
407
|
serverResponse,
|
|
380
408
|
serve,
|
|
381
409
|
route,
|
|
382
|
-
request,
|
|
410
|
+
request2 as request,
|
|
383
411
|
middlewares,
|
|
412
|
+
listRoutes,
|
|
384
413
|
Router,
|
|
385
414
|
Request,
|
|
386
415
|
Middleware
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacksjs/router",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.61.
|
|
4
|
+
"version": "0.61.19",
|
|
5
5
|
"description": "The Stacks framework router.",
|
|
6
6
|
"author": "Chris Breuer",
|
|
7
7
|
"license": "MIT",
|
|
@@ -49,13 +49,13 @@
|
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"@stacksjs/config": "latest",
|
|
52
|
-
"unplugin-vue-router": "^0.8.
|
|
52
|
+
"unplugin-vue-router": "^0.8.8",
|
|
53
53
|
"vue-router": "^4.3.2"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@stacksjs/config": "latest",
|
|
57
57
|
"@stacksjs/logging": "latest",
|
|
58
|
-
"unplugin-vue-router": "^0.8.
|
|
58
|
+
"unplugin-vue-router": "^0.8.8",
|
|
59
59
|
"vue-router": "^4.3.2"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
package/src/index.ts
CHANGED
package/src/request.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { RouteParam } from '@stacksjs/types'
|
|
2
|
+
|
|
1
3
|
interface RequestData {
|
|
2
4
|
[key: string]: string
|
|
3
5
|
}
|
|
@@ -21,6 +23,10 @@ export class Request {
|
|
|
21
23
|
this.query = Object.fromEntries(url.searchParams)
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
public addParam(param: RouteParam): void {
|
|
27
|
+
this.params = param
|
|
28
|
+
}
|
|
29
|
+
|
|
24
30
|
public get(element: string): string | number | undefined {
|
|
25
31
|
return this.query[element]
|
|
26
32
|
}
|
|
@@ -44,10 +50,14 @@ export class Request {
|
|
|
44
50
|
if (match?.groups) this.params = match.groups
|
|
45
51
|
}
|
|
46
52
|
|
|
47
|
-
public
|
|
53
|
+
public getParam(key: string): number | string | null {
|
|
48
54
|
return this.params ? this.params[key] || null : null
|
|
49
55
|
}
|
|
50
56
|
|
|
57
|
+
public getParams(): RouteParams {
|
|
58
|
+
return this.params
|
|
59
|
+
}
|
|
60
|
+
|
|
51
61
|
public getParamAsInt(key: string): number | null {
|
|
52
62
|
const value = this.params ? this.params[key] || null : null
|
|
53
63
|
return value ? Number.parseInt(value) : null
|
package/src/router.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import type { Action } from '@stacksjs/actions'
|
|
1
2
|
import { log } from '@stacksjs/logging'
|
|
2
3
|
import { path as p, projectStoragePath, routesPath } from '@stacksjs/path'
|
|
3
|
-
import { pascalCase } from '@stacksjs/strings'
|
|
4
|
+
import { kebabCase, pascalCase } from '@stacksjs/strings'
|
|
5
|
+
import type { Job } from '@stacksjs/types'
|
|
4
6
|
import type { RedirectCode, Route, RouteGroupOptions, RouterInterface, StatusCode } from '@stacksjs/types'
|
|
5
7
|
|
|
8
|
+
type ActionPath = string // TODO: narrow this by automating its generation
|
|
9
|
+
|
|
6
10
|
export class Router implements RouterInterface {
|
|
7
11
|
private routes: Route[] = []
|
|
8
12
|
private apiPrefix = '/api'
|
|
@@ -22,14 +26,14 @@ export class Router implements RouterInterface {
|
|
|
22
26
|
})}$`,
|
|
23
27
|
)
|
|
24
28
|
|
|
25
|
-
let routeCallback: Route['callback']
|
|
29
|
+
// let routeCallback: Route['callback']
|
|
26
30
|
|
|
27
|
-
if (typeof callback === 'string' || typeof callback === 'object') {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
} else {
|
|
31
|
-
|
|
32
|
-
}
|
|
31
|
+
// if (typeof callback === 'string' || typeof callback === 'object') {
|
|
32
|
+
// // Convert string or object to RouteCallback
|
|
33
|
+
// routeCallback = () => callback
|
|
34
|
+
// } else {
|
|
35
|
+
// routeCallback = callback
|
|
36
|
+
// }
|
|
33
37
|
|
|
34
38
|
log.debug(`Adding route: ${method} ${uri} with name ${name}`)
|
|
35
39
|
|
|
@@ -38,21 +42,20 @@ export class Router implements RouterInterface {
|
|
|
38
42
|
method,
|
|
39
43
|
url: uri,
|
|
40
44
|
uri,
|
|
41
|
-
callback
|
|
45
|
+
callback,
|
|
42
46
|
pattern,
|
|
43
47
|
statusCode,
|
|
44
48
|
paramNames: [],
|
|
49
|
+
// middleware: [],
|
|
45
50
|
})
|
|
46
51
|
|
|
47
52
|
return this
|
|
48
53
|
}
|
|
49
54
|
|
|
50
|
-
public
|
|
55
|
+
public get(path: Route['url'], callback: Route['callback']): this {
|
|
51
56
|
this.path = this.normalizePath(path)
|
|
52
57
|
log.debug(`Normalized Path: ${this.path}`)
|
|
53
58
|
|
|
54
|
-
callback = await this.resolveCallback(callback)
|
|
55
|
-
|
|
56
59
|
const uri = this.prepareUri(this.path)
|
|
57
60
|
log.debug(`Prepared URI: ${uri}`)
|
|
58
61
|
|
|
@@ -62,9 +65,8 @@ export class Router implements RouterInterface {
|
|
|
62
65
|
public async email(path: Route['url']): Promise<this> {
|
|
63
66
|
path = pascalCase(path)
|
|
64
67
|
|
|
65
|
-
const emailModule = await import(p.userNotificationsPath(`${path}.ts`))
|
|
66
|
-
const callback = emailModule.
|
|
67
|
-
|
|
68
|
+
const emailModule = (await import(p.userNotificationsPath(`${path}.ts`))).default as Action
|
|
69
|
+
const callback = emailModule.handle
|
|
68
70
|
const uri = this.prepareUri(path)
|
|
69
71
|
this.addRoute('GET', uri, callback, 200)
|
|
70
72
|
|
|
@@ -72,10 +74,9 @@ export class Router implements RouterInterface {
|
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
public async health(): Promise<this> {
|
|
75
|
-
const healthModule = await import(p.userActionsPath('HealthAction.ts'))
|
|
76
|
-
const callback = healthModule.
|
|
77
|
-
|
|
78
|
-
const path = healthModule.default.path ?? `${this.apiPrefix}/health`
|
|
77
|
+
const healthModule = (await import(p.userActionsPath('HealthAction.ts'))).default as Action
|
|
78
|
+
const callback = healthModule.handle
|
|
79
|
+
const path = healthModule.path ?? `${this.apiPrefix}/health`
|
|
79
80
|
|
|
80
81
|
this.addRoute('GET', path, callback, 200)
|
|
81
82
|
|
|
@@ -86,71 +87,80 @@ export class Router implements RouterInterface {
|
|
|
86
87
|
path = pascalCase(path)
|
|
87
88
|
|
|
88
89
|
// removes the potential `JobJob` suffix in case the user does not choose to use the Job suffix in their file name
|
|
89
|
-
const
|
|
90
|
-
const callback = jobModule.default.handle
|
|
90
|
+
const job = (await import(p.userJobsPath(`${path}.ts`))).default as Job
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
this.addRoute('GET', path, callback, 200)
|
|
94
|
-
|
|
95
|
-
return this
|
|
92
|
+
return this.addRoute('GET', this.prepareUri(path), job.handle, 200)
|
|
96
93
|
}
|
|
97
94
|
|
|
98
|
-
public async action(path: Route['
|
|
95
|
+
public async action(path: ActionPath | Route['path']): Promise<this> {
|
|
96
|
+
// check if action is a file anywhere in ./app/Actions/**/*.ts
|
|
97
|
+
if (path?.endsWith('.ts')) {
|
|
98
|
+
// given it ends with .ts, we treat it as an Actions path
|
|
99
|
+
const action = (await import(p.userActionsPath(path))).default as Action
|
|
100
|
+
path = action.path ?? kebabCase(path as string)
|
|
101
|
+
return this.addRoute(action.method ?? 'GET', path, action.handle, 200)
|
|
102
|
+
}
|
|
103
|
+
|
|
99
104
|
path = pascalCase(path) // actions are PascalCase
|
|
105
|
+
const userActionsPath = p.userActionsPath(`${path}.ts`)
|
|
100
106
|
|
|
101
|
-
let callback: Route['callback']
|
|
102
107
|
try {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
108
|
+
const action = (await import(userActionsPath)).default as Action
|
|
109
|
+
|
|
110
|
+
return this.addRoute(action.method ?? 'GET', this.prepareUri(path), action.handle, 200)
|
|
106
111
|
} catch (error) {
|
|
107
112
|
try {
|
|
108
|
-
const
|
|
109
|
-
|
|
113
|
+
const action = (await import(p.userActionsPath(`${path}.ts`))).default as Action
|
|
114
|
+
|
|
115
|
+
return this.addRoute(action.method ?? 'GET', this.prepareUri(path), action.handle, 200)
|
|
110
116
|
} catch (error) {
|
|
111
|
-
log.error(`Could not find
|
|
117
|
+
log.error(`Could not find Action for path: ${path}`)
|
|
118
|
+
|
|
112
119
|
return this
|
|
113
120
|
}
|
|
114
121
|
}
|
|
115
|
-
|
|
116
|
-
path = this.prepareUri(path)
|
|
117
|
-
this.addRoute('GET', path, callback, 200)
|
|
118
|
-
|
|
119
|
-
return this
|
|
120
122
|
}
|
|
121
123
|
|
|
122
124
|
public post(path: Route['url'], callback: Route['callback']): this {
|
|
123
|
-
path = this.
|
|
124
|
-
this.addRoute('POST', path, callback, 201)
|
|
125
|
+
this.path = this.normalizePath(path)
|
|
125
126
|
|
|
126
|
-
|
|
127
|
+
const uri = this.prepareUri(this.path)
|
|
128
|
+
|
|
129
|
+
return this.addRoute('POST', uri, callback, 201)
|
|
127
130
|
}
|
|
128
131
|
|
|
129
132
|
public view(path: Route['url'], callback: Route['callback']): this {
|
|
130
|
-
this.
|
|
133
|
+
this.path = this.normalizePath(path)
|
|
131
134
|
|
|
132
|
-
|
|
135
|
+
const uri = this.prepareUri(this.path)
|
|
136
|
+
|
|
137
|
+
return this.addRoute('GET', uri, callback, 200)
|
|
133
138
|
}
|
|
134
139
|
|
|
135
140
|
public redirect(path: Route['url'], callback: Route['callback'], _status?: RedirectCode): this {
|
|
136
|
-
this.addRoute('GET', path, callback, 302)
|
|
137
|
-
|
|
138
|
-
return this
|
|
141
|
+
return this.addRoute('GET', path, callback, 302)
|
|
139
142
|
}
|
|
140
143
|
|
|
141
144
|
public delete(path: Route['url'], callback: Route['callback']): this {
|
|
142
|
-
this.addRoute('DELETE', path, callback, 204)
|
|
143
|
-
return this
|
|
145
|
+
return this.addRoute('DELETE', this.prepareUri(path), callback, 204)
|
|
144
146
|
}
|
|
145
147
|
|
|
146
148
|
public patch(path: Route['url'], callback: Route['callback']): this {
|
|
147
|
-
this.
|
|
148
|
-
|
|
149
|
+
this.path = this.normalizePath(path)
|
|
150
|
+
log.debug(`Normalized Path: ${this.path}`)
|
|
151
|
+
|
|
152
|
+
const uri = this.prepareUri(this.path)
|
|
153
|
+
log.debug(`Prepared URI: ${uri}`)
|
|
154
|
+
|
|
155
|
+
return this.addRoute('PATCH', uri, callback, 202)
|
|
149
156
|
}
|
|
150
157
|
|
|
151
158
|
public put(path: Route['url'], callback: Route['callback']): this {
|
|
152
|
-
this.
|
|
153
|
-
|
|
159
|
+
this.path = this.normalizePath(path)
|
|
160
|
+
|
|
161
|
+
const uri = this.prepareUri(this.path)
|
|
162
|
+
|
|
163
|
+
return this.addRoute('PUT', uri, callback, 202)
|
|
154
164
|
}
|
|
155
165
|
|
|
156
166
|
public group(options: string | RouteGroupOptions, callback?: () => void): this {
|
|
@@ -211,7 +221,6 @@ export class Router implements RouterInterface {
|
|
|
211
221
|
}
|
|
212
222
|
|
|
213
223
|
public prefix(prefix: string): this {
|
|
214
|
-
// @ts-expect-error - this is fine for now
|
|
215
224
|
this.routes[this.routes.length - 1].prefix = prefix
|
|
216
225
|
|
|
217
226
|
return this
|
|
@@ -252,7 +261,7 @@ export class Router implements RouterInterface {
|
|
|
252
261
|
return
|
|
253
262
|
}
|
|
254
263
|
|
|
255
|
-
|
|
264
|
+
public async resolveCallback(callback: Route['callback']): Promise<Route['callback']> {
|
|
256
265
|
if (callback instanceof Promise) {
|
|
257
266
|
const actionModule = await callback
|
|
258
267
|
return actionModule.default
|
|
@@ -261,10 +270,10 @@ export class Router implements RouterInterface {
|
|
|
261
270
|
if (typeof callback === 'string') return await this.importCallbackFromPath(callback, this.path)
|
|
262
271
|
|
|
263
272
|
// in this case, the callback ends up being a function
|
|
264
|
-
return
|
|
273
|
+
return callback
|
|
265
274
|
}
|
|
266
275
|
|
|
267
|
-
|
|
276
|
+
public async importCallbackFromPath(callbackPath: string, originalPath: string): Promise<Route['callback']> {
|
|
268
277
|
let modulePath = callbackPath
|
|
269
278
|
let importPathFunction = p.appPath // Default import path function
|
|
270
279
|
|
|
@@ -287,7 +296,17 @@ export class Router implements RouterInterface {
|
|
|
287
296
|
const newPath = actionModule.default.path ?? originalPath
|
|
288
297
|
this.updatePathIfNeeded(newPath, originalPath)
|
|
289
298
|
|
|
290
|
-
|
|
299
|
+
// we need to make sure the validation happens here
|
|
300
|
+
// to do so, we need to:
|
|
301
|
+
// find the ./app/Models/* file
|
|
302
|
+
// then check via a regex which model attributes validations to utilize by checking what's in between t
|
|
303
|
+
// then validate
|
|
304
|
+
// if succeeds, run the handle
|
|
305
|
+
// if fails, return validation error
|
|
306
|
+
|
|
307
|
+
if (condition) return await actionModule.default.handle()
|
|
308
|
+
|
|
309
|
+
return await actionModule.default.handle(request)
|
|
291
310
|
}
|
|
292
311
|
|
|
293
312
|
private normalizePath(path: string): string {
|
|
@@ -300,8 +319,6 @@ export class Router implements RouterInterface {
|
|
|
300
319
|
|
|
301
320
|
path = `${this.apiPrefix}${this.groupPrefix}/${path}`
|
|
302
321
|
|
|
303
|
-
console.log(path)
|
|
304
|
-
|
|
305
322
|
// if path ends in "/", then remove it
|
|
306
323
|
// e.g. triggered when route is "/"
|
|
307
324
|
return path.endsWith('/') ? path.slice(0, -1) : path
|
package/src/server.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import process from 'node:process'
|
|
2
2
|
import { log } from '@stacksjs/logging'
|
|
3
3
|
import { extname } from '@stacksjs/path'
|
|
4
|
-
import type { Route, StatusCode } from '@stacksjs/types'
|
|
4
|
+
import type { Route, RouteParam, StatusCode } from '@stacksjs/types'
|
|
5
5
|
import { route } from '.'
|
|
6
6
|
import { middlewares } from './middleware'
|
|
7
7
|
import { request as RequestParam } from './request'
|
|
@@ -13,6 +13,10 @@ interface ServeOptions {
|
|
|
13
13
|
timezone?: string
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
interface Options {
|
|
17
|
+
statusCode?: StatusCode
|
|
18
|
+
}
|
|
19
|
+
|
|
16
20
|
export async function serve(options: ServeOptions = {}) {
|
|
17
21
|
const hostname = options.host || 'localhost'
|
|
18
22
|
const port = options.port || 3000
|
|
@@ -45,83 +49,72 @@ export async function serverResponse(req: Request) {
|
|
|
45
49
|
const trimmedUrl = req.url.endsWith('/') && req.url.length > 1 ? req.url.slice(0, -1) : req.url
|
|
46
50
|
|
|
47
51
|
const url = new URL(trimmedUrl)
|
|
48
|
-
addRouteParamsAndQuery(url)
|
|
49
52
|
|
|
50
53
|
const routesList: Route[] = await route.getRoutes()
|
|
51
54
|
log.info(`Routes List: ${JSON.stringify(routesList)}`)
|
|
52
55
|
|
|
53
56
|
log.info(`URL: ${JSON.stringify(url)}`)
|
|
54
57
|
|
|
55
|
-
const foundRoute: Route | undefined = routesList
|
|
56
|
-
|
|
58
|
+
const foundRoute: Route | undefined = routesList
|
|
59
|
+
.filter((route: Route) => {
|
|
60
|
+
const pattern = new RegExp(`^${route.uri.replace(/\{(\w+)\}/g, '(\\w+)')}$`)
|
|
57
61
|
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
return pattern.test(url.pathname)
|
|
63
|
+
})
|
|
64
|
+
.find((route: Route) => route.method === req.method)
|
|
60
65
|
|
|
61
66
|
log.info(`Found Route: ${JSON.stringify(foundRoute)}`)
|
|
62
|
-
|
|
63
67
|
// if (url.pathname === '/favicon.ico')
|
|
64
68
|
// return new Response('')
|
|
65
69
|
|
|
66
70
|
if (!foundRoute) return new Response('Pretty 404 page coming soon', { status: 404 }) // TODO: create a pretty 404 page
|
|
67
71
|
|
|
72
|
+
const routeParams = extractDynamicSegments(foundRoute.uri, url.pathname)
|
|
73
|
+
|
|
74
|
+
addRouteQuery(url)
|
|
75
|
+
addRouteParam(routeParams)
|
|
76
|
+
|
|
68
77
|
await executeMiddleware(foundRoute)
|
|
69
78
|
|
|
70
79
|
return await execute(foundRoute, req, { statusCode: foundRoute?.statusCode })
|
|
71
80
|
}
|
|
72
81
|
|
|
73
|
-
function
|
|
74
|
-
|
|
82
|
+
function extractDynamicSegments(routePattern: string, path: string): RouteParam {
|
|
83
|
+
const regexPattern = new RegExp(`^${routePattern.replace(/\{(\w+)\}/g, '(\\w+)')}$`)
|
|
84
|
+
const match = path.match(regexPattern)
|
|
75
85
|
|
|
76
|
-
|
|
77
|
-
|
|
86
|
+
if (!match) {
|
|
87
|
+
return null
|
|
88
|
+
}
|
|
89
|
+
const dynamicSegmentNames = [...routePattern.matchAll(/\{(\w+)\}/g)].map((m) => m[1])
|
|
90
|
+
const dynamicSegmentValues = match.slice(1) // First match is the whole string, so we slice it off
|
|
78
91
|
|
|
79
|
-
|
|
80
|
-
|
|
92
|
+
const dynamicSegments: { [key: string]: string } = {}
|
|
93
|
+
dynamicSegmentNames.forEach((name, index) => {
|
|
94
|
+
dynamicSegments[name] = dynamicSegmentValues[index]
|
|
95
|
+
})
|
|
81
96
|
|
|
82
|
-
|
|
83
|
-
// let middlewareItem: MiddlewareOptions
|
|
84
|
-
if (isString(middleware)) {
|
|
85
|
-
// TODO: fix and uncomment this
|
|
86
|
-
// middlewareItem = middlewares.find((m) => {
|
|
87
|
-
// return m.name === middleware
|
|
88
|
-
// })
|
|
89
|
-
// if (middlewareItem)
|
|
90
|
-
// middlewareItem.handle() // Invoke only if it exists and is not undefined.
|
|
91
|
-
} else {
|
|
92
|
-
// middleware.forEach((m) => {
|
|
93
|
-
middleware.forEach(() => {
|
|
94
|
-
// TODO: fix and uncomment this
|
|
95
|
-
// middlewareItem = middlewares.find((middlewareItem: MiddlewareOptions) => {
|
|
96
|
-
// return middlewareItem.name === m
|
|
97
|
-
// })
|
|
98
|
-
// if (middlewareItem)
|
|
99
|
-
// middlewareItem.handle() // Again, invoke only if it exists.
|
|
100
|
-
})
|
|
101
|
-
}
|
|
102
|
-
}
|
|
97
|
+
return dynamicSegments
|
|
103
98
|
}
|
|
104
99
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
100
|
+
async function execute(foundRoute: Route, req: Request, { statusCode }: Options) {
|
|
101
|
+
const foundCallback = await route.resolveCallback(foundRoute.callback)
|
|
108
102
|
|
|
109
|
-
async function execute(route: Route, req: Request, { statusCode }: Options) {
|
|
110
103
|
if (!statusCode) statusCode = 200
|
|
111
104
|
|
|
112
|
-
if (
|
|
113
|
-
const callback = String(
|
|
105
|
+
if (foundRoute?.method === 'GET' && (statusCode === 301 || statusCode === 302)) {
|
|
106
|
+
const callback = String(foundCallback)
|
|
114
107
|
const response = Response.redirect(callback, statusCode)
|
|
115
108
|
|
|
116
109
|
return await noCache(response)
|
|
117
110
|
}
|
|
118
111
|
|
|
119
|
-
if (
|
|
112
|
+
if (foundRoute?.method !== req.method) return new Response('Method not allowed', { status: 405 })
|
|
120
113
|
|
|
121
114
|
// Check if it's a path to an HTML file
|
|
122
|
-
if (isString(
|
|
115
|
+
if (isString(foundCallback) && extname(foundCallback) === '.html') {
|
|
123
116
|
try {
|
|
124
|
-
const fileContent = Bun.file(
|
|
117
|
+
const fileContent = Bun.file(foundCallback)
|
|
125
118
|
|
|
126
119
|
return await new Response(fileContent, {
|
|
127
120
|
headers: { 'Content-Type': 'text/html' },
|
|
@@ -131,14 +124,15 @@ async function execute(route: Route, req: Request, { statusCode }: Options) {
|
|
|
131
124
|
}
|
|
132
125
|
}
|
|
133
126
|
|
|
134
|
-
if (isString(
|
|
127
|
+
if (isString(foundCallback)) return await new Response(foundCallback)
|
|
128
|
+
|
|
129
|
+
if (isFunction(foundCallback)) {
|
|
130
|
+
const result = foundCallback()
|
|
135
131
|
|
|
136
|
-
if (isFunction(route.callback)) {
|
|
137
|
-
const result = route.callback()
|
|
138
132
|
return await new Response(JSON.stringify(result))
|
|
139
133
|
}
|
|
140
134
|
|
|
141
|
-
if (isObject(
|
|
135
|
+
if (isObject(foundCallback)) return await new Response(JSON.stringify(foundCallback))
|
|
142
136
|
|
|
143
137
|
// If no known type matched, return a generic error.
|
|
144
138
|
return await new Response('Unknown callback type.', { status: 500 })
|
|
@@ -152,6 +146,41 @@ function noCache(response: Response) {
|
|
|
152
146
|
return response
|
|
153
147
|
}
|
|
154
148
|
|
|
149
|
+
function addRouteQuery(url: URL): void {
|
|
150
|
+
if (!isObjectNotEmpty(url.searchParams)) RequestParam.addQuery(url)
|
|
151
|
+
|
|
152
|
+
// requestInstance.extractParamsFromRoute(route.uri, url.pathname)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function addRouteParam(param: RouteParam): void {
|
|
156
|
+
RequestParam.addParam(param)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function executeMiddleware(route: Route): void {
|
|
160
|
+
const { middleware = null } = route
|
|
161
|
+
|
|
162
|
+
if (middleware && middlewares && isObjectNotEmpty(middlewares)) {
|
|
163
|
+
// let middlewareItem: MiddlewareOptions
|
|
164
|
+
if (isString(middleware)) {
|
|
165
|
+
// TODO: fix and uncomment this
|
|
166
|
+
// middlewareItem = middlewares.find((m) => {
|
|
167
|
+
// return m.name === middleware
|
|
168
|
+
// })
|
|
169
|
+
// if (middlewareItem)
|
|
170
|
+
// middlewareItem.handle() // Invoke only if it exists and is not undefined.
|
|
171
|
+
} else {
|
|
172
|
+
// middleware.forEach((m) => {
|
|
173
|
+
middleware.forEach(() => {
|
|
174
|
+
// TODO: fix and uncomment this
|
|
175
|
+
// middlewareItem = middlewares.find((middlewareItem: MiddlewareOptions) => {
|
|
176
|
+
// return middlewareItem.name === m
|
|
177
|
+
// })
|
|
178
|
+
// if (middlewareItem)
|
|
179
|
+
// middlewareItem.handle() // Again, invoke only if it exists.
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
155
184
|
function isString(val: unknown): val is string {
|
|
156
185
|
return typeof val === 'string'
|
|
157
186
|
}
|
|
@@ -165,5 +194,5 @@ function isFunction(val: unknown): val is Function {
|
|
|
165
194
|
}
|
|
166
195
|
|
|
167
196
|
function isObject(val: unknown): val is object {
|
|
168
|
-
return
|
|
197
|
+
return typeof val === 'object'
|
|
169
198
|
}
|