@wxn0brp/falcon-frame 0.1.0 → 0.2.1

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/body.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import querystring from "querystring";
2
2
  const parseBodyFunctions = {
3
3
  "application/json": async (body) => ({ body: JSON.parse(body) }),
4
- "application/x-www-form-urlencoded": async (body) => ({ body: querystring.parse(body) }),
4
+ "application/x-www-form-urlencoded": async (body) => ({
5
+ body: querystring.parse(body),
6
+ }),
5
7
  };
6
8
  export async function parseBody(req, body, FF) {
7
9
  const funcs = Object.assign({}, parseBodyFunctions, FF.customParsers || {});
@@ -38,9 +40,13 @@ function parseLimit(limit) {
38
40
  const num = parseInt(match[1], 10);
39
41
  const unit = match[2]?.toLowerCase();
40
42
  switch (unit) {
41
- case "k": return num * 1024;
42
- case "m": return num * 1024 * 1024;
43
- case "g": return num * 1024 * 1024 * 1024;
44
- default: return num;
43
+ case "k":
44
+ return num * 1024;
45
+ case "m":
46
+ return num * 1024 * 1024;
47
+ case "g":
48
+ return num * 1024 * 1024 * 1024;
49
+ default:
50
+ return num;
45
51
  }
46
52
  }
package/dist/helpers.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Cookies, RouteHandler } from "./types.js";
1
+ import { Cookies, RouteHandler, StaticServeOptions } from "./types.js";
2
2
  export declare function parseCookies(cookieHeader: string): Cookies;
3
3
  export declare function getContentType(filePath: string, utf8?: boolean): string;
4
- export declare function handleStaticFiles(dirPath: string, utf8?: boolean): RouteHandler;
4
+ export declare function handleStaticFiles(dirPath: string, opts: StaticServeOptions): RouteHandler;
package/dist/helpers.js CHANGED
@@ -2,42 +2,30 @@ import fs from "fs";
2
2
  import path from "path";
3
3
  export function parseCookies(cookieHeader) {
4
4
  const cookies = {};
5
- cookieHeader.split(";").forEach(cookie => {
5
+ cookieHeader.split(";").forEach((cookie) => {
6
6
  const [name, ...valueParts] = cookie.split("=");
7
7
  const value = decodeURIComponent(valueParts.join("=").trim());
8
8
  cookies[name.trim()] = value;
9
9
  });
10
10
  return cookies;
11
11
  }
12
+ const mimeTypes = {
13
+ ".html": "text/html",
14
+ ".css": "text/css",
15
+ ".js": "application/javascript",
16
+ ".json": "application/json",
17
+ ".png": "image/png",
18
+ ".jpg": "image/jpeg",
19
+ ".jpeg": "image/jpeg",
20
+ ".gif": "image/gif",
21
+ ".svg": "image/svg+xml",
22
+ ".ico": "image/x-icon",
23
+ ".txt": "text/plain",
24
+ ".pdf": "application/pdf",
25
+ };
12
26
  function _getContentType(filePath) {
13
27
  const ext = path.extname(filePath).toLowerCase();
14
- switch (ext) {
15
- case ".html":
16
- return "text/html";
17
- case ".css":
18
- return "text/css";
19
- case ".js":
20
- return "application/javascript";
21
- case ".json":
22
- return "application/json";
23
- case ".png":
24
- return "image/png";
25
- case ".jpg":
26
- case ".jpeg":
27
- return "image/jpeg";
28
- case ".gif":
29
- return "image/gif";
30
- case ".svg":
31
- return "image/svg+xml";
32
- case ".ico":
33
- return "image/x-icon";
34
- case ".txt":
35
- return "text/plain";
36
- case ".pdf":
37
- return "application/pdf";
38
- default:
39
- return "application/octet-stream";
40
- }
28
+ return mimeTypes[ext] || "application/octet-stream";
41
29
  }
42
30
  export function getContentType(filePath, utf8 = false) {
43
31
  let contentType = _getContentType(filePath);
@@ -45,34 +33,73 @@ export function getContentType(filePath, utf8 = false) {
45
33
  contentType += "; charset=utf-8";
46
34
  return contentType;
47
35
  }
48
- export function handleStaticFiles(dirPath, utf8 = true) {
36
+ export function handleStaticFiles(dirPath, opts) {
37
+ opts = {
38
+ utf8: true,
39
+ render: true,
40
+ etag: true,
41
+ errorIfDirNotFound: true,
42
+ ...opts,
43
+ };
44
+ if (opts.errorIfDirNotFound && !fs.existsSync(dirPath)) {
45
+ throw new Error(`Directory ${dirPath} does not exist`);
46
+ }
47
+ const serveFile = (req, res, filePath, stats) => {
48
+ if (opts.etag) {
49
+ const etag = `W/"${stats.size}-${stats.mtime.getTime()}"`;
50
+ if (req.headers["if-none-match"] === etag) {
51
+ res.status(304).end();
52
+ return true;
53
+ }
54
+ res.setHeader("ETag", etag);
55
+ }
56
+ if (opts.render && filePath.endsWith(".html")) {
57
+ res.ct("text/html");
58
+ res.render(filePath);
59
+ return true;
60
+ }
61
+ res.ct(getContentType(filePath, opts.utf8));
62
+ fs.createReadStream(filePath).pipe(res);
63
+ return true;
64
+ };
49
65
  return (req, res, next) => {
50
66
  if (req.method.toLowerCase() !== "get")
51
67
  return next();
52
68
  const apiPath = req.middleware.path;
53
69
  const filePath = path.join(dirPath, req.path.replace(apiPath, ""));
54
- if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
55
- res.ct(getContentType(filePath, utf8));
56
- fs.createReadStream(filePath).pipe(res);
57
- return true;
58
- }
59
- if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
60
- const indexPath = path.join(filePath, "index.html");
61
- if (fs.existsSync(indexPath) && fs.statSync(indexPath).isFile()) {
62
- if (!req.path.endsWith("/")) {
63
- res.redirect(req.path + "/");
64
- return true;
70
+ try {
71
+ const stats = fs.statSync(filePath);
72
+ if (stats.isFile()) {
73
+ return serveFile(req, res, filePath, stats);
74
+ }
75
+ if (stats.isDirectory()) {
76
+ const indexPath = path.join(filePath, "index.html");
77
+ try {
78
+ const indexStats = fs.statSync(indexPath);
79
+ if (indexStats.isFile()) {
80
+ if (!req.path.endsWith("/")) {
81
+ res.redirect(req.path + "/");
82
+ return true;
83
+ }
84
+ return serveFile(req, res, indexPath, indexStats);
85
+ }
86
+ }
87
+ catch (e) {
88
+ /* index.html not found, do nothing */
65
89
  }
66
- res.ct(getContentType(indexPath, utf8));
67
- fs.createReadStream(indexPath).pipe(res);
68
- return true;
69
90
  }
70
91
  }
71
- const htmlPath = filePath + ".html";
72
- if (fs.existsSync(htmlPath) && fs.statSync(htmlPath).isFile()) {
73
- res.ct(getContentType(htmlPath, utf8));
74
- fs.createReadStream(htmlPath).pipe(res);
75
- return true;
92
+ catch (e) {
93
+ /* file/dir not found, proceed to check for .html */
94
+ }
95
+ try {
96
+ const htmlPath = filePath + ".html";
97
+ const htmlStats = fs.statSync(htmlPath);
98
+ if (htmlStats.isFile())
99
+ return serveFile(req, res, htmlPath, htmlStats);
100
+ }
101
+ catch (e) {
102
+ /* .html file not found, fall through to next() */
76
103
  }
77
104
  next();
78
105
  };
package/dist/index.d.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import { Logger, LoggerOptions } from "@wxn0brp/lucerna-log";
2
2
  import http from "http";
3
- import { PluginSystem } from "./plugins.js";
3
+ import { PluginSystem } from "./plugin.js";
4
4
  import { renderHTML } from "./render.js";
5
5
  import { FFResponse } from "./res.js";
6
6
  import { Router } from "./router.js";
7
7
  import type { BeforeHandleRequest, FFRequest, ParseBodyFunction, RouteHandler } from "./types.js";
8
+ export type EngineCallback = (path: string, options: any, callback: (e: any, rendered?: string) => void) => void;
8
9
  export interface Opts {
9
10
  loggerOpts?: LoggerOptions;
10
11
  bodyLimit?: string;
@@ -14,14 +15,24 @@ export declare class FalconFrame<Vars extends Record<string, any> = any> extends
14
15
  customParsers: Record<string, ParseBodyFunction>;
15
16
  vars: Vars;
16
17
  opts: Opts;
18
+ engines: Record<string, EngineCallback>;
17
19
  constructor(opts?: Partial<Opts>);
18
- listen(port: number, callback?: (() => void) | boolean, beforeHandleRequest?: BeforeHandleRequest): http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
20
+ listen(port: number | string, callback?: (() => void) | boolean, beforeHandleRequest?: BeforeHandleRequest): http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
19
21
  getApp(beforeHandleRequest?: BeforeHandleRequest): (req: any, res: any) => Promise<void>;
22
+ engine(ext: string, callback: EngineCallback): this;
20
23
  setVar(key: keyof Vars, value: typeof this.vars[keyof Vars]): void;
21
24
  getVar(key: string): typeof this.vars[keyof Vars];
25
+ /**
26
+ * Sets the allowed origins for CORS.
27
+ * This method is a shortcut that simplifies CORS configuration
28
+ * without needing to manually create and register a plugin.
29
+ * @param origin - An array of allowed origins.
30
+ * @example
31
+ * app.setOrigin(["http://example.com", "https://example.com"]);
32
+ */
33
+ setOrigin(origin: string[]): void;
22
34
  }
23
35
  export default FalconFrame;
24
- export { FFRequest, FFResponse, PluginSystem, renderHTML, Router, RouteHandler };
25
- export * as Plugins from "./plugins/index.js";
26
- export * as PluginsEngine from "./plugins.js";
27
36
  export * as Helpers from "./helpers.js";
37
+ export * as PluginsEngine from "./plugin.js";
38
+ export { FFRequest, FFResponse, PluginSystem, renderHTML, RouteHandler, Router };
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Logger } from "@wxn0brp/lucerna-log";
2
2
  import http from "http";
3
- import { PluginSystem } from "./plugins.js";
3
+ import { PluginSystem } from "./plugin.js";
4
+ import { createCORSPlugin } from "./plugins/cors.js";
4
5
  import { renderHTML } from "./render.js";
5
6
  import { handleRequest } from "./req.js";
6
7
  import { FFResponse } from "./res.js";
@@ -10,6 +11,7 @@ export class FalconFrame extends Router {
10
11
  customParsers = {};
11
12
  vars = {};
12
13
  opts = {};
14
+ engines = {};
13
15
  constructor(opts = {}) {
14
16
  super();
15
17
  this.logger = new Logger({
@@ -20,6 +22,15 @@ export class FalconFrame extends Router {
20
22
  bodyLimit: "10m",
21
23
  ...opts,
22
24
  };
25
+ this.engine(".html", (path, options, callback) => {
26
+ try {
27
+ const content = renderHTML(path, options);
28
+ callback(null, content);
29
+ }
30
+ catch (e) {
31
+ callback(e);
32
+ }
33
+ });
23
34
  }
24
35
  listen(port, callback, beforeHandleRequest) {
25
36
  const server = http.createServer(this.getApp(beforeHandleRequest));
@@ -44,6 +55,13 @@ export class FalconFrame extends Router {
44
55
  await handleRequest(req, res, this);
45
56
  };
46
57
  }
58
+ engine(ext, callback) {
59
+ if (ext[0] !== ".") {
60
+ ext = "." + ext;
61
+ }
62
+ this.engines[ext] = callback;
63
+ return this;
64
+ }
47
65
  setVar(key, value) {
48
66
  // @ts-ignore
49
67
  this.vars[key] = value;
@@ -52,9 +70,19 @@ export class FalconFrame extends Router {
52
70
  // @ts-ignore
53
71
  return this.vars[key];
54
72
  }
73
+ /**
74
+ * Sets the allowed origins for CORS.
75
+ * This method is a shortcut that simplifies CORS configuration
76
+ * without needing to manually create and register a plugin.
77
+ * @param origin - An array of allowed origins.
78
+ * @example
79
+ * app.setOrigin(["http://example.com", "https://example.com"]);
80
+ */
81
+ setOrigin(origin) {
82
+ this.use(createCORSPlugin(origin).process);
83
+ }
55
84
  }
56
85
  export default FalconFrame;
57
- export { FFResponse, PluginSystem, renderHTML, Router };
58
- export * as Plugins from "./plugins/index.js";
59
- export * as PluginsEngine from "./plugins.js";
60
86
  export * as Helpers from "./helpers.js";
87
+ export * as PluginsEngine from "./plugin.js";
88
+ export { FFResponse, PluginSystem, renderHTML, Router };
@@ -3,7 +3,8 @@ export type PluginId = string;
3
3
  export interface Plugin {
4
4
  id: PluginId;
5
5
  process: RouteHandler;
6
- priority?: number;
6
+ before?: PluginId | PluginId[];
7
+ after?: PluginId | PluginId[];
7
8
  }
8
9
  export interface PluginOptions {
9
10
  before?: PluginId | PluginId[];
@@ -7,30 +7,29 @@ export class PluginSystem {
7
7
  * @param options - Options for positioning plugins
8
8
  */
9
9
  register(plugin, options) {
10
- if (this.plugins.some(p => p.id === plugin.id)) {
10
+ if (this.plugins.some((p) => p.id === plugin.id)) {
11
11
  throw new Error(`Plugin with id '${plugin.id}' already registered`);
12
12
  }
13
- // Add plugin to the list
14
13
  this.plugins.push(plugin);
15
- // Update the execution order
16
- this.updateExecutionOrder(plugin.id, options);
14
+ this.updateExecutionOrder(plugin, options);
17
15
  }
18
16
  /**
19
17
  * Updates the execution order of plugins
20
18
  * @param pluginId - ID of the plugin to position
21
19
  * @param options - Options for positioning
22
20
  */
23
- updateExecutionOrder(pluginId, options) {
21
+ updateExecutionOrder(plugin, options) {
22
+ const pluginId = plugin.id;
24
23
  if (this.executionOrder.includes(pluginId))
25
24
  return;
26
25
  const resolveTarget = (target) => {
27
26
  if (!target)
28
27
  return null;
29
28
  const list = Array.isArray(target) ? target : [target];
30
- return list.find(id => this.executionOrder.includes(id)) || null;
29
+ return list.find((id) => this.executionOrder.includes(id)) || null;
31
30
  };
32
- const beforeTarget = resolveTarget(options?.before);
33
- const afterTarget = resolveTarget(options?.after);
31
+ const beforeTarget = resolveTarget(options?.before || plugin.before);
32
+ const afterTarget = resolveTarget(options?.after || plugin.after);
34
33
  if (beforeTarget) {
35
34
  const index = this.executionOrder.indexOf(beforeTarget);
36
35
  this.executionOrder.splice(index, 0, pluginId);
@@ -74,7 +73,7 @@ export class PluginSystem {
74
73
  return;
75
74
  }
76
75
  const pluginId = this.executionOrder[index];
77
- const plugin = this.plugins.find(p => p.id === pluginId);
76
+ const plugin = this.plugins.find((p) => p.id === pluginId);
78
77
  if (!plugin) {
79
78
  throw new Error(`Plugin '${pluginId}' not found`);
80
79
  }
@@ -87,7 +86,7 @@ export class PluginSystem {
87
86
  */
88
87
  getPluginsInOrder() {
89
88
  return this.executionOrder
90
- .map(id => this.plugins.find(p => p.id === id))
89
+ .map((id) => this.plugins.find((p) => p.id === id))
91
90
  .filter((p) => p !== undefined);
92
91
  }
93
92
  }
@@ -1,7 +1,8 @@
1
- import { Plugin } from "../plugins.js";
1
+ import { Plugin } from "../plugin.js";
2
2
  interface Opts {
3
3
  accessControlAllowMethods?: boolean;
4
4
  accessControlAllowHeaders?: boolean;
5
+ headers?: Record<string, string>;
5
6
  }
6
7
  export declare function createCORSPlugin(allowedOrigins: string[], opts?: Opts): Plugin;
7
8
  export {};
@@ -13,6 +13,11 @@ export function createCORSPlugin(allowedOrigins, opts = {}) {
13
13
  return {
14
14
  id: "cors",
15
15
  process: (req, res, next) => {
16
+ if (opts.headers) {
17
+ for (const [key, value] of Object.entries(opts.headers)) {
18
+ res.setHeader(key, value);
19
+ }
20
+ }
16
21
  if (allowedOrigins.includes("*")) {
17
22
  res.setHeader("Access-Control-Allow-Origin", "*");
18
23
  setHeader(res, opts);
@@ -1,2 +1,2 @@
1
- import { Plugin } from "../plugins.js";
1
+ import { Plugin } from "../plugin.js";
2
2
  export declare function createRateLimiterPlugin(maxRequests: number, windowMs: number): Plugin;
@@ -0,0 +1,2 @@
1
+ import { Plugin } from "../plugin.js";
2
+ export declare function createSecurityPlugin(): Plugin;
@@ -0,0 +1,14 @@
1
+ export function createSecurityPlugin() {
2
+ return {
3
+ id: "security",
4
+ process: (req, res, next) => {
5
+ res.setHeader("X-Content-Type-Options", "nosniff");
6
+ res.setHeader("X-Frame-Options", "SAMEORIGIN");
7
+ res.setHeader("Referrer-Policy", "no-referrer");
8
+ res.setHeader("X-XSS-Protection", "1; mode=block");
9
+ res.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
10
+ next();
11
+ },
12
+ after: "cors",
13
+ };
14
+ }
package/dist/render.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  interface RenderData {
2
2
  [key: string]: string;
3
3
  }
4
- export declare function renderHTML(templatePath: string, data: RenderData): string;
4
+ export declare function renderHTML(templatePath: string, data: RenderData, renderedPaths?: string[]): string;
5
5
  export {};
package/dist/render.js CHANGED
@@ -1,18 +1,25 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
- export function renderHTML(templatePath, data) {
4
- if (!fs.existsSync(templatePath))
5
- return "Template not found";
6
- let template = fs.readFileSync(templatePath, "utf8");
7
- // Inserting data, e.g. {{name}}
8
- template = template.replace(/{{(.*?)}}/g, (_, key) => data[key.trim()] || "");
9
- // Loading partials, e.g. <!-- include header -->
10
- template = template.replace(/<!--\s*include\s*(.*?)\s*-->/g, (_, partialName) => {
11
- const partialPath = path.join(path.dirname(templatePath), partialName + ".html");
12
- if (fs.existsSync(partialPath))
13
- return renderHTML(partialPath, data);
14
- else
15
- return `<!-- Partial "${partialName}" (${partialPath.replace(process.cwd() + "/", "")}) not found -->`;
16
- });
17
- return template;
3
+ export function renderHTML(templatePath, data, renderedPaths = []) {
4
+ try {
5
+ const realPath = path.resolve(templatePath);
6
+ if (renderedPaths.includes(realPath)) {
7
+ return `<!-- Circular dependency detected: tried to render ${templatePath} again -->`;
8
+ }
9
+ let template = fs.readFileSync(templatePath, "utf8");
10
+ // Inserting data, e.g. {{name}}
11
+ template = template.replace(/{{(.*?)}}/g, (_, key) => data[key.trim()] || "");
12
+ // Loading partials, e.g. <!-- include header -->
13
+ template = template.replace(/<!--\s*include\s*(.*?)\s*-->/g, (_, partialName) => {
14
+ const partialPath = path.join(path.dirname(templatePath), partialName + ".html");
15
+ return renderHTML(partialPath, data, [
16
+ ...renderedPaths,
17
+ realPath,
18
+ ]);
19
+ });
20
+ return template;
21
+ }
22
+ catch (error) {
23
+ return `<!-- Template not found: ${templatePath} -->`;
24
+ }
18
25
  }
package/dist/req.js CHANGED
@@ -31,9 +31,11 @@ export function handleRequest(req, res, FF) {
31
31
  logger.info(`Incoming request: ${req.method} ${req.url}`);
32
32
  const middlewaresPath = req.path + "/";
33
33
  const middlewares = getMiddlewares(FF.middlewares, middlewaresPath.replace(/\/+/g, "/"));
34
- const matchedTypeMiddlewares = middlewares.filter(middleware => middleware.method === req.method.toLowerCase() || middleware.method === "all");
34
+ const matchedTypeMiddlewares = middlewares.filter((middleware) => middleware.method === req.method.toLowerCase() ||
35
+ middleware.method === "all");
35
36
  const matchedMiddlewares = matchMiddleware(req.path, matchedTypeMiddlewares);
36
- logger.debug("Matched middlewares: " + matchedMiddlewares.map(middleware => middleware.path).join(", "));
37
+ logger.debug("Matched middlewares: " +
38
+ matchedMiddlewares.map((middleware) => middleware.path).join(", "));
37
39
  if (matchedMiddlewares.length === 0) {
38
40
  res.status(404).end("404: File had second thoughts");
39
41
  return;
@@ -69,12 +71,15 @@ export function handleRequest(req, res, FF) {
69
71
  }
70
72
  }
71
73
  }
72
- if (req.method === "GET" && middlewares[middlewares.length - 1]?.sse) {
74
+ if (req.method === "GET" ||
75
+ req.method === "HEAD" ||
76
+ req.method === "OPTIONS") {
77
+ req.body = {};
73
78
  next();
74
79
  return;
75
80
  }
76
81
  let body = "";
77
- req.on("data", chunk => (body += chunk.toString()));
82
+ req.on("data", (chunk) => (body += chunk.toString()));
78
83
  req.on("end", async () => {
79
84
  const parsedBody = await parseBody(req, body, FF);
80
85
  Object.assign(req, parsedBody);
package/dist/res.d.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  import http from "http";
2
- import { CookieOptions } from "./types.js";
3
2
  import FalconFrame from "./index.js";
3
+ import { CookieOptions } from "./types.js";
4
4
  export declare class FFResponse extends http.ServerResponse {
5
5
  _ended: boolean;
6
6
  FF: FalconFrame;
7
7
  /**
8
8
  * bind end for compatibility
9
- */
9
+ */
10
10
  send(data: string): void;
11
11
  /**
12
12
  * Sets a header. This is a shortcut for setHeader.
@@ -56,13 +56,13 @@ export declare class FFResponse extends http.ServerResponse {
56
56
  */
57
57
  sendFile(filePath: string, contentType?: string): this;
58
58
  /**
59
- * Renders an HTML template with the provided data and sends it as the response.
60
- * Sets the "Content-Type" header to "text/html".
61
- * @param templatePath The path to the HTML template file.
59
+ * Renders a view with the given data and sends it as the response.
60
+ * It uses the registered template engine.
61
+ * @param view The name of the view file to render.
62
62
  * @param data An object containing data to be injected into the template.
63
63
  * @returns The response object.
64
64
  */
65
- render(templatePath: string, data?: any): this;
65
+ render(view: string, data?: any): this;
66
66
  /**
67
67
  * Initialize SSE headers to start a server-sent event stream.
68
68
  * Sets:
package/dist/res.js CHANGED
@@ -1,14 +1,13 @@
1
+ import { createReadStream } from "fs";
1
2
  import http from "http";
3
+ import path from "path";
2
4
  import { getContentType } from "./helpers.js";
3
- import { createReadStream } from "fs";
4
- import { renderHTML } from "./render.js";
5
- import { resolve } from "path";
6
5
  export class FFResponse extends http.ServerResponse {
7
6
  _ended = false;
8
7
  FF;
9
8
  /**
10
9
  * bind end for compatibility
11
- */
10
+ */
12
11
  send(data) {
13
12
  this.end(data);
14
13
  }
@@ -99,18 +98,47 @@ export class FFResponse extends http.ServerResponse {
99
98
  return this;
100
99
  }
101
100
  /**
102
- * Renders an HTML template with the provided data and sends it as the response.
103
- * Sets the "Content-Type" header to "text/html".
104
- * @param templatePath The path to the HTML template file.
101
+ * Renders a view with the given data and sends it as the response.
102
+ * It uses the registered template engine.
103
+ * @param view The name of the view file to render.
105
104
  * @param data An object containing data to be injected into the template.
106
105
  * @returns The response object.
107
106
  */
108
- render(templatePath, data = {}) {
109
- this.setHeader("Content-Type", "text/html");
110
- if (this.FF.vars["views"] && !templatePath.endsWith(".html")) {
111
- templatePath = resolve(this.FF.vars["views"] + "/" + templatePath + ".html");
107
+ render(view, data = {}) {
108
+ const ff = this.FF;
109
+ const viewEngine = ff.getVar("view engine");
110
+ const viewsDir = ff.getVar("views") || ".";
111
+ let finalExt = path.extname(view);
112
+ let filePath = view;
113
+ if (!finalExt) {
114
+ const defaultExt = viewEngine ? (viewEngine.startsWith(".") ? viewEngine : "." + viewEngine) : ".html";
115
+ finalExt = defaultExt;
116
+ filePath += finalExt;
117
+ }
118
+ const engine = ff.engines[finalExt];
119
+ if (!engine) {
120
+ const errMessage = `No engine registered for extension ${finalExt}`;
121
+ ff.logger.error(errMessage);
122
+ this.status(500).end("Server Error: " + errMessage);
123
+ return this;
124
+ }
125
+ const fullPath = path.resolve(viewsDir, filePath);
126
+ try {
127
+ engine(fullPath, data, (err, str) => {
128
+ if (err) {
129
+ ff.logger.error(`Error rendering view: ${err}`);
130
+ this.status(500).end("Server Error: Failed to render view.");
131
+ }
132
+ else {
133
+ this.setHeader("Content-Type", "text/html");
134
+ this.end(str);
135
+ }
136
+ });
137
+ }
138
+ catch (err) {
139
+ ff.logger.error(`Unhandled error in template engine: ${err}`);
140
+ this.status(500).end("Server Error: Unhandled exception in template engine.");
112
141
  }
113
- this.end(renderHTML(templatePath, data));
114
142
  return this;
115
143
  }
116
144
  /**
package/dist/router.d.ts CHANGED
@@ -1,13 +1,15 @@
1
- import { Method, Middleware, RouteHandler } from "./types.js";
1
+ import { PluginSystem } from "./plugin.js";
2
+ import { Method, Middleware, RouteHandler, StaticServeOptions } from "./types.js";
3
+ export type MiddlewareFn = RouteHandler | Router | PluginSystem;
2
4
  export declare class Router {
3
5
  middlewares: Middleware[];
4
6
  addRoute(method: Method, path: string, ...handlers: RouteHandler[]): number;
5
- use(path?: string | RouteHandler | Router, middlewareFn?: RouteHandler | Router, method?: Method): this;
7
+ use(path?: string | MiddlewareFn, middlewareFn?: MiddlewareFn, method?: Method): this;
6
8
  get(path: string, ...handlers: RouteHandler[]): this;
7
9
  post(path: string, ...handlers: RouteHandler[]): this;
8
10
  put(path: string, ...handlers: RouteHandler[]): this;
9
11
  delete(path: string, ...handlers: RouteHandler[]): this;
10
12
  all(path: string, ...handlers: RouteHandler[]): this;
11
- static(apiPath: string, dirPath?: string, utf8?: boolean): this;
13
+ static(apiPath: string, dirPath?: string, opts?: StaticServeOptions): this;
12
14
  sse(path: string, ...handlers: RouteHandler[]): this;
13
15
  }
package/dist/router.js CHANGED
@@ -1,13 +1,14 @@
1
1
  import { handleStaticFiles } from "./helpers.js";
2
+ import { PluginSystem } from "./plugin.js";
2
3
  export class Router {
3
4
  middlewares = [];
4
5
  addRoute(method, path, ...handlers) {
5
6
  const handler = handlers.pop();
6
- handlers.forEach(middleware => this.use(path, middleware));
7
+ handlers.forEach((middleware) => this.use(path, middleware));
7
8
  return this.middlewares.push({ path, middleware: handler, method });
8
9
  }
9
10
  use(path = "/", middlewareFn, method = "all") {
10
- if (typeof path === "function" || path instanceof Router) {
11
+ if (typeof path === "function" || path instanceof Router || path instanceof PluginSystem) {
11
12
  middlewareFn = path;
12
13
  path = "/";
13
14
  }
@@ -15,11 +16,14 @@ export class Router {
15
16
  path,
16
17
  method,
17
18
  middleware: null,
18
- use: true
19
+ use: true,
19
20
  };
20
21
  if (middlewareFn instanceof Router) {
21
22
  middleware.router = middlewareFn.middlewares;
22
23
  }
24
+ else if (middlewareFn instanceof PluginSystem) {
25
+ middleware.middleware = middlewareFn.getRouteHandler();
26
+ }
23
27
  else {
24
28
  middleware.middleware = middlewareFn;
25
29
  }
@@ -46,12 +50,12 @@ export class Router {
46
50
  this.addRoute("all", path, ...handlers);
47
51
  return this;
48
52
  }
49
- static(apiPath, dirPath, utf8 = true) {
53
+ static(apiPath, dirPath, opts = {}) {
50
54
  if (!dirPath) {
51
55
  dirPath = apiPath;
52
56
  apiPath = "/";
53
57
  }
54
- this.use(apiPath, handleStaticFiles(dirPath, utf8));
58
+ this.use(apiPath, handleStaticFiles(dirPath, opts));
55
59
  return this;
56
60
  }
57
61
  sse(path, ...handlers) {
package/dist/types.d.ts CHANGED
@@ -54,3 +54,9 @@ export interface ValidationResult {
54
54
  };
55
55
  }
56
56
  export type BeforeHandleRequest = (req: http.IncomingMessage, res: http.ServerResponse) => any;
57
+ export interface StaticServeOptions {
58
+ utf8?: boolean;
59
+ render?: boolean;
60
+ etag?: boolean;
61
+ errorIfDirNotFound?: boolean;
62
+ }
package/dist/valid.js CHANGED
@@ -24,7 +24,8 @@ export function validate(schema, data) {
24
24
  }
25
25
  break;
26
26
  case "min":
27
- if (typeof value === "string" && value.length < parseInt(param)) {
27
+ if (typeof value === "string" &&
28
+ value.length < parseInt(param)) {
28
29
  fieldErrors.push(`${key} must be at least ${param} characters long`);
29
30
  }
30
31
  if (typeof value === "number" && value < parseInt(param)) {
@@ -32,7 +33,8 @@ export function validate(schema, data) {
32
33
  }
33
34
  break;
34
35
  case "max":
35
- if (typeof value === "string" && value.length > parseInt(param)) {
36
+ if (typeof value === "string" &&
37
+ value.length > parseInt(param)) {
36
38
  fieldErrors.push(`${key} must not exceed ${param} characters`);
37
39
  }
38
40
  if (typeof value === "number" && value > parseInt(param)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wxn0brp/falcon-frame",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "author": "wxn0brP",
@@ -1,2 +0,0 @@
1
- export * from "./cors.js";
2
- export * from "./rateLimit.js";
@@ -1,2 +0,0 @@
1
- export * from "./cors.js";
2
- export * from "./rateLimit.js";