@wxn0brp/falcon-frame 0.8.1 → 0.8.3

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 wxn0brP
3
+ Copyright (c) 2026 wxn0brP
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,25 +1,24 @@
1
1
  # FalconFrame
2
2
 
3
- > Lightweight modular TypeScript web framework built for speed and clarity.
3
+ A minimal, modular TypeScript web library.
4
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.
5
+ ## Features
6
6
 
7
- ## ✨ Features
7
+ - Zero-dependency routing engine
8
+ - Middleware chaining
9
+ - Static file serving
10
+ - Request validation via inline schemas
11
+ - OpenAPI document generation
12
+ - Cookie management
13
+ - Debuggable logger
8
14
 
9
- - 🚀 Zero-dependency routing engine
10
- - 🧱 Middleware chaining
11
- - 📁 Static file serving
12
- - 📦 Request validation via inline schemas
13
- - 🍪 Cookie management
14
- - 🔍 Debuggable logger
15
-
16
- ## 📦 Installation
15
+ ## Installation
17
16
 
18
17
  ```bash
19
- yarn add @wxn0brp/falcon-frame
18
+ bun add @wxn0brp/falcon-frame
20
19
  ```
21
20
 
22
- ## 🚦 Usage Example
21
+ ## Usage Example
23
22
 
24
23
  ```ts
25
24
  import FalconFrame, { validateBody } from "@wxn0brp/falcon-frame";
@@ -83,13 +82,13 @@ app.post(
83
82
  // Protected route
84
83
  app.get("/dashboard", requireAuth, (req, res) => {
85
84
  return {
86
- message: `👑 Welcome to the dashboard, ${req.cookies.session}`,
85
+ message: `Welcome to the dashboard, ${req.cookies.session}`,
87
86
  });
88
87
  });
89
88
 
90
89
  app.post("/logout", (req, res) => {
91
90
  res.cookie("session", "", { maxAge: 0 });
92
- res.json({ message: "👋 Logged out" });
91
+ res.json({ message: "Logged out" });
93
92
  });
94
93
 
95
94
  // if no route matches
@@ -101,6 +100,6 @@ app.set404((req, res) => {
101
100
  app.l(3000);
102
101
  ```
103
102
 
104
- ## 📜 License
103
+ ## License
105
104
 
106
- Published under the MIT license
105
+ Published under the MIT license.
package/dist/compress.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { createDeflate, createGzip } from "zlib";
2
2
  export function compression(req, res) {
3
+ if (req._compression)
4
+ return;
5
+ req._compression = true;
3
6
  const encoding = req.headers["accept-encoding"];
4
7
  if (!encoding || typeof encoding !== "string" || res.headersSent) {
5
8
  return;
package/dist/helpers.d.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { Cookies, RouteHandler, StaticServeOptions } from "./types.js";
1
+ import { Cookies } 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, opts: StaticServeOptions): RouteHandler;
5
4
  export declare const ensureLeadingDot: (str: string) => string;
package/dist/helpers.js CHANGED
@@ -1,4 +1,3 @@
1
- import fs from "fs";
2
1
  import path from "path";
3
2
  export function parseCookies(cookieHeader) {
4
3
  const cookies = {};
@@ -33,86 +32,4 @@ export function getContentType(filePath, utf8 = false) {
33
32
  contentType += "; charset=utf-8";
34
33
  return contentType;
35
34
  }
36
- export function handleStaticFiles(dirPath, opts) {
37
- opts = {
38
- utf8: true,
39
- render: true,
40
- renderData: {},
41
- etag: true,
42
- errorIfDirNotFound: true,
43
- ...opts,
44
- };
45
- if (opts.errorIfDirNotFound && !fs.existsSync(dirPath)) {
46
- throw new Error(`Directory ${dirPath} does not exist`);
47
- }
48
- const serveFile = (req, res, filePath, stats) => {
49
- if (opts.render) {
50
- const defaultRenderer = res.FF.getVar("view engine");
51
- const renderStatement = (defaultRenderer && filePath.endsWith(ensureLeadingDot(defaultRenderer))) ||
52
- (!opts.notRenderHtml && filePath.endsWith(".html"));
53
- if (renderStatement) {
54
- const relativePath = filePath.replace(dirPath + "/", "").replace(".html", "");
55
- res.render(filePath, opts.renderData?.[relativePath] || {}, { notUseViews: true });
56
- return true;
57
- }
58
- }
59
- if (opts.etag) {
60
- const etag = `W/"${stats.size}-${stats.mtime.getTime()}"`;
61
- if (req.headers["if-none-match"] === etag) {
62
- res.status(304).end();
63
- return true;
64
- }
65
- res.setHeader("ETag", etag);
66
- }
67
- res.ct(getContentType(filePath, opts.utf8));
68
- fs.createReadStream(filePath).pipe(res);
69
- return true;
70
- };
71
- return (req, res, next) => {
72
- if (req.method.toLowerCase() !== "get")
73
- return next();
74
- const apiPath = req.middleware.path;
75
- const unsafePath = req.path.replace(apiPath, "");
76
- const filePath = path.join(dirPath, path.normalize(unsafePath));
77
- const resolvedPath = path.resolve(filePath);
78
- const resolvedDir = path.resolve(dirPath);
79
- if (!resolvedPath.startsWith(resolvedDir))
80
- return next();
81
- try {
82
- const stats = fs.statSync(filePath);
83
- if (stats.isFile()) {
84
- return serveFile(req, res, filePath, stats);
85
- }
86
- if (stats.isDirectory()) {
87
- const indexPath = path.join(filePath, "index.html");
88
- try {
89
- const indexStats = fs.statSync(indexPath);
90
- if (indexStats.isFile()) {
91
- if (!req.path.endsWith("/")) {
92
- res.redirect(req.path + "/");
93
- return true;
94
- }
95
- return serveFile(req, res, indexPath, indexStats);
96
- }
97
- }
98
- catch (e) {
99
- /* index.html not found, do nothing */
100
- }
101
- }
102
- }
103
- catch (e) {
104
- /* file/dir not found, proceed to check for .html */
105
- }
106
- try {
107
- const htmlPath = filePath + ".html";
108
- const htmlStats = fs.statSync(htmlPath);
109
- if (htmlStats.isFile())
110
- return serveFile(req, res, htmlPath, htmlStats);
111
- }
112
- catch (e) {
113
- /* .html file not found, fall through to next() */
114
- }
115
- next();
116
- };
117
- }
118
35
  export const ensureLeadingDot = (str) => (str.startsWith(".") ? str : "." + str);
package/dist/req.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import FalconFrame from "./index.js";
2
2
  import { FFResponse } from "./res.js";
3
3
  import { FFRequest } from "./types.js";
4
- export declare function handleRequest(req: FFRequest, res: FFResponse, FF: FalconFrame<any>): void;
4
+ export declare function handleRequest(req: FFRequest, res: FFResponse, FF: FalconFrame<any>, reRoute?: boolean): void;
package/dist/req.js CHANGED
@@ -1,17 +1,20 @@
1
+ import { randomUUID } from "crypto";
1
2
  import { URL } from "url";
2
3
  import { parseCookies } from "./helpers.js";
3
4
  import { getMiddlewares, matchMiddleware } from "./middleware.js";
4
5
  import { FFResponse } from "./res.js";
5
6
  import { validate } from "./valid.js";
6
- export function handleRequest(req, res, FF) {
7
- Object.setPrototypeOf(res, FFResponse.prototype);
8
- req.FF = FF;
9
- res.FF = FF;
10
- const originalEnd = res.end;
11
- res.end = function (...any) {
12
- res._ended = true;
13
- return originalEnd.call(res, ...any);
14
- };
7
+ export function handleRequest(req, res, FF, reRoute = false) {
8
+ if (!reRoute) {
9
+ Object.setPrototypeOf(res, FFResponse.prototype);
10
+ req.FF = FF;
11
+ res.FF = FF;
12
+ const originalEnd = res.end;
13
+ res.end = function (...any) {
14
+ res._ended = true;
15
+ return originalEnd.call(res, ...any);
16
+ };
17
+ }
15
18
  const { logger } = FF;
16
19
  try {
17
20
  const [path, params] = (req.url || "").split("?");
@@ -28,6 +31,16 @@ export function handleRequest(req, res, FF) {
28
31
  req.cookies = parseCookies(req.headers.cookie || "");
29
32
  req.params = {};
30
33
  req.valid = (schema) => validate(schema, req.body);
34
+ req.reRoute = (fn) => {
35
+ fn(req);
36
+ handleRequest(req, res, FF, true);
37
+ };
38
+ if (FF.opts.xRequestId !== "disable") {
39
+ if (req.headers["x-request-id"])
40
+ res.setHeader("x-request-id", req.headers["x-request-id"]);
41
+ else if (FF.opts.xRequestId === "auto")
42
+ res.setHeader("x-request-id", randomUUID());
43
+ }
31
44
  logger.info(`Incoming request: ${req.method} ${req.url}`);
32
45
  const middlewaresPath = req.path + "/";
33
46
  const middlewares = getMiddlewares(FF.middlewares, middlewaresPath.replace(/\/+/g, "/"));
@@ -98,6 +111,11 @@ export function handleRequest(req, res, FF) {
98
111
  next();
99
112
  return;
100
113
  }
114
+ if (reRoute) {
115
+ logger.debug("Re-route. Executing middlewares");
116
+ next();
117
+ return;
118
+ }
101
119
  req.body = {};
102
120
  let bodyParserIndex = 0;
103
121
  function nextBodyParser() {
package/dist/res.d.ts CHANGED
@@ -38,13 +38,14 @@ export declare class FFResponse extends http.ServerResponse {
38
38
  */
39
39
  status(code: number): this;
40
40
  /**
41
- * Set status code to 302 and set location header to the provided url.
41
+ * Set status code to 303 and set location header to the provided url.
42
42
  * If end is true, ends the response.
43
43
  * @param url The url to redirect to
44
+ * @param status Status code to set
44
45
  * @param end Whether to end the response after setting the headers
45
46
  * @returns The response object
46
47
  */
47
- redirect(url: string, end?: boolean): this;
48
+ redirect(url: string, status?: number, end?: boolean): this;
48
49
  /**
49
50
  * Sends a file as the response.
50
51
  * Sets the "Content-Type" header based on the provided contentType or the file's extension.
package/dist/res.js CHANGED
@@ -67,14 +67,15 @@ export class FFResponse extends http.ServerResponse {
67
67
  return this;
68
68
  }
69
69
  /**
70
- * Set status code to 302 and set location header to the provided url.
70
+ * Set status code to 303 and set location header to the provided url.
71
71
  * If end is true, ends the response.
72
72
  * @param url The url to redirect to
73
+ * @param status Status code to set
73
74
  * @param end Whether to end the response after setting the headers
74
75
  * @returns The response object
75
76
  */
76
- redirect(url, end = true) {
77
- this.statusCode = 302;
77
+ redirect(url, status = 303, end = true) {
78
+ this.statusCode = status;
78
79
  this.setHeader("Location", url);
79
80
  if (end)
80
81
  this.end();
package/dist/router.js CHANGED
@@ -1,5 +1,5 @@
1
- import { handleStaticFiles } from "./helpers.js";
2
1
  import { SSEManager } from "./sse.js";
2
+ import { handleStaticFiles } from "./static.js";
3
3
  export class Router {
4
4
  middlewares = [];
5
5
  addRoute(method, path, ...handlers) {
package/dist/sse.d.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  import { FFResponse } from "./res.js";
2
2
  import type { FFRequest, RouteHandler } from "./types.js";
3
3
  export declare class SSEManager {
4
- private clients;
4
+ _clients: Map<string, {
5
+ req: FFRequest;
6
+ res: FFResponse;
7
+ }>;
5
8
  getMiddleware(lastHandler?: RouteHandler): RouteHandler;
6
9
  getClients(): Map<string, {
7
10
  req: FFRequest;
package/dist/sse.js CHANGED
@@ -1,37 +1,37 @@
1
1
  import { randomUUID } from "crypto";
2
2
  export class SSEManager {
3
- clients = new Map();
3
+ _clients = new Map();
4
4
  getMiddleware(lastHandler) {
5
5
  return (req, res, next) => {
6
6
  res.sseInit();
7
7
  const id = randomUUID();
8
- this.clients.set(id, { req, res });
8
+ this._clients.set(id, { req, res });
9
9
  req.on("close", () => {
10
- this.clients.delete(id);
10
+ this._clients.delete(id);
11
11
  });
12
12
  req.sseId = id;
13
13
  lastHandler?.(req, res, next);
14
14
  };
15
15
  }
16
16
  getClients() {
17
- return this.clients;
17
+ return this._clients;
18
18
  }
19
19
  sendAll(data) {
20
- for (const { res } of this.clients.values()) {
20
+ for (const { res } of this._clients.values()) {
21
21
  res.sseSend(data);
22
22
  }
23
23
  }
24
24
  sendTo(id, data) {
25
- const client = this.clients.get(id);
25
+ const client = this._clients.get(id);
26
26
  if (client) {
27
27
  client.res.sseSend(data);
28
28
  }
29
29
  }
30
30
  disconnect(id) {
31
- const client = this.clients.get(id);
31
+ const client = this._clients.get(id);
32
32
  if (client) {
33
33
  client.res.end();
34
- this.clients.delete(id);
34
+ this._clients.delete(id);
35
35
  }
36
36
  }
37
37
  }
@@ -0,0 +1,2 @@
1
+ import { RouteHandler, StaticServeOptions } from "./types.js";
2
+ export declare function handleStaticFiles(dirPath: string, opts: StaticServeOptions): RouteHandler;
package/dist/static.js ADDED
@@ -0,0 +1,85 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { ensureLeadingDot, getContentType } from "./helpers.js";
4
+ export function handleStaticFiles(dirPath, opts) {
5
+ opts = {
6
+ utf8: true,
7
+ render: true,
8
+ renderData: {},
9
+ etag: true,
10
+ errorIfDirNotFound: true,
11
+ ...opts,
12
+ };
13
+ if (opts.errorIfDirNotFound && !fs.existsSync(dirPath)) {
14
+ throw new Error(`Directory ${dirPath} does not exist`);
15
+ }
16
+ const serveFile = (req, res, filePath, stats) => {
17
+ if (opts.render) {
18
+ const defaultRenderer = res.FF.getVar("view engine");
19
+ const renderStatement = (defaultRenderer && filePath.endsWith(ensureLeadingDot(defaultRenderer))) ||
20
+ (!opts.notRenderHtml && filePath.endsWith(".html"));
21
+ if (renderStatement) {
22
+ const relativePath = filePath.replace(dirPath + "/", "").replace(".html", "");
23
+ res.render(filePath, opts.renderData?.[relativePath] || {}, { notUseViews: true });
24
+ return true;
25
+ }
26
+ }
27
+ if (opts.etag) {
28
+ const etag = `W/"${stats.size}-${stats.mtime.getTime()}"`;
29
+ if (req.headers["if-none-match"] === etag) {
30
+ res.status(304).end();
31
+ return true;
32
+ }
33
+ res.setHeader("ETag", etag);
34
+ }
35
+ res.ct(getContentType(filePath, opts.utf8));
36
+ fs.createReadStream(filePath).pipe(res);
37
+ return true;
38
+ };
39
+ return (req, res, next) => {
40
+ if (req.method.toLowerCase() !== "get")
41
+ return next();
42
+ const apiPath = req.middleware.path;
43
+ const unsafePath = req.path.replace(apiPath, "");
44
+ const filePath = path.join(dirPath, path.normalize(unsafePath));
45
+ const resolvedPath = path.resolve(filePath);
46
+ const resolvedDir = path.resolve(dirPath);
47
+ if (!resolvedPath.startsWith(resolvedDir))
48
+ return next();
49
+ try {
50
+ const stats = fs.statSync(filePath);
51
+ if (stats.isFile()) {
52
+ return serveFile(req, res, filePath, stats);
53
+ }
54
+ if (stats.isDirectory()) {
55
+ const indexPath = path.join(filePath, "index.html");
56
+ try {
57
+ const indexStats = fs.statSync(indexPath);
58
+ if (indexStats.isFile()) {
59
+ if (!req.path.endsWith("/")) {
60
+ res.redirect(req.path + "/");
61
+ return true;
62
+ }
63
+ return serveFile(req, res, indexPath, indexStats);
64
+ }
65
+ }
66
+ catch (e) {
67
+ /* index.html not found, do nothing */
68
+ }
69
+ }
70
+ }
71
+ catch (e) {
72
+ /* file/dir not found, proceed to check for .html */
73
+ }
74
+ try {
75
+ const htmlPath = filePath + ".html";
76
+ const htmlStats = fs.statSync(htmlPath);
77
+ if (htmlStats.isFile())
78
+ return serveFile(req, res, htmlPath, htmlStats);
79
+ }
80
+ catch (e) {
81
+ /* .html file not found, fall through to next() */
82
+ }
83
+ next();
84
+ };
85
+ }
package/dist/types.d.ts CHANGED
@@ -31,6 +31,8 @@ export declare class FFRequest extends http.IncomingMessage {
31
31
  valid: (schema: ValidationSchema) => ValidationResult;
32
32
  middleware: Middleware;
33
33
  sseId?: string;
34
+ reRoute: (fn: (req: FFRequest) => void) => void;
35
+ _compression?: true;
34
36
  }
35
37
  export interface Middleware {
36
38
  path: string;
@@ -80,6 +82,7 @@ export interface FFOpts {
80
82
  bodyLimit?: string;
81
83
  disableJsonParser?: boolean;
82
84
  disableUrlencodedParser?: boolean;
85
+ xRequestId?: "auto" | "disable" | "manual";
83
86
  }
84
87
  export type FFVars = {
85
88
  "render data": Record<string, any>;
package/dist/types.js CHANGED
@@ -9,4 +9,6 @@ export class FFRequest extends http.IncomingMessage {
9
9
  valid;
10
10
  middleware;
11
11
  sseId;
12
+ reRoute;
13
+ _compression;
12
14
  }
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@wxn0brp/falcon-frame",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "author": "wxn0brP",
7
7
  "license": "MIT",
8
8
  "type": "module",
9
- "description": "Lightweight modular TypeScript web framework",
9
+ "description": "Lightweight modular TypeScript web library",
10
10
  "homepage": "https://github.com/wxn0brP/FalconFrame",
11
11
  "repository": {
12
12
  "type": "git",
@@ -15,7 +15,6 @@
15
15
  "keywords": [
16
16
  "http",
17
17
  "web",
18
- "framework",
19
18
  "typescript",
20
19
  "modular",
21
20
  "lightweight",
@@ -26,10 +25,10 @@
26
25
  "devDependencies": {
27
26
  "@types/node": "*",
28
27
  "tsc-alias": "*",
29
- "typescript": "^6"
28
+ "typescript": "^7"
30
29
  },
31
30
  "dependencies": {
32
- "@wxn0brp/lucerna-log": "^0.2.2"
31
+ "@wxn0brp/lucerna-log": "^0.2.4"
33
32
  },
34
33
  "files": [
35
34
  "dist"