@wxn0brp/falcon-frame 0.0.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/.github/workflows/build.yml +20 -0
- package/LICENSE +21 -0
- package/README.md +113 -0
- package/dist/helpers.d.ts +5 -0
- package/dist/helpers.js +73 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +69 -0
- package/dist/req.d.ts +4 -0
- package/dist/req.js +109 -0
- package/dist/res.d.ts +10 -0
- package/dist/res.js +39 -0
- package/dist/test.d.ts +1 -0
- package/dist/test.js +60 -0
- package/dist/types.d.ts +46 -0
- package/dist/types.js +9 -0
- package/dist/valid.d.ts +2 -0
- package/dist/valid.js +56 -0
- package/package.json +33 -0
- package/public/index.html +47 -0
- package/public/style.css +41 -0
- package/src/helpers.ts +79 -0
- package/src/index.ts +87 -0
- package/src/req.ts +123 -0
- package/src/res.ts +43 -0
- package/src/test.ts +68 -0
- package/src/types.ts +56 -0
- package/src/valid.ts +62 -0
- package/suglite.json +10 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: Build
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- master
|
|
7
|
+
tags:
|
|
8
|
+
- "*"
|
|
9
|
+
|
|
10
|
+
workflow_dispatch:
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build:
|
|
14
|
+
uses: wxn0brP/workflow-dist/.github/workflows/build-ts.yml@main
|
|
15
|
+
with:
|
|
16
|
+
scriptsHandling: "remove-all"
|
|
17
|
+
publishToNpm: true
|
|
18
|
+
|
|
19
|
+
secrets:
|
|
20
|
+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 wxn0brP
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# FalconFrame
|
|
2
|
+
|
|
3
|
+
> Lightweight modular TypeScript web framework built for speed and clarity.
|
|
4
|
+
|
|
5
|
+
**FalconFrame** is a minimalist web framework inspired by the middleware-first style, with routing similar to Express, but written in pure TypeScript. It supports static files, data validation, and cookie management, all without external dependencies apart from a logger.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## ✨ Features
|
|
10
|
+
|
|
11
|
+
- 🚀 Zero-dependency routing engine
|
|
12
|
+
- 🧱 Middleware chaining
|
|
13
|
+
- 📁 Static file serving
|
|
14
|
+
- 📦 Request validation via inline schemas
|
|
15
|
+
- 🍪 Cookie management
|
|
16
|
+
- 🔍 Debuggable logger
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 📦 Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
yarn add github:wxn0brP/FalconFrame#dist
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 🚦 Usage Example
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import FalconFrame from "@wxn0brp/falcon-frame";
|
|
30
|
+
const app = new FalconFrame();
|
|
31
|
+
|
|
32
|
+
const USERS = {
|
|
33
|
+
admin: "hunter2",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Global middleware
|
|
37
|
+
app.use((req, res, next) => {
|
|
38
|
+
console.log(`[${req.method}] ${req.path}`);
|
|
39
|
+
next();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Middleware: auth check
|
|
43
|
+
const requireAuth = (req, res, next) => {
|
|
44
|
+
if (req.cookies.session === "admin") {
|
|
45
|
+
return next();
|
|
46
|
+
}
|
|
47
|
+
res.status(401).json({ error: "Unauthorized" });
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Static files
|
|
51
|
+
app.static("/", "public");
|
|
52
|
+
|
|
53
|
+
app.post("/login", (req, res) => {
|
|
54
|
+
const { valid, validErrors } = req.valid({
|
|
55
|
+
username: "required|string",
|
|
56
|
+
password: "required|string",
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!valid) {
|
|
60
|
+
return res.status(400).json({ status: "error", errors: validErrors });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const { username, password } = req.body;
|
|
64
|
+
if (USERS[username] === password) {
|
|
65
|
+
res.cookie("session", username, { httpOnly: true });
|
|
66
|
+
return res.redirect("/dashboard");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
res.status(401).json({ status: "fail", message: "Invalid credentials" });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
app.post("/register", (req, res, next) => {
|
|
73
|
+
const { valid, validErrors } = req.valid({
|
|
74
|
+
username: "required|string|min:3|max:20",
|
|
75
|
+
password: "required|string|min:8",
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (!valid) {
|
|
79
|
+
return res.status(400).json({ status: "error", errors: validErrors });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
next();
|
|
83
|
+
}, (req, res) => {
|
|
84
|
+
const { username, password } = req.body;
|
|
85
|
+
USERS[username] = password;
|
|
86
|
+
return { status: "success", message: "User registered successfully" };
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Protected route
|
|
90
|
+
app.get("/dashboard", requireAuth, (req, res) => {
|
|
91
|
+
return {
|
|
92
|
+
message: `👑 Welcome to the dashboard, ${req.cookies.session}`,
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
app.post("/logout", (req, res) => {
|
|
97
|
+
res.cookie("session", "", { maxAge: 0 });
|
|
98
|
+
res.json({ message: "👋 Logged out" });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// if no route matches
|
|
102
|
+
app.all("*", (req, res) => {
|
|
103
|
+
res.status(404).json({ error: "Not found" });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
app.listen(3000, () => {
|
|
107
|
+
console.log("Server running on http://localhost:3000");
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## 📜 License
|
|
112
|
+
|
|
113
|
+
Published under the MIT license
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Body, Cookies, RouteHandler } from "./types.js";
|
|
2
|
+
export declare function parseCookies(cookieHeader: string): Cookies;
|
|
3
|
+
export declare function parseBody(contentType: string, body: string): Body;
|
|
4
|
+
export declare function getContentType(filePath: string): string;
|
|
5
|
+
export declare function handleStaticFiles(apiPath: string, dirPath: string): RouteHandler;
|
package/dist/helpers.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import querystring from "querystring";
|
|
4
|
+
export function parseCookies(cookieHeader) {
|
|
5
|
+
const cookies = {};
|
|
6
|
+
cookieHeader.split(";").forEach(cookie => {
|
|
7
|
+
const [name, ...valueParts] = cookie.split("=");
|
|
8
|
+
const value = decodeURIComponent(valueParts.join("=").trim());
|
|
9
|
+
cookies[name.trim()] = value;
|
|
10
|
+
});
|
|
11
|
+
return cookies;
|
|
12
|
+
}
|
|
13
|
+
export function parseBody(contentType, body) {
|
|
14
|
+
if (contentType.includes("application/json")) {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(body);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
else if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
23
|
+
return querystring.parse(body);
|
|
24
|
+
}
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
export function getContentType(filePath) {
|
|
28
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
29
|
+
switch (ext) {
|
|
30
|
+
case ".html":
|
|
31
|
+
return "text/html";
|
|
32
|
+
case ".css":
|
|
33
|
+
return "text/css";
|
|
34
|
+
case ".js":
|
|
35
|
+
return "application/javascript";
|
|
36
|
+
case ".json":
|
|
37
|
+
return "application/json";
|
|
38
|
+
case ".png":
|
|
39
|
+
return "image/png";
|
|
40
|
+
case ".jpg":
|
|
41
|
+
case ".jpeg":
|
|
42
|
+
return "image/jpeg";
|
|
43
|
+
case ".gif":
|
|
44
|
+
return "image/gif";
|
|
45
|
+
case ".svg":
|
|
46
|
+
return "image/svg+xml";
|
|
47
|
+
default:
|
|
48
|
+
return "application/octet-stream";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export function handleStaticFiles(apiPath, dirPath) {
|
|
52
|
+
return (req, res, next) => {
|
|
53
|
+
if (!req.path.startsWith(apiPath))
|
|
54
|
+
return next();
|
|
55
|
+
const filePath = path.join(dirPath, req.path.slice(apiPath.length));
|
|
56
|
+
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
|
57
|
+
res.setHeader("Content-Type", getContentType(filePath));
|
|
58
|
+
fs.createReadStream(filePath).pipe(res);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
if (req.path.endsWith("/")) {
|
|
62
|
+
if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
|
|
63
|
+
const indexPath = path.join(filePath, "index.html");
|
|
64
|
+
if (fs.existsSync(indexPath) && fs.statSync(indexPath).isFile()) {
|
|
65
|
+
res.setHeader("Content-Type", getContentType(indexPath));
|
|
66
|
+
fs.createReadStream(indexPath).pipe(res);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
next();
|
|
72
|
+
};
|
|
73
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Logger, LoggerOptions } from "@wxn0brp/wts-logger";
|
|
2
|
+
import http from "http";
|
|
3
|
+
import { FFResponse } from "./res.js";
|
|
4
|
+
import { FFRequest, Method, Middleware, RouteHandler } from "./types.js";
|
|
5
|
+
export declare class FalconFrame {
|
|
6
|
+
middlewares: Middleware[];
|
|
7
|
+
logger: Logger;
|
|
8
|
+
constructor(loggerOpts?: LoggerOptions);
|
|
9
|
+
addRoute(method: Method, path: string, ...handlers: RouteHandler[]): void;
|
|
10
|
+
use(path?: string | RouteHandler, middleware?: RouteHandler, method?: Method): void;
|
|
11
|
+
get(path: string, ...handlers: RouteHandler[]): void;
|
|
12
|
+
post(path: string, ...handlers: RouteHandler[]): void;
|
|
13
|
+
put(path: string, ...handlers: RouteHandler[]): void;
|
|
14
|
+
delete(path: string, ...handlers: RouteHandler[]): void;
|
|
15
|
+
all(path: string, ...handlers: RouteHandler[]): void;
|
|
16
|
+
static(apiPath: string, dirPath: string): void;
|
|
17
|
+
listen(port: number, callback?: () => void): http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
|
|
18
|
+
getApp(): (req: any, res: any) => void;
|
|
19
|
+
}
|
|
20
|
+
export default FalconFrame;
|
|
21
|
+
export { FFResponse, FFRequest, RouteHandler, };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Logger } from "@wxn0brp/wts-logger";
|
|
2
|
+
import http from "http";
|
|
3
|
+
import { handleStaticFiles } from "./helpers.js";
|
|
4
|
+
import { handleRequest } from "./req.js";
|
|
5
|
+
import { FFResponse } from "./res.js";
|
|
6
|
+
import { FFRequest } from "./types.js";
|
|
7
|
+
export class FalconFrame {
|
|
8
|
+
middlewares = [];
|
|
9
|
+
logger;
|
|
10
|
+
constructor(loggerOpts) {
|
|
11
|
+
this.logger = new Logger({
|
|
12
|
+
loggerName: "falcon-frame",
|
|
13
|
+
...loggerOpts
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
addRoute(method, path, ...handlers) {
|
|
17
|
+
const handler = handlers.pop();
|
|
18
|
+
handlers.forEach(middleware => this.use(path, middleware));
|
|
19
|
+
this.middlewares.push({ path, middleware: handler, method });
|
|
20
|
+
}
|
|
21
|
+
use(path = "/", middleware, method = "all") {
|
|
22
|
+
if (typeof path === "function") {
|
|
23
|
+
middleware = path;
|
|
24
|
+
path = "/";
|
|
25
|
+
}
|
|
26
|
+
this.middlewares.push({ path, middleware, method, use: true });
|
|
27
|
+
}
|
|
28
|
+
get(path, ...handlers) {
|
|
29
|
+
this.addRoute("get", path, ...handlers);
|
|
30
|
+
}
|
|
31
|
+
post(path, ...handlers) {
|
|
32
|
+
this.addRoute("post", path, ...handlers);
|
|
33
|
+
}
|
|
34
|
+
put(path, ...handlers) {
|
|
35
|
+
this.addRoute("put", path, ...handlers);
|
|
36
|
+
}
|
|
37
|
+
delete(path, ...handlers) {
|
|
38
|
+
this.addRoute("delete", path, ...handlers);
|
|
39
|
+
}
|
|
40
|
+
all(path, ...handlers) {
|
|
41
|
+
this.addRoute("all", path, ...handlers);
|
|
42
|
+
}
|
|
43
|
+
static(apiPath, dirPath) {
|
|
44
|
+
this.middlewares.push({
|
|
45
|
+
path: (apiPath + "/*").replace("//", "/"),
|
|
46
|
+
method: "get",
|
|
47
|
+
middleware: handleStaticFiles(apiPath, dirPath)
|
|
48
|
+
});
|
|
49
|
+
this.middlewares.push({
|
|
50
|
+
path: apiPath,
|
|
51
|
+
method: "get",
|
|
52
|
+
middleware: handleStaticFiles(apiPath, dirPath)
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
listen(port, callback) {
|
|
56
|
+
const server = http.createServer((req, res) => {
|
|
57
|
+
handleRequest(req, res, this);
|
|
58
|
+
});
|
|
59
|
+
server.listen(port, callback);
|
|
60
|
+
return server;
|
|
61
|
+
}
|
|
62
|
+
getApp() {
|
|
63
|
+
return (req, res) => {
|
|
64
|
+
handleRequest(req, res, this);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export default FalconFrame;
|
|
69
|
+
export { FFResponse, FFRequest, };
|
package/dist/req.d.ts
ADDED
package/dist/req.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { URL } from "url";
|
|
2
|
+
import { parseBody, parseCookies } from "./helpers.js";
|
|
3
|
+
import { FFResponse } from "./res.js";
|
|
4
|
+
import { validate } from "./valid.js";
|
|
5
|
+
export function handleRequest(req, res, FF) {
|
|
6
|
+
Object.setPrototypeOf(res, FFResponse.prototype);
|
|
7
|
+
const originalEnd = res.end;
|
|
8
|
+
res.end = function (...any) {
|
|
9
|
+
res._ended = true;
|
|
10
|
+
return originalEnd.call(res, ...any);
|
|
11
|
+
};
|
|
12
|
+
const { logger, middlewares } = FF;
|
|
13
|
+
const parsedUrl = new URL(req.url || "", "http://localhost");
|
|
14
|
+
req.path = parsedUrl.pathname || "/";
|
|
15
|
+
req.query = Object.fromEntries(parsedUrl.searchParams);
|
|
16
|
+
req.cookies = parseCookies(req.headers.cookie || "");
|
|
17
|
+
req.params = {};
|
|
18
|
+
req.valid = (schema) => validate(schema, req.body);
|
|
19
|
+
logger.info(`Incoming request: ${req.method} ${req.url}`);
|
|
20
|
+
let body = "";
|
|
21
|
+
req.on("data", chunk => (body += chunk.toString()));
|
|
22
|
+
req.on("end", () => {
|
|
23
|
+
const contentType = req.headers["content-type"] || "";
|
|
24
|
+
req.body = parseBody(contentType, body);
|
|
25
|
+
logger.debug(`Request body: ${JSON.stringify(req.body)}`);
|
|
26
|
+
const matchedTypeMiddlewares = middlewares.filter(middleware => middleware.method === req.method.toLocaleLowerCase() || middleware.method === "all");
|
|
27
|
+
const matchedMiddlewares = matchMiddleware(req.path, matchedTypeMiddlewares);
|
|
28
|
+
if (matchedMiddlewares.length === 0) {
|
|
29
|
+
return res.status(404).end("404: File had second thoughts.");
|
|
30
|
+
}
|
|
31
|
+
logger.debug("Matched middlewares: " + matchedMiddlewares.map(middleware => middleware.path).join(", "));
|
|
32
|
+
let middlewareIndex = 0;
|
|
33
|
+
const next = async () => {
|
|
34
|
+
if (middlewareIndex >= matchedMiddlewares.length) {
|
|
35
|
+
return res.status(404).end("404: File had second thoughts");
|
|
36
|
+
}
|
|
37
|
+
const middleware = matchedMiddlewares[middlewareIndex++];
|
|
38
|
+
logger.debug(`Executing middleware ${middlewareIndex} of ${matchedMiddlewares.length} matched for path [${middleware.path}]`);
|
|
39
|
+
if (middleware.path.includes(":")) {
|
|
40
|
+
const middlewareParts = middleware.path.split("/");
|
|
41
|
+
const reqPathParts = req.path.split("/");
|
|
42
|
+
req.params = {};
|
|
43
|
+
for (let i = 0; i < middlewareParts.length; i++) {
|
|
44
|
+
if (middlewareParts[i].startsWith(":")) {
|
|
45
|
+
const paramName = middlewareParts[i].slice(1);
|
|
46
|
+
req.params[paramName] = reqPathParts[i];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const result = await middleware.middleware(req, res, next);
|
|
51
|
+
if (result && !res._ended) {
|
|
52
|
+
if (typeof result === "string") {
|
|
53
|
+
return res.end(result);
|
|
54
|
+
}
|
|
55
|
+
else if (typeof result === "object") {
|
|
56
|
+
return res.json(result);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
next();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function matchMiddleware(url, middlewares) {
|
|
64
|
+
const matchedMiddlewares = [];
|
|
65
|
+
url = url.replace(/\/$/, "");
|
|
66
|
+
for (const middleware of middlewares) {
|
|
67
|
+
const cleanedMiddleware = middleware.path.replace(/\/$/, "");
|
|
68
|
+
if (middleware.use) {
|
|
69
|
+
if (url.startsWith(cleanedMiddleware)) {
|
|
70
|
+
matchedMiddlewares.push(middleware);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else if (cleanedMiddleware === "*") {
|
|
74
|
+
matchedMiddlewares.push(middleware);
|
|
75
|
+
}
|
|
76
|
+
else if (cleanedMiddleware.endsWith("/*")) {
|
|
77
|
+
const prefix = cleanedMiddleware.slice(0, -2);
|
|
78
|
+
if (url.startsWith(prefix)) {
|
|
79
|
+
matchedMiddlewares.push(middleware);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else if (cleanedMiddleware.includes(":")) {
|
|
83
|
+
const middlewareParts = cleanedMiddleware.split("/");
|
|
84
|
+
const urlParts = url.split("/");
|
|
85
|
+
if (middlewareParts.length !== urlParts.length) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
let matches = true;
|
|
89
|
+
for (let i = 0; i < middlewareParts.length; i++) {
|
|
90
|
+
if (middlewareParts[i].startsWith(":")) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
else if (middlewareParts[i] !== urlParts[i]) {
|
|
94
|
+
matches = false;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (matches) {
|
|
99
|
+
matchedMiddlewares.push(middleware);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
if (url === cleanedMiddleware) {
|
|
104
|
+
matchedMiddlewares.push(middleware);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return matchedMiddlewares;
|
|
109
|
+
}
|
package/dist/res.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import http from "http";
|
|
2
|
+
import { CookieOptions } from "./types.js";
|
|
3
|
+
export declare class FFResponse extends http.ServerResponse {
|
|
4
|
+
_ended: boolean;
|
|
5
|
+
json(data: any): void;
|
|
6
|
+
cookie(name: string, value: string, options?: CookieOptions): this;
|
|
7
|
+
status(code: number): this;
|
|
8
|
+
redirect(url: string): this;
|
|
9
|
+
sendFile(filePath: string): this;
|
|
10
|
+
}
|
package/dist/res.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import http from "http";
|
|
2
|
+
import { getContentType } from "./helpers.js";
|
|
3
|
+
import { createReadStream } from "fs";
|
|
4
|
+
export class FFResponse extends http.ServerResponse {
|
|
5
|
+
_ended = false;
|
|
6
|
+
json(data) {
|
|
7
|
+
this.setHeader("Content-Type", "application/json");
|
|
8
|
+
this.end(JSON.stringify(data));
|
|
9
|
+
}
|
|
10
|
+
cookie(name, value, options = {}) {
|
|
11
|
+
let cookie = `${name}=${encodeURIComponent(value)}`;
|
|
12
|
+
if (options.maxAge)
|
|
13
|
+
cookie += `; Max-Age=${options.maxAge}`;
|
|
14
|
+
if (options.path)
|
|
15
|
+
cookie += `; Path=${options.path}`;
|
|
16
|
+
if (options.httpOnly)
|
|
17
|
+
cookie += `; HttpOnly`;
|
|
18
|
+
if (options.secure)
|
|
19
|
+
cookie += `; Secure`;
|
|
20
|
+
if (options.sameSite)
|
|
21
|
+
cookie += `; SameSite=${options.sameSite}`;
|
|
22
|
+
this.setHeader("Set-Cookie", cookie);
|
|
23
|
+
return this;
|
|
24
|
+
}
|
|
25
|
+
status(code) {
|
|
26
|
+
this.statusCode = code;
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
redirect(url) {
|
|
30
|
+
this.statusCode = 302;
|
|
31
|
+
this.setHeader("Location", url);
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
sendFile(filePath) {
|
|
35
|
+
this.setHeader("Content-Type", getContentType(filePath));
|
|
36
|
+
createReadStream(filePath).pipe(this);
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
}
|
package/dist/test.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/test.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import FalconFrame from "./index.js";
|
|
2
|
+
const app = new FalconFrame({
|
|
3
|
+
logLevel: "INFO",
|
|
4
|
+
});
|
|
5
|
+
app.use((req, res, next) => {
|
|
6
|
+
console.log(`[${req.method}] ${req.path}`);
|
|
7
|
+
next();
|
|
8
|
+
});
|
|
9
|
+
app.static("/", "public");
|
|
10
|
+
app.get("/hello", (req, res) => {
|
|
11
|
+
const name = req.query.name || "World";
|
|
12
|
+
res.json({
|
|
13
|
+
message: `Hello, ${name}?`,
|
|
14
|
+
query: req.query,
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
app.get("/hello/*", (req, res) => {
|
|
18
|
+
res.json({
|
|
19
|
+
message: `Hello, ${req.params.name}!`,
|
|
20
|
+
query: req.query,
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
app.get("/greet/:name", (req, res, next) => {
|
|
24
|
+
console.log(req.params);
|
|
25
|
+
next();
|
|
26
|
+
}, (req, res) => {
|
|
27
|
+
res.json({
|
|
28
|
+
message: `Hello, ${req.params.name}!`,
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
app.post("/submit", (req, res, next) => {
|
|
32
|
+
const { validErrors, valid } = req.valid({
|
|
33
|
+
login: "required|string",
|
|
34
|
+
password: "required|string|min:8",
|
|
35
|
+
});
|
|
36
|
+
if (!valid) {
|
|
37
|
+
res.status(400).json({
|
|
38
|
+
status: "error",
|
|
39
|
+
errors: validErrors,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
else
|
|
43
|
+
next();
|
|
44
|
+
}, async (req, res) => {
|
|
45
|
+
console.log("run");
|
|
46
|
+
res.redirect("/hello?name=" + req.body.login);
|
|
47
|
+
return {
|
|
48
|
+
status: "success",
|
|
49
|
+
data: `Hello ${req.body.login}`,
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
app.use((req, res) => {
|
|
53
|
+
res.status(404);
|
|
54
|
+
res.json({
|
|
55
|
+
message: "Not found",
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
app.listen(3000, () => {
|
|
59
|
+
console.log("Server running on http://localhost:3000");
|
|
60
|
+
});
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { FFResponse } from "./res.js";
|
|
2
|
+
import http from "http";
|
|
3
|
+
export type RouteHandler = (req: FFRequest, res: FFResponse, next?: () => void) => void | any;
|
|
4
|
+
export type Method = "get" | "post" | "put" | "delete" | "all";
|
|
5
|
+
export interface Params {
|
|
6
|
+
[key: string]: string;
|
|
7
|
+
}
|
|
8
|
+
export interface Cookies {
|
|
9
|
+
[key: string]: string;
|
|
10
|
+
}
|
|
11
|
+
export interface Query {
|
|
12
|
+
[key: string]: string | string[];
|
|
13
|
+
}
|
|
14
|
+
export interface Body {
|
|
15
|
+
[key: string]: any;
|
|
16
|
+
}
|
|
17
|
+
export declare class FFRequest extends http.IncomingMessage {
|
|
18
|
+
path: string;
|
|
19
|
+
query: Query;
|
|
20
|
+
params: Params;
|
|
21
|
+
cookies: Cookies;
|
|
22
|
+
body: Body;
|
|
23
|
+
valid: (schema: ValidationSchema) => ValidationResult;
|
|
24
|
+
}
|
|
25
|
+
export interface Middleware {
|
|
26
|
+
path: string;
|
|
27
|
+
method: Method;
|
|
28
|
+
middleware: RouteHandler;
|
|
29
|
+
use?: true;
|
|
30
|
+
}
|
|
31
|
+
export interface CookieOptions {
|
|
32
|
+
maxAge?: number;
|
|
33
|
+
path?: string;
|
|
34
|
+
httpOnly?: boolean;
|
|
35
|
+
secure?: boolean;
|
|
36
|
+
sameSite?: "Strict" | "Lax" | "None";
|
|
37
|
+
}
|
|
38
|
+
export interface ValidationSchema {
|
|
39
|
+
[key: string]: string;
|
|
40
|
+
}
|
|
41
|
+
export interface ValidationResult {
|
|
42
|
+
valid: boolean;
|
|
43
|
+
validErrors: {
|
|
44
|
+
[key: string]: string[];
|
|
45
|
+
};
|
|
46
|
+
}
|
package/dist/types.js
ADDED
package/dist/valid.d.ts
ADDED
package/dist/valid.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export function validate(schema, data) {
|
|
2
|
+
const errors = {};
|
|
3
|
+
let isValid = true;
|
|
4
|
+
for (const key in schema) {
|
|
5
|
+
const rules = schema[key].split("|");
|
|
6
|
+
const value = data[key];
|
|
7
|
+
const fieldErrors = [];
|
|
8
|
+
for (const rule of rules) {
|
|
9
|
+
const [ruleName, param] = rule.split(":");
|
|
10
|
+
switch (ruleName) {
|
|
11
|
+
case "required":
|
|
12
|
+
if (!value && value !== 0) {
|
|
13
|
+
fieldErrors.push(`${key} is required`);
|
|
14
|
+
}
|
|
15
|
+
break;
|
|
16
|
+
case "string":
|
|
17
|
+
if (typeof value !== "string") {
|
|
18
|
+
fieldErrors.push(`${key} must be a string`);
|
|
19
|
+
}
|
|
20
|
+
break;
|
|
21
|
+
case "number":
|
|
22
|
+
if (typeof value !== "number") {
|
|
23
|
+
fieldErrors.push(`${key} must be a number`);
|
|
24
|
+
}
|
|
25
|
+
break;
|
|
26
|
+
case "min":
|
|
27
|
+
if (typeof value === "string" && value.length < parseInt(param)) {
|
|
28
|
+
fieldErrors.push(`${key} must be at least ${param} characters long`);
|
|
29
|
+
}
|
|
30
|
+
if (typeof value === "number" && value < parseInt(param)) {
|
|
31
|
+
fieldErrors.push(`${key} must be at least ${param}`);
|
|
32
|
+
}
|
|
33
|
+
break;
|
|
34
|
+
case "max":
|
|
35
|
+
if (typeof value === "string" && value.length > parseInt(param)) {
|
|
36
|
+
fieldErrors.push(`${key} must not exceed ${param} characters`);
|
|
37
|
+
}
|
|
38
|
+
if (typeof value === "number" && value > parseInt(param)) {
|
|
39
|
+
fieldErrors.push(`${key} must not exceed ${param}`);
|
|
40
|
+
}
|
|
41
|
+
break;
|
|
42
|
+
case "email":
|
|
43
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
44
|
+
if (!emailRegex.test(value)) {
|
|
45
|
+
fieldErrors.push(`${key} must be a valid email`);
|
|
46
|
+
}
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (fieldErrors.length > 0) {
|
|
51
|
+
isValid = false;
|
|
52
|
+
errors[key] = fieldErrors;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { valid: isValid, validErrors: errors };
|
|
56
|
+
}
|