@wxn0brp/falcon-frame 0.0.16 → 0.0.18

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/helpers.js CHANGED
@@ -76,6 +76,7 @@ export function handleStaticFiles(dirPath, utf8 = true) {
76
76
  if (fs.existsSync(indexPath) && fs.statSync(indexPath).isFile()) {
77
77
  if (!req.path.endsWith("/")) {
78
78
  res.redirect(req.path + "/");
79
+ return true;
79
80
  }
80
81
  res.ct(getContentType(indexPath, utf8));
81
82
  fs.createReadStream(indexPath).pipe(res);
@@ -0,0 +1,3 @@
1
+ import { Middleware } from "./types.js";
2
+ export declare function matchMiddleware(url: string, middlewares: Middleware[]): Middleware[];
3
+ export declare function getMiddlewares(middlewares: Middleware[], matchUrl: string, basePath?: string): Middleware[];
@@ -0,0 +1,69 @@
1
+ export function matchMiddleware(url, middlewares) {
2
+ const matchedMiddlewares = [];
3
+ url = url.replace(/\/$/, "");
4
+ for (const middleware of middlewares) {
5
+ const cleanedMiddleware = middleware.path.replace(/\/$/, "");
6
+ if (middleware.use) {
7
+ if (url.startsWith(cleanedMiddleware)) {
8
+ matchedMiddlewares.push(middleware);
9
+ }
10
+ }
11
+ else if (cleanedMiddleware === "*") {
12
+ matchedMiddlewares.push(middleware);
13
+ }
14
+ else if (cleanedMiddleware.endsWith("/*")) {
15
+ const prefix = cleanedMiddleware.slice(0, -2);
16
+ if (url.startsWith(prefix)) {
17
+ matchedMiddlewares.push(middleware);
18
+ }
19
+ }
20
+ else if (cleanedMiddleware.includes(":")) {
21
+ const middlewareParts = cleanedMiddleware.split("/");
22
+ const urlParts = url.split("/");
23
+ if (middlewareParts.length !== urlParts.length) {
24
+ continue;
25
+ }
26
+ let matches = true;
27
+ for (let i = 0; i < middlewareParts.length; i++) {
28
+ if (middlewareParts[i].startsWith(":")) {
29
+ continue;
30
+ }
31
+ else if (middlewareParts[i] !== urlParts[i]) {
32
+ matches = false;
33
+ break;
34
+ }
35
+ }
36
+ if (matches) {
37
+ matchedMiddlewares.push(middleware);
38
+ }
39
+ }
40
+ else {
41
+ if (url === cleanedMiddleware) {
42
+ matchedMiddlewares.push(middleware);
43
+ }
44
+ }
45
+ }
46
+ return matchedMiddlewares;
47
+ }
48
+ export function getMiddlewares(middlewares, matchUrl, basePath = "") {
49
+ const result = [];
50
+ for (const middleware of middlewares) {
51
+ const midPath = (middleware.path || "").replace(/\/+$/, "");
52
+ const fullPath = (basePath + "/" + midPath).replace(/\/+/g, "/");
53
+ const matches = matchUrl === fullPath ||
54
+ (middleware.use && matchUrl.startsWith(fullPath)) ||
55
+ fullPath.includes(":") ||
56
+ fullPath.includes("*") ||
57
+ matchUrl.startsWith(fullPath + "/");
58
+ if (!matches)
59
+ continue;
60
+ if (middleware.router) {
61
+ const nested = getMiddlewares(middleware.router, matchUrl, fullPath);
62
+ result.push(...nested);
63
+ }
64
+ else {
65
+ result.push({ ...middleware, path: fullPath });
66
+ }
67
+ }
68
+ return result;
69
+ }
package/dist/render.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
3
  export function renderHTML(templatePath, data) {
4
+ if (!fs.existsSync(templatePath))
5
+ return "Template not found";
4
6
  let template = fs.readFileSync(templatePath, "utf8");
5
7
  // Inserting data, e.g. {{name}}
6
8
  template = template.replace(/{{(.*?)}}/g, (_, key) => data[key.trim()] || "");
package/dist/req.js CHANGED
@@ -2,6 +2,7 @@ import { URL } from "url";
2
2
  import { parseBody, parseCookies } from "./helpers.js";
3
3
  import { FFResponse } from "./res.js";
4
4
  import { validate } from "./valid.js";
5
+ import { getMiddlewares, matchMiddleware } from "./middleware.js";
5
6
  export function handleRequest(req, res, FF) {
6
7
  Object.setPrototypeOf(res, FFResponse.prototype);
7
8
  const originalEnd = res.end;
@@ -26,117 +27,56 @@ export function handleRequest(req, res, FF) {
26
27
  req.params = {};
27
28
  req.valid = (schema) => validate(schema, req.body);
28
29
  logger.info(`Incoming request: ${req.method} ${req.url}`);
29
- const middlewares = getMiddlewares(FF.middlewares, (req.url + "/").replace(/\/+/g, "/"));
30
- let body = "";
31
- req.on("data", chunk => (body += chunk.toString()));
32
- req.on("end", () => {
33
- const contentType = req.headers["content-type"] || "";
34
- req.body = parseBody(contentType, body);
35
- logger.debug(`Request body: ${JSON.stringify(req.body)}`);
36
- const matchedTypeMiddlewares = middlewares.filter(middleware => middleware.method === req.method.toLocaleLowerCase() || middleware.method === "all");
37
- const matchedMiddlewares = matchMiddleware(req.path, matchedTypeMiddlewares);
38
- if (matchedMiddlewares.length === 0) {
39
- return res.status(404).end("404: File had second thoughts.");
40
- }
41
- logger.debug("Matched middlewares: " + matchedMiddlewares.map(middleware => middleware.path).join(", "));
42
- let middlewareIndex = 0;
43
- const next = async () => {
44
- if (middlewareIndex >= matchedMiddlewares.length) {
45
- return res.status(404).end("404: File had second thoughts");
46
- }
47
- const middleware = matchedMiddlewares[middlewareIndex++];
48
- logger.debug(`Executing middleware ${middlewareIndex} of ${matchedMiddlewares.length} matched for path [${middleware.path}]`);
49
- if (middleware.path.includes(":")) {
50
- const middlewareParts = middleware.path.split("/");
51
- const reqPathParts = req.path.split("/");
52
- req.params = {};
53
- for (let i = 0; i < middlewareParts.length; i++) {
54
- if (middlewareParts[i].startsWith(":")) {
55
- const paramName = middlewareParts[i].slice(1);
56
- req.params[paramName] = reqPathParts[i];
57
- }
58
- }
59
- }
60
- req.middleware = middleware;
61
- const result = await middleware.middleware(req, res, next);
62
- if (result && !res._ended) {
63
- if (typeof result === "string") {
64
- return res.end(result);
65
- }
66
- else if (typeof result === "object") {
67
- return res.json(result);
68
- }
69
- }
70
- };
71
- next();
72
- });
73
- }
74
- function matchMiddleware(url, middlewares) {
75
- const matchedMiddlewares = [];
76
- url = url.replace(/\/$/, "");
77
- for (const middleware of middlewares) {
78
- const cleanedMiddleware = middleware.path.replace(/\/$/, "");
79
- if (middleware.use) {
80
- if (url.startsWith(cleanedMiddleware)) {
81
- matchedMiddlewares.push(middleware);
82
- }
83
- }
84
- else if (cleanedMiddleware === "*") {
85
- matchedMiddlewares.push(middleware);
86
- }
87
- else if (cleanedMiddleware.endsWith("/*")) {
88
- const prefix = cleanedMiddleware.slice(0, -2);
89
- if (url.startsWith(prefix)) {
90
- matchedMiddlewares.push(middleware);
91
- }
30
+ const middlewaresPath = req.path + "/";
31
+ const middlewares = getMiddlewares(FF.middlewares, middlewaresPath.replace(/\/+/g, "/"));
32
+ const matchedTypeMiddlewares = middlewares.filter(middleware => middleware.method === req.method.toLowerCase() || middleware.method === "all");
33
+ const matchedMiddlewares = matchMiddleware(req.path, matchedTypeMiddlewares);
34
+ logger.debug("Matched middlewares: " + matchedMiddlewares.map(middleware => middleware.path).join(", "));
35
+ if (matchedMiddlewares.length === 0) {
36
+ res.status(404).end("404: File had second thoughts");
37
+ return;
38
+ }
39
+ let middlewareIndex = 0;
40
+ async function next() {
41
+ if (middlewareIndex >= matchedMiddlewares.length) {
42
+ return res.status(404).end("404: File had second thoughts");
92
43
  }
93
- else if (cleanedMiddleware.includes(":")) {
94
- const middlewareParts = cleanedMiddleware.split("/");
95
- const urlParts = url.split("/");
96
- if (middlewareParts.length !== urlParts.length) {
97
- continue;
98
- }
99
- let matches = true;
44
+ const middleware = matchedMiddlewares[middlewareIndex++];
45
+ logger.debug(`Executing middleware ${middlewareIndex} of ${matchedMiddlewares.length} matched for path [${middleware.path}]`);
46
+ if (middleware.path.includes(":")) {
47
+ const middlewareParts = middleware.path.split("/");
48
+ const reqPathParts = req.path.split("/");
49
+ req.params = {};
100
50
  for (let i = 0; i < middlewareParts.length; i++) {
101
51
  if (middlewareParts[i].startsWith(":")) {
102
- continue;
52
+ const paramName = middlewareParts[i].slice(1);
53
+ req.params[paramName] = reqPathParts[i];
103
54
  }
104
- else if (middlewareParts[i] !== urlParts[i]) {
105
- matches = false;
106
- break;
107
- }
108
- }
109
- if (matches) {
110
- matchedMiddlewares.push(middleware);
111
55
  }
112
56
  }
113
- else {
114
- if (url === cleanedMiddleware) {
115
- matchedMiddlewares.push(middleware);
57
+ req.middleware = middleware;
58
+ const result = await middleware.middleware(req, res, next);
59
+ if (result && !res._ended) {
60
+ if (typeof result === "string") {
61
+ return res.end(result);
62
+ }
63
+ else if (typeof result === "object") {
64
+ if (result instanceof FFResponse)
65
+ return res.end();
66
+ return res.json(result);
116
67
  }
117
68
  }
118
69
  }
119
- return matchedMiddlewares;
120
- }
121
- function getMiddlewares(middlewares, matchUrl, basePath = "") {
122
- const result = [];
123
- for (const middleware of middlewares) {
124
- const midPath = (middleware.path || "").replace(/\/+$/, "");
125
- const fullPath = (basePath + "/" + midPath).replace(/\/+/g, "/");
126
- const matches = matchUrl === fullPath ||
127
- (middleware.use && matchUrl.startsWith(fullPath)) ||
128
- fullPath.includes(":") ||
129
- fullPath.includes("*") ||
130
- matchUrl.startsWith(fullPath + "/");
131
- if (!matches)
132
- continue;
133
- if (middleware.router) {
134
- const nested = getMiddlewares(middleware.router, matchUrl, fullPath);
135
- result.push(...nested);
136
- }
137
- else {
138
- result.push({ ...middleware, path: fullPath });
139
- }
70
+ if (req.method === "GET" && middlewares[middlewares.length - 1]?.sse) {
71
+ next();
72
+ return;
140
73
  }
141
- return result;
74
+ let body = "";
75
+ req.on("data", chunk => (body += chunk.toString()));
76
+ req.on("end", () => {
77
+ const contentType = req.headers["content-type"] || "";
78
+ req.body = parseBody(contentType, body);
79
+ logger.debug(`Request body: ${JSON.stringify(req.body)}`);
80
+ next();
81
+ });
142
82
  }
package/dist/res.d.ts CHANGED
@@ -6,14 +6,77 @@ export declare class FFResponse extends http.ServerResponse {
6
6
  * bind end for compatibility
7
7
  */
8
8
  send(data: string): void;
9
+ /**
10
+ * Sets a header. This is a shortcut for setHeader.
11
+ * @param name The name of the header
12
+ * @param value The value of the header
13
+ * @returns The response object
14
+ */
15
+ header(name: string, value: string): this;
9
16
  /**
10
17
  * Set content type
11
18
  */
12
- ct(contentType?: string): void;
19
+ ct(contentType?: string): this;
20
+ /**
21
+ * Set the content type to application/json and write the given data as json
22
+ * @param data The data to be written as json
23
+ */
13
24
  json(data: any): void;
25
+ /**
26
+ * Set a cookie in the response
27
+ * @param name The name of the cookie
28
+ * @param value The value of the cookie
29
+ * @param options The options for the cookie
30
+ */
14
31
  cookie(name: string, value: string, options?: CookieOptions): this;
32
+ /**
33
+ * Set the status code for the response
34
+ * @param code The status code to set
35
+ * @returns The response object
36
+ */
15
37
  status(code: number): this;
16
- redirect(url: string): this;
38
+ /**
39
+ * Set status code to 302 and set location header to the provided url.
40
+ * If end is true, ends the response.
41
+ * @param url The url to redirect to
42
+ * @param end Whether to end the response after setting the headers
43
+ * @returns The response object
44
+ */
45
+ redirect(url: string, end?: boolean): this;
46
+ /**
47
+ * Sends a file as the response.
48
+ * Sets the "Content-Type" header based on the provided contentType or the file's extension.
49
+ * Uses streaming to send the file content.
50
+ * @param filePath The path to the file to be sent.
51
+ * @param contentType Optional. The MIME type for the Content-Type header.
52
+ * If "utf8", the charset will be set to UTF-8.
53
+ * @returns The response object.
54
+ */
17
55
  sendFile(filePath: string, contentType?: string): this;
18
- render(templatePath: string, data: any): this;
56
+ /**
57
+ * Renders an HTML template with the provided data and sends it as the response.
58
+ * Sets the "Content-Type" header to "text/html".
59
+ * @param templatePath The path to the HTML template file.
60
+ * @param data An object containing data to be injected into the template.
61
+ * @returns The response object.
62
+ */
63
+ render(templatePath: string, data?: any): this;
64
+ /**
65
+ * Initialize SSE headers to start a server-sent event stream.
66
+ * Sets:
67
+ *
68
+ * Content-Type: "text/event-stream"
69
+ *
70
+ * Cache-Control: "no-cache"
71
+ *
72
+ * Connection: "keep-alive"
73
+ * @returns The response object
74
+ */
75
+ sseInit(): this;
76
+ /**
77
+ * Sends a Server-Sent Event to the client.
78
+ * @param data The data to be sent. If an object, it will be JSON.stringified.
79
+ * @returns The response object
80
+ */
81
+ sseSend(data: string | object): this;
19
82
  }
package/dist/res.js CHANGED
@@ -10,16 +10,37 @@ export class FFResponse extends http.ServerResponse {
10
10
  send(data) {
11
11
  this.end(data);
12
12
  }
13
+ /**
14
+ * Sets a header. This is a shortcut for setHeader.
15
+ * @param name The name of the header
16
+ * @param value The value of the header
17
+ * @returns The response object
18
+ */
19
+ header(name, value) {
20
+ this.setHeader(name, value);
21
+ return this;
22
+ }
13
23
  /**
14
24
  * Set content type
15
25
  */
16
26
  ct(contentType = "text/plain") {
17
27
  this.setHeader("Content-Type", contentType);
28
+ return this;
18
29
  }
30
+ /**
31
+ * Set the content type to application/json and write the given data as json
32
+ * @param data The data to be written as json
33
+ */
19
34
  json(data) {
20
35
  this.setHeader("Content-Type", "application/json");
21
36
  this.end(JSON.stringify(data));
22
37
  }
38
+ /**
39
+ * Set a cookie in the response
40
+ * @param name The name of the cookie
41
+ * @param value The value of the cookie
42
+ * @param options The options for the cookie
43
+ */
23
44
  cookie(name, value, options = {}) {
24
45
  let cookie = `${name}=${encodeURIComponent(value)}`;
25
46
  if (options.maxAge)
@@ -35,25 +56,84 @@ export class FFResponse extends http.ServerResponse {
35
56
  this.setHeader("Set-Cookie", cookie);
36
57
  return this;
37
58
  }
59
+ /**
60
+ * Set the status code for the response
61
+ * @param code The status code to set
62
+ * @returns The response object
63
+ */
38
64
  status(code) {
39
65
  this.statusCode = code;
40
66
  return this;
41
67
  }
42
- redirect(url) {
68
+ /**
69
+ * Set status code to 302 and set location header to the provided url.
70
+ * If end is true, ends the response.
71
+ * @param url The url to redirect to
72
+ * @param end Whether to end the response after setting the headers
73
+ * @returns The response object
74
+ */
75
+ redirect(url, end = true) {
43
76
  this.statusCode = 302;
44
77
  this.setHeader("Location", url);
78
+ if (end)
79
+ this.end();
45
80
  return this;
46
81
  }
82
+ /**
83
+ * Sends a file as the response.
84
+ * Sets the "Content-Type" header based on the provided contentType or the file's extension.
85
+ * Uses streaming to send the file content.
86
+ * @param filePath The path to the file to be sent.
87
+ * @param contentType Optional. The MIME type for the Content-Type header.
88
+ * If "utf8", the charset will be set to UTF-8.
89
+ * @returns The response object.
90
+ */
47
91
  sendFile(filePath, contentType) {
48
92
  if (contentType === "utf8")
49
93
  contentType = getContentType(filePath);
50
94
  this.ct(contentType || getContentType(filePath));
51
95
  createReadStream(filePath).pipe(this);
96
+ this._ended = true;
52
97
  return this;
53
98
  }
54
- render(templatePath, data) {
99
+ /**
100
+ * Renders an HTML template with the provided data and sends it as the response.
101
+ * Sets the "Content-Type" header to "text/html".
102
+ * @param templatePath The path to the HTML template file.
103
+ * @param data An object containing data to be injected into the template.
104
+ * @returns The response object.
105
+ */
106
+ render(templatePath, data = {}) {
55
107
  this.setHeader("Content-Type", "text/html");
56
108
  this.end(renderHTML(templatePath, data));
57
109
  return this;
58
110
  }
111
+ /**
112
+ * Initialize SSE headers to start a server-sent event stream.
113
+ * Sets:
114
+ *
115
+ * Content-Type: "text/event-stream"
116
+ *
117
+ * Cache-Control: "no-cache"
118
+ *
119
+ * Connection: "keep-alive"
120
+ * @returns The response object
121
+ */
122
+ sseInit() {
123
+ this.setHeader("Content-Type", "text/event-stream");
124
+ this.setHeader("Cache-Control", "no-cache");
125
+ this.setHeader("Connection", "keep-alive");
126
+ return this;
127
+ }
128
+ /**
129
+ * Sends a Server-Sent Event to the client.
130
+ * @param data The data to be sent. If an object, it will be JSON.stringified.
131
+ * @returns The response object
132
+ */
133
+ sseSend(data) {
134
+ if (typeof data === "object")
135
+ data = JSON.stringify(data);
136
+ this.write(`data: ${data}\n\n`);
137
+ return this;
138
+ }
59
139
  }
package/dist/router.d.ts CHANGED
@@ -1,12 +1,13 @@
1
1
  import { Method, Middleware, RouteHandler } from "./types.js";
2
2
  export declare class Router {
3
3
  middlewares: Middleware[];
4
- addRoute(method: Method, path: string, ...handlers: RouteHandler[]): void;
5
- use(path?: string | RouteHandler | Router, middlewareFn?: RouteHandler | Router, method?: Method): void;
6
- get(path: string, ...handlers: RouteHandler[]): void;
7
- post(path: string, ...handlers: RouteHandler[]): void;
8
- put(path: string, ...handlers: RouteHandler[]): void;
9
- delete(path: string, ...handlers: RouteHandler[]): void;
10
- all(path: string, ...handlers: RouteHandler[]): void;
11
- static(apiPath: string, dirPath?: string, utf8?: boolean): void;
4
+ addRoute(method: Method, path: string, ...handlers: RouteHandler[]): number;
5
+ use(path?: string | RouteHandler | Router, middlewareFn?: RouteHandler | Router, method?: Method): this;
6
+ get(path: string, ...handlers: RouteHandler[]): this;
7
+ post(path: string, ...handlers: RouteHandler[]): this;
8
+ put(path: string, ...handlers: RouteHandler[]): this;
9
+ delete(path: string, ...handlers: RouteHandler[]): this;
10
+ all(path: string, ...handlers: RouteHandler[]): this;
11
+ static(apiPath: string, dirPath?: string, utf8?: boolean): this;
12
+ sse(path: string, ...handlers: RouteHandler[]): this;
12
13
  }
package/dist/router.js CHANGED
@@ -4,7 +4,7 @@ export class Router {
4
4
  addRoute(method, path, ...handlers) {
5
5
  const handler = handlers.pop();
6
6
  handlers.forEach(middleware => this.use(path, middleware));
7
- this.middlewares.push({ path, middleware: handler, method });
7
+ return this.middlewares.push({ path, middleware: handler, method });
8
8
  }
9
9
  use(path = "/", middlewareFn, method = "all") {
10
10
  if (typeof path === "function" || path instanceof Router) {
@@ -24,21 +24,27 @@ export class Router {
24
24
  middleware.middleware = middlewareFn;
25
25
  }
26
26
  this.middlewares.push(middleware);
27
+ return this;
27
28
  }
28
29
  get(path, ...handlers) {
29
30
  this.addRoute("get", path, ...handlers);
31
+ return this;
30
32
  }
31
33
  post(path, ...handlers) {
32
34
  this.addRoute("post", path, ...handlers);
35
+ return this;
33
36
  }
34
37
  put(path, ...handlers) {
35
38
  this.addRoute("put", path, ...handlers);
39
+ return this;
36
40
  }
37
41
  delete(path, ...handlers) {
38
42
  this.addRoute("delete", path, ...handlers);
43
+ return this;
39
44
  }
40
45
  all(path, ...handlers) {
41
46
  this.addRoute("all", path, ...handlers);
47
+ return this;
42
48
  }
43
49
  static(apiPath, dirPath, utf8 = true) {
44
50
  if (!dirPath) {
@@ -46,5 +52,11 @@ export class Router {
46
52
  apiPath = "/";
47
53
  }
48
54
  this.use(apiPath, handleStaticFiles(dirPath, utf8));
55
+ return this;
56
+ }
57
+ sse(path, ...handlers) {
58
+ const index = this.addRoute("get", path, ...handlers);
59
+ this.middlewares[index - 1].sse = true;
60
+ return this;
49
61
  }
50
62
  }
package/dist/types.d.ts CHANGED
@@ -29,6 +29,7 @@ export interface Middleware {
29
29
  middleware: RouteHandler;
30
30
  use?: true;
31
31
  router?: Middleware[];
32
+ sse?: true;
32
33
  }
33
34
  export interface CookieOptions {
34
35
  maxAge?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wxn0brp/falcon-frame",
3
- "version": "0.0.16",
3
+ "version": "0.0.18",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "author": "wxn0brP",