@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 +11 -5
- package/dist/helpers.d.ts +2 -2
- package/dist/helpers.js +75 -48
- package/dist/index.d.ts +16 -5
- package/dist/index.js +32 -4
- package/dist/{plugins.d.ts → plugin.d.ts} +2 -1
- package/dist/{plugins.js → plugin.js} +9 -10
- package/dist/plugins/cors.d.ts +2 -1
- package/dist/plugins/cors.js +5 -0
- package/dist/plugins/rateLimit.d.ts +1 -1
- package/dist/plugins/security.d.ts +2 -0
- package/dist/plugins/security.js +14 -0
- package/dist/render.d.ts +1 -1
- package/dist/render.js +22 -15
- package/dist/req.js +9 -4
- package/dist/res.d.ts +6 -6
- package/dist/res.js +40 -12
- package/dist/router.d.ts +5 -3
- package/dist/router.js +9 -5
- package/dist/types.d.ts +6 -0
- package/dist/valid.js +4 -2
- package/package.json +1 -1
- package/dist/plugins/index.d.ts +0 -2
- package/dist/plugins/index.js +0 -2
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) => ({
|
|
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":
|
|
42
|
-
|
|
43
|
-
case "
|
|
44
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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 "./
|
|
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 "./
|
|
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 };
|
|
@@ -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
|
-
|
|
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(
|
|
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
|
}
|
package/dist/plugins/cors.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { Plugin } from "../
|
|
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 {};
|
package/dist/plugins/cors.js
CHANGED
|
@@ -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 "../
|
|
1
|
+
import { Plugin } from "../plugin.js";
|
|
2
2
|
export declare function createRateLimiterPlugin(maxRequests: number, windowMs: number): 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
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return
|
|
16
|
-
|
|
17
|
-
|
|
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() ||
|
|
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: " +
|
|
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"
|
|
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
|
|
60
|
-
*
|
|
61
|
-
* @param
|
|
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(
|
|
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
|
|
103
|
-
*
|
|
104
|
-
* @param
|
|
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(
|
|
109
|
-
this.
|
|
110
|
-
|
|
111
|
-
|
|
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 {
|
|
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 |
|
|
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,
|
|
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,
|
|
53
|
+
static(apiPath, dirPath, opts = {}) {
|
|
50
54
|
if (!dirPath) {
|
|
51
55
|
dirPath = apiPath;
|
|
52
56
|
apiPath = "/";
|
|
53
57
|
}
|
|
54
|
-
this.use(apiPath, handleStaticFiles(dirPath,
|
|
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" &&
|
|
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" &&
|
|
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
package/dist/plugins/index.d.ts
DELETED
package/dist/plugins/index.js
DELETED