jetpath
Version:
Jetpath - A fast, seamless and minimalist framework for Node, Deno and Bun.js. Embrace the speed and elegance of the next-gen server-side experience.
1,250 lines (1,249 loc) • 47.6 kB
JavaScript
// compatible node imports
import { opendir, readdir, readFile, writeFile } from "node:fs/promises";
import { dirname, join, resolve, sep } from "node:path";
import { cwd } from "node:process";
import { createServer } from "node:http";
// type imports
import {} from "node:http";
import { ArraySchema, BooleanSchema, Context, DateSchema, FileSchema, JetPlugin, JetSocket, LOG, NumberSchema, ObjectSchema, SchemaBuilder, SchemaCompiler, StringSchema, Trie, } from "./classes.js";
import { networkInterfaces } from "node:os";
import { execFile } from "node:child_process";
import { mkdirSync } from "node:fs";
/**
* an inbuilt CORS post middleware
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer/Planned_changes
* - {Boolean} privateNetworkAccess handle `Access-Control-Request-Private-Network` request by return `Access-Control-Allow-Private-Network`, default to false
* @see https://wicg.github.io/private-network-access/
*/
const optionsCtx = {
payload: undefined,
_2: {
"Vary": "Origin",
"Connection": "keep-alive",
},
_6: false,
code: 204,
set(field, value) {
if (field && value) {
this._2[field] = value;
}
},
request: { method: "OPTIONS" },
};
const cachedCorsHeaders = {
"Vary": "Origin",
"Connection": "keep-alive",
};
export function corsMiddleware(options) {
//
options.keepHeadersOnError = options.keepHeadersOnError === undefined ||
!!options.keepHeadersOnError;
//? pre populate context for Preflight Request
if (options.maxAge) {
optionsCtx.set("Access-Control-Max-Age", options.maxAge);
}
if (!options.privateNetworkAccess) {
if (options.allowMethods) {
optionsCtx.set("Access-Control-Allow-Methods", options.allowMethods.join(","));
}
if (options.secureContext) {
optionsCtx.set("Cross-Origin-Opener-Policy", options.secureContext["Cross-Origin-Embedder-Policy"] || "unsafe-none");
optionsCtx.set("Cross-Origin-Embedder-Policy", options.secureContext["Cross-Origin-Embedder-Policy"] || "unsafe-none");
}
if (options.allowHeaders) {
optionsCtx.set("Access-Control-Allow-Headers", options.allowHeaders.join(","));
}
}
optionsCtx.set("Vary", "Origin");
if (options.credentials === true) {
optionsCtx.set("Access-Control-Allow-Credentials", "true");
}
if (Array.isArray(options.origin)) {
optionsCtx.set("Access-Control-Allow-Origin", options.origin.join(","));
}
// ? Pre-popular normal response headers.
//? Add Vary header to indicate response varies based on the Origin header
cachedCorsHeaders["Vary"] = "Origin";
if (options.credentials === true) {
cachedCorsHeaders["Access-Control-Allow-Credentials"] = "true";
}
if (Array.isArray(options.origin)) {
cachedCorsHeaders["Access-Control-Allow-Origin"] = options.origin.join(",");
}
if (options.secureContext) {
cachedCorsHeaders["Cross-Origin-Opener-Policy"] = options.secureContext["Cross-Origin-Embedder-Policy"];
cachedCorsHeaders["Cross-Origin-Embedder-Policy"] = options.secureContext["Cross-Origin-Embedder-Policy"];
}
}
export const JetSocketInstance = new JetSocket();
export let _JetPath_paths = {
GET: {},
POST: {},
HEAD: {},
PUT: {},
PATCH: {},
DELETE: {},
OPTIONS: {},
CONNECT: {},
TRACE: {},
};
export let _JetPath_paths_trie = {
GET: new Trie("GET"),
POST: new Trie("POST"),
HEAD: new Trie("HEAD"),
PUT: new Trie("PUT"),
PATCH: new Trie("PATCH"),
DELETE: new Trie("DELETE"),
OPTIONS: new Trie("OPTIONS"),
TRACE: new Trie("TRACE"),
CONNECT: new Trie("CONNECT"),
};
export const _jet_middleware = {};
export const ctxPool = [];
export let runtime = {
bun: false,
deno: false,
node: false,
edge: false,
};
const plugins = {};
export function abstractPluginCreator(ctx) {
const abstractPlugin = {};
for (const key in plugins) {
abstractPlugin[key] = plugins[key].bind(ctx);
}
return abstractPlugin;
}
const ae = (cb) => {
try {
cb();
return true;
}
catch (error) {
return false;
}
};
(() => {
const bun = ae(() => Bun);
// @ts-expect-error
const deno = ae(() => Deno);
runtime = { bun, deno, node: !bun && !deno, edge: false };
})();
// ? isNode
export const isNode = runtime["node"];
// ? server
export const server = (plugs, options) => {
let server;
let server_else;
if (runtime["node"]) {
server = createServer({
keepAliveTimeout: options.keepAliveTimeout || 120_000,
keepAlive: true,
}, (x, y) => {
Jetpath(x, y);
});
}
if (runtime["deno"]) {
server = {
listen(port) {
// @ts-expect-error
server_else = Deno.serve({ port: port }, Jetpath);
},
edge: false,
};
}
if (runtime["bun"]) {
if (options.upgrade && options.upgrade === true) {
server = {
listen(port) {
server_else = Bun.serve({
port,
// @ts-expect-error
fetch: Jetpath,
websocket: {
message(...p) {
p[1] = {
// @ts-expect-error
data: p[1],
};
JetSocketInstance.__binder("message", p);
},
close(...p) {
JetSocketInstance.__binder("close", p);
},
drain(...p) {
JetSocketInstance.__binder("drain", p);
},
open(...p) {
JetSocketInstance.__binder("open", p);
},
},
});
},
edge: false,
};
}
else {
server = {
listen(port) {
server_else = Bun.serve({
port,
// @ts-expect-error
fetch: Jetpath,
});
},
edge: false,
};
}
}
// ? yes a plugin can bring it's own server
//? compile plugins
for (let i = 0; i < plugs.length; i++) {
const decs = plugs[i].setup({
server: !runtime["node"] ? server_else : server,
runtime: runtime,
routesObject: _JetPath_paths,
JetPath_app: Jetpath,
});
Object.assign(plugins, decs);
}
const edgePlugin = plugs.find((plug) => plug.plugin.server);
// ? adding ctx plugin bindings
if (edgePlugin) {
const edge_server = edgePlugin.plugin.server({
server: !runtime["node"] ? server_else : server,
runtime: runtime,
routesObject: _JetPath_paths,
handler: Jetpath,
router: _JetPath_paths,
});
if (edge_server !== undefined) {
server = edge_server;
server.edge = true;
}
}
return server;
};
export const getCtx = (req, res, path, route, params) => {
if (ctxPool.length) {
const ctx = ctxPool.shift();
// ? reset the COntext to default state
ctx.state["__state__"] = true;
ctx.request = req;
ctx.res = res;
ctx.method = req.method;
ctx.params = params;
ctx.$_internal_query = undefined;
ctx.$_internal_body = undefined; // ? very important.
ctx.path = path;
//? load
ctx.payload = undefined;
// ? header of response
ctx._2 = cachedCorsHeaders;
// //? stream
ctx._3 = undefined;
//? the route handler
ctx.handler = route;
//? custom response
ctx._6 = false;
// ? code
ctx.code = 200;
return ctx;
}
const ctx = new Context();
// ? add middlewares to the plugins object
ctx.request = req;
ctx.res = res;
ctx._2 = cachedCorsHeaders;
ctx.method = req.method;
ctx.params = params;
ctx.path = path;
ctx.handler = route;
return ctx;
};
let makeRes;
const makeResBunAndDeno = (_res, ctx) => {
// ? prepare response
// redirect
// if (ctx?.code === 301 && ctx._2?.["Location"]) {
// ctxPool.push(ctx);
// // @ts-ignore
// return Response.redirect(ctx._2?.["Location"]);
// }
// ? streaming with ctx.sendStream
if (ctx?._3) {
// handle deno promise.
// @ts-expect-error
if (runtime["deno"] && ctx._3.then) {
ctxPool.push(ctx);
// @ts-expect-error
return ctx._3.then((stream) => {
return new Response(stream?.readable, {
status: ctx.code,
headers: ctx?._2,
});
});
}
ctxPool.push(ctx);
return new Response(ctx?._3, {
status: ctx.code,
headers: ctx?._2,
});
}
if (ctx._6 !== false) {
ctxPool.push(ctx);
return ctx?._6;
}
// normal response
ctx.__jet_pool && ctxPool.push(ctx);
return new Response(ctx?.payload, {
status: ctx.code,
headers: ctx?._2,
});
};
const makeResNode = (res, ctx) => {
// ? prepare response
if (ctx?._3) {
res.writeHead(ctx?.code, ctx?._2);
ctx?._3.on("error", (_err) => {
res.statusCode;
res.end("File not found");
});
ctx._3.pipe(res);
ctxPool.push(ctx);
return undefined;
}
res.writeHead(ctx.code, ctx?._2 || { "Content-Type": "text/plain" });
res.end(ctx?.payload);
ctx.__jet_pool && ctxPool.push(ctx);
return undefined;
};
if (isNode) {
makeRes = makeResNode;
}
else {
makeRes = makeResBunAndDeno;
}
const Jetpath = async (req, res) => {
if (req.method === "OPTIONS") {
optionsCtx.code = 200;
return makeRes(res, optionsCtx);
}
const ctx = _JetPath_paths_trie[req.method]?.get_responder(req, res);
let returned;
if (ctx) {
const r = ctx.handler;
try {
//? pre-request middlewares here
returned = r.jet_middleware?.length
? await Promise.all(r.jet_middleware.map((m) => m(ctx)))
: undefined;
//? check if the payload is already set by middleware chain;
if (ctx.payload)
return makeRes(res, ctx);
//? route handler call
await r(ctx);
//? post-request middlewares here
returned && await Promise.all(returned.map((m) => m?.(ctx, null)));
return makeRes(res, ctx);
}
catch (error) {
try {
//? report error to error middleware
if (returned) {
await Promise.all(returned.map((m) => m?.(ctx, error)));
}
else {
console.log(error);
}
}
finally {
if (!returned && ctx.code < 400) {
ctx.code = 500;
}
return makeRes(res, ctx);
}
}
}
const ctx404 = optionsCtx;
ctx404.code = 404;
return makeRes(res, ctx404);
};
const handlersPath = (path) => {
let [method, ...segments] = path.split("_");
let route = "/" + segments.join("/");
route = route
.replace(/\$0/g, "/*") // Convert wildcard
.replace(/\$/g, "/:") // Convert params
.replaceAll(/\/\//g, "/"); // change normalize akk extra /(s) to just /
return /^(GET|POST|PUT|PATCH|DELETE|OPTIONS|MIDDLEWARE|HEAD|CONNECT|TRACE)$/
.test(method)
? [method, route]
: undefined;
};
const getModule = async (src, name) => {
try {
const mod = await import(resolve(src + "/" + name));
return mod;
}
catch (error) {
LOG.log("Error at " + src + "/" + name + " loading failed!", "info");
LOG.log(String(error), "error");
return String(error);
}
};
export async function getHandlers(source, print, errorsCount = undefined, again = false) {
const curr_d = cwd();
let error_source = source;
source = source || "";
if (!again) {
source = resolve(join(curr_d, source));
if (!source.includes(curr_d)) {
LOG.log('source: "' + error_source + '" is invalid', "warn");
LOG.log("Jetpath source must be within the project directory", "error");
process.exit(1);
}
}
else {
source = resolve(curr_d, source);
}
const dir = await opendir(source);
for await (const dirent of dir) {
if (dirent.isFile() &&
(dirent.name.endsWith(".jet.js") || dirent.name.endsWith(".jet.ts"))) {
if (print) {
LOG.log("Loading " + source.replace(curr_d + "/", "") + sep +
dirent.name, "info");
}
try {
const module = await getModule(source, dirent.name);
if (typeof module !== "string") {
for (const p in module) {
const params = handlersPath(p);
if (params) {
if (p.startsWith("MIDDLEWARE")) {
_jet_middleware[params[1]] = module[p];
}
else {
// ! HTTP handler
if (typeof params !== "string") {
// ? set the method
module[p].method = params[0];
// ? set the path
module[p].path = params[1];
_JetPath_paths[params[0]][params[1]] = module[p];
_JetPath_paths_trie[params[0]].insert(params[1], module[p]);
}
}
}
}
}
else {
// record errors
if (!errorsCount) {
errorsCount = [];
}
errorsCount.push({
file: dirent.path + "/" + dirent.name,
error: module,
});
}
}
catch (error) {
if (!errorsCount) {
errorsCount = [];
}
errorsCount.push({
file: dirent.path + "/" + dirent.name,
error: String(error),
});
}
}
if (dirent.isDirectory() &&
dirent.name !== "node_modules" &&
dirent.name !== ".git") {
errorsCount = await getHandlers(source + "/" + dirent.name, print, errorsCount, true);
}
}
return errorsCount;
}
export function validator(schema, data) {
if (!schema || typeof data !== "object") {
throw new Error("Invalid schema or data");
}
const errors = [];
const out = {};
for (const [key, defs] of Object.entries(schema)) {
let { RegExp, arrayType, err, objectSchema, required, type, validator: validate, } = defs;
const value = data[key];
// Required check
if (required && value == null) {
errors.push(`${key} is required`);
continue;
}
// Skip if optional and undefined
if (!required && value == null) {
continue;
}
// Type validation
if (type) {
if (type === "array") {
if (!Array.isArray(value)) {
errors.push(`${key} must be an array`);
continue;
}
if (arrayType === "object" && objectSchema) {
try {
const validatedArray = value.map((item) => validator(objectSchema, item));
out[key] = validatedArray;
continue;
}
catch (e) {
errors.push(`${key}: ${String(e)}`);
continue;
}
}
else if (arrayType &&
!value.every((item) => typeof item === arrayType)) {
errors.push(`${key} must be an array of ${arrayType}`);
continue;
}
}
else if (type === "object") {
if (typeof value !== "object" || Array.isArray(value)) {
errors.push(`${key} must be an object`);
continue;
}
// Handle objectSchema validation
if (objectSchema) {
try {
out[key] = validator(objectSchema, value);
continue;
}
catch (e) {
errors.push(`${key}: ${String(e)}`);
continue;
}
}
}
else {
if (typeof value !== type) {
if (type === "file" && typeof value === "object") {
out[key] = value;
continue;
}
errors.push(`${key} must be of type ${type}`);
continue;
}
}
}
// Regex validation
if (RegExp && !RegExp.test(value)) {
errors.push(err || `${key} is incorrect`);
continue;
}
// Custom validator
if (validate) {
const result = validate(value);
if (result !== true) {
errors.push(typeof result === "string"
? result
: err || `${key} validation failed`);
continue;
}
}
out[key] = value;
}
if (errors.length > 0) {
throw new Error(errors.join(", "));
}
return out;
}
export const compileUI = (UI, options, api) => {
// ? global headers
const globalHeaders = JSON.stringify(options?.globalHeaders || {
"Authorization": "Bearer <jwt token>",
});
return UI.replace('"{ JETPATH }"', `\`${api}\``)
.replaceAll('"{ JETENVIRONMENTS }"', JSON.stringify(options?.apiDoc?.environments))
.replaceAll('"{ JETPATHGH }"', `${JSON.stringify(globalHeaders)}`)
.replaceAll("{NAME}", options?.apiDoc?.name || "Jetpath API Doc")
.replaceAll("JETPATHCOLOR", options?.apiDoc?.color || "#4285f4")
.replaceAll("{LOGO}", options?.apiDoc?.logo ||
"https://raw.githubusercontent.com/codedynasty-dev/jetpath/main/icon-transparent.png")
.replaceAll("{INFO}", options?.apiDoc?.info?.replaceAll("\n", "<br>") ||
"This is a Jetpath api preview.");
};
export const compileAPI = (options) => {
let handlersCount = 0;
let compiledAPIArray = [];
let compiledRoutes = [];
// ? global headers
const globalHeaders = options?.globalHeaders || {};
// ? loop through apis
for (const method in _JetPath_paths) {
// ? get all api paths from router for each method;
const routesOfMethod = (Object.keys(_JetPath_paths[method]).map((value) => _JetPath_paths[method][value])).filter((value) => value.length > 0);
if (routesOfMethod && Object.keys(routesOfMethod).length) {
for (const route of routesOfMethod) {
// ? Retrieve api handler
const validator = route;
// ? Retrieve api body definitions
const body = validator.body;
// ? Retrieve api headers definitions
const initialHeader = {};
Object.assign(initialHeader, validator?.headers || {}, globalHeaders);
const headers = [];
// ? parse headers
for (const name in initialHeader) {
headers.push(name + ":" + initialHeader[name]);
}
// ? parse body
let bodyData = undefined;
if (body) {
bodyData = {};
const processSchema = (schema, target) => {
for (const key in schema) {
const field = schema[key];
if (field.type === "object" && field.objectSchema) {
target[key] = {};
processSchema(field.objectSchema, target[key]);
}
else if (field.type === "array") {
if (field.arrayType === "object" && field.objectSchema) {
target[key] = [{}];
processSchema(field.objectSchema, target[key][0]);
}
else {
target[key] = [
field.arrayType + ":" + (field.arrayDefaultValue || ""),
];
}
}
else {
target[key] = field?.inputType + ":" +
(field?.inputDefaultValue || "");
}
}
};
processSchema(body, bodyData);
}
// ? combine api infos into .http format
const api = `\n
${method} ${options?.apiDoc?.display === "UI"
? "[--host--]"
: "http://localhost:" + (options?.port || 8080)}${route.path} HTTP/1.1
${headers.length ? headers.join("\n") : ""}\n
${(body && method !== "GET" ? method : "")
? JSON.stringify(bodyData)
: ""}\n\n${validator?.["title"]
? "#-JET-TITLE " + validator?.["title"].replaceAll("\n", "\n# ") +
"#-JET-TITLE"
: ""}\n${validator?.["description"]
? "#-JET-DESCRIPTION\n# " +
validator?.["description"].replaceAll("\n", "\n# ") +
"\n#-JET-DESCRIPTION"
: ""}\n
### break ###`;
// ? combine api(s)
const low = sorted_insert(compiledRoutes, route.path);
compiledRoutes.splice(low, 0, route.path);
compiledAPIArray.splice(low, 0, api);
// ? increment handler count
handlersCount += 1;
}
}
}
// sort and join here
const compileAPIString = compiledAPIArray.join("");
return [handlersCount, compileAPIString];
};
const sorted_insert = (paths, path) => {
let low = 0;
let high = paths.length - 1;
for (; low <= high;) {
const mid = Math.floor((low + high) / 2);
const current = paths[mid];
if (current < path) {
low = mid + 1;
}
else {
high = mid - 1;
}
}
return low;
};
/**
* Assigns middleware functions to routes while ensuring that each route gets exactly one middleware function.
* A middleware function can be shared across multiple routes.
*
* @param _JetPath_paths - An object mapping HTTP methods to route-handler maps.
* @param _jet_middleware - An object mapping route paths to an array of middleware functions.
*/
export function assignMiddleware(_JetPath_paths, _jet_middleware) {
// Iterate over each HTTP method's routes.
for (const method in _JetPath_paths) {
const routes = (Object.keys(_JetPath_paths[method]).map((value) => _JetPath_paths[method][value])).filter((value) => value.length > 0);
for (const route of routes) {
if (!Array.isArray(route.jet_middleware)) {
route.jet_middleware = [];
}
// If middleware is defined for the route, ensure it has exactly one middleware function.
for (const key in _jet_middleware) {
if (route.path.startsWith(key)) {
const middleware = _jet_middleware[key];
// Assign the middleware function to the route handler.
route.jet_middleware.push(middleware);
}
}
}
}
}
export function parseFormData(rawBody, contentType, options = {}) {
const { maxBodySize } = options;
if (maxBodySize && rawBody.byteLength > maxBodySize) {
throw new Error(`Body exceeds max size: ${rawBody.byteLength} > ${maxBodySize}`);
}
const boundaryMatch = contentType.match(/boundary="?([^";]+)"?/i);
if (!boundaryMatch)
throw new Error("Invalid multipart boundary");
const boundary = `--${boundaryMatch[1]}`;
const boundaryBytes = new TextEncoder().encode(boundary);
const decoder = new TextDecoder("utf-8");
const fields = {};
const files = {};
const parts = splitBuffer(rawBody, boundaryBytes).slice(1, -1); // remove preamble and epilogue
for (const part of parts) {
const headerEndIndex = indexOfDoubleCRLF(part);
if (headerEndIndex === -1)
continue;
const headerBytes = part.slice(0, headerEndIndex);
let body = part.slice(headerEndIndex + 4); // Skip \r\n\r\n
// 2) Strip leading CRLF
if (body[0] === 13 && body[1] === 10) {
body = body.slice(2);
}
// 3) Strip trailing CRLF
if (body[body.length - 2] === 13 &&
body[body.length - 1] === 10) {
body = body.slice(0, body.length - 2);
}
const headerText = decoder.decode(headerBytes);
const headers = parseHeaders(headerText);
const disposition = headers["content-disposition"];
if (!disposition)
continue;
const nameMatch = disposition.match(/name="([^"]+)"/);
if (!nameMatch)
continue;
const fieldName = nameMatch[1];
const fileNameMatch = disposition.match(/filename="([^"]*)"/);
const fileName = fileNameMatch?.[1] || null;
if (fileName) {
const mimeType = headers["content-type"] || "application/octet-stream";
files[fieldName] = {
fileName,
content: body,
mimeType,
size: body.length,
};
}
else {
const value = decoder.decode(body);
if (fieldName in fields) {
const existing = fields[fieldName];
fields[fieldName] = Array.isArray(existing)
? [...existing, value]
: [existing, value];
}
else {
try {
fields[fieldName] = JSON.parse(value.toString());
}
catch (error) {
fields[fieldName] = value;
}
}
}
}
return { ...fields, ...files };
}
function parseHeaders(headerText) {
const headers = {};
const lines = headerText.split(/\r\n/);
for (const line of lines) {
const idx = line.indexOf(":");
if (idx === -1)
continue;
const key = line.slice(0, idx).trim().toLowerCase();
const val = line.slice(idx + 1).trim();
headers[key] = val;
}
return headers;
}
function indexOfDoubleCRLF(buffer) {
for (let i = 0; i < buffer.length - 3; i++) {
if (buffer[i] === 13 &&
buffer[i + 1] === 10 &&
buffer[i + 2] === 13 &&
buffer[i + 3] === 10) {
return i;
}
}
return -1;
}
function splitBuffer(buffer, delimiter) {
const parts = [];
let start = 0;
while (start < buffer.length) {
const idx = indexOf(buffer, delimiter, start);
if (idx === -1)
break;
parts.push(buffer.slice(start, idx));
start = idx + delimiter.length;
}
if (start <= buffer.length) {
parts.push(buffer.slice(start));
}
return parts;
}
function indexOf(buffer, search, from = 0) {
outer: for (let i = from; i <= buffer.length - search.length; i++) {
for (let j = 0; j < search.length; j++) {
if (buffer[i + j] !== search[j])
continue outer;
}
return i;
}
return -1;
}
export function parseUrlEncoded(bodyText) {
const params = new URLSearchParams(bodyText);
const result = {};
for (const [key, value] of params.entries()) {
if (result.hasOwnProperty(key)) {
if (Array.isArray(result[key])) {
result[key].push(value);
}
else {
result[key] = [result[key], value];
}
}
else {
result[key] = value;
}
}
return result;
}
/**
* Helper for Node.js: Reads the IncomingMessage stream, collecting chunks and checking size.
*/
function collectRequestBody(req, maxBodySize) {
return new Promise((resolve, reject) => {
const chunks = [];
let size = 0;
req.on("data", (chunk) => {
size += chunk.length;
if (maxBodySize && size > maxBodySize) {
reject(new Error("Payload Too Large"));
req.destroy();
return;
}
chunks.push(chunk);
});
req.on("end", () => {
resolve(new Uint8Array(Buffer.concat(chunks)));
});
req.on("error", (err) => reject(err));
});
}
/**
* Reads the request/stream and returns a Promise that resolves to the parsed body.
*/
export async function parseRequest(req, options = {}) {
const { maxBodySize = 5 * 1024 * 1024 } = options;
let contentType = options.contentType || "";
let rawBody;
if (typeof req.arrayBuffer === "function") {
if (!contentType && req.headers && typeof req.headers.get === "function") {
contentType = req.headers.get("content-type") || "";
}
const arrayBuffer = await req.arrayBuffer();
rawBody = new Uint8Array(arrayBuffer);
if (rawBody.byteLength > maxBodySize) {
throw new Error("Payload Too Large");
}
}
else if (typeof req.on === "function") {
if (!contentType && req.headers) {
contentType = req.headers["content-type"] || "";
}
rawBody = await collectRequestBody(req, maxBodySize);
}
else {
throw new Error("Unsupported request object type");
}
const ct = contentType.toLowerCase();
const decoder = new TextDecoder("utf-8");
let bodyText;
if (ct.includes("application/json")) {
bodyText = decoder.decode(rawBody);
return JSON.parse(bodyText);
}
else if (ct.includes("application/x-www-form-urlencoded")) {
bodyText = decoder.decode(rawBody);
return parseUrlEncoded(bodyText);
}
else if (ct.includes("multipart/form-data")) {
return parseFormData(rawBody, contentType, { maxBodySize });
}
else {
bodyText = decoder.decode(rawBody);
return { parsed: bodyText };
}
}
export const v = {
string: (options) => new StringSchema(options),
number: (options) => new NumberSchema(options),
boolean: () => new BooleanSchema(),
array: (itemType) => new ArraySchema(itemType),
object: (shape) => new ObjectSchema(shape),
date: () => new DateSchema(),
file: (options) => new FileSchema(options),
};
function createSchema(schemaDefinition) {
const rawSchema = schemaDefinition(v);
return SchemaCompiler.compile(rawSchema);
}
/**
* Configures the endpoint with API documentation and validation
* @param endpoint - The endpoint function to configure
* @returns The current compiler object
*/
export function use(endpoint) {
const compiler = {
/**
* Sets the API documentation body for the endpoint
*/
body: function (schemaFn) {
endpoint.body = createSchema(schemaFn);
return compiler;
},
/**
* Sets the API documentation body for the endpoint
*/
response: function (schemaFn) {
endpoint.response = createSchema(schemaFn);
return compiler;
},
/**
* Sets the API documentation headers for the endpoint
* @param {Object} headers - The API documentation headers
*/
headers: function (headers) {
if (typeof endpoint !== "function") {
throw new Error("Endpoint must be a function");
}
endpoint.headers = headers;
return compiler;
},
/**
* Sets the API documentation title for the endpoint
* @param {string} title - The API documentation title
*/
title: function (title) {
if (typeof endpoint !== "function") {
throw new Error("Endpoint must be a function");
}
endpoint.title = title;
return compiler;
},
/**
* Sets the API documentation description for the endpoint
* @param {string} description - The API documentation description
*/
description: function (description) {
if (typeof endpoint !== "function") {
throw new Error("Endpoint must be a function");
}
endpoint.description = description;
return compiler;
},
/**
* Sets the API documentation params for the endpoint
*/
params: function (schemaFn) {
if (typeof endpoint !== "function") {
throw new Error("Endpoint must be a function");
}
endpoint.params = createSchema(schemaFn);
return compiler;
},
query: function (schemaFn) {
if (typeof endpoint !== "function") {
throw new Error("Endpoint must be a function");
}
endpoint.query = createSchema(schemaFn);
return compiler;
},
};
return compiler;
}
//? needs to optimized, does exactly the same as the getModule function
export async function codeGen(ROUTES_DIR, mode, generatedRoutesFilePath) {
//? Regex to find exported const variables
// ? let's make sure if this line is a comments then it should not be matched!
const ROUTE_EXPORT_REGEX = /^(?!\s*\/\/)export\s+const\s+((?:GET|POST|PUT|DELETE|PATCH|OPTIONS|HEAD|MIDDLEWARE)_[a-zA-Z0-9$]*\$?[a-zA-Z0-9$_]*)\s*/gm;
//? let's make sure if this line is a comments then it should not be matched!
const METHOD_PATH_REGEX = /(?:GET|POST|PUT|DELETE|PATCH|OPTIONS|HEAD|MIDDLEWARE)_[a-zA-Z0-9$_]*$/;
const OUTPUT_FILE = resolve(join(cwd(), "node_modules", "@jetpath", "index.ts"));
const ROUTE_FILE = resolve(generatedRoutesFilePath
? generatedRoutesFilePath
: join(cwd(), "definitions.ts"));
mkdirSync(dirname(OUTPUT_FILE), { recursive: true });
const declarations = [];
let mIdex = 0;
async function walkDir(currentDir) {
try {
const entries = await readdir(currentDir, {
withFileTypes: true,
});
for (const entry of entries) {
const fullPath = join(currentDir, entry.name);
if (entry.isDirectory()) {
if (!entry.name.startsWith(".")) {
await walkDir(fullPath);
}
}
else if (entry.isFile() && entry.name.endsWith(".jet.ts")) {
try {
const fileContent = await readFile(fullPath, "utf-8");
const foundExports = [];
let match;
while ((match = ROUTE_EXPORT_REGEX.exec(fileContent)) !== null) {
const exportName = match[1];
if (METHOD_PATH_REGEX.test(exportName)) {
foundExports.push(exportName);
}
else {
LOG.log(` ${exportName} is not a valid JetRoute export`, "error");
}
}
if (foundExports.length > 0) {
const moduleName = "m" + mIdex; // only alphanumeric letters;
//? Generate the declare module block for this file
let moduleDeclaration = `import * as ${moduleName} from '${fullPath}';\n\n\n`;
//? Add declarations for each found route export
for (const exportName of foundExports) {
//? Declare the export with the basic JetRoute<any, any> type
if (exportName.startsWith("MIDDLEWARE_")) {
moduleDeclaration +=
`const ${exportName} = ${moduleName}.${exportName} satisfies JetMiddleware<any, any>\n\n `;
}
else {
moduleDeclaration +=
`const ${exportName} = ${moduleName}.${exportName} satisfies JetRoute<any, any>\n\n `;
}
}
declarations.push(moduleDeclaration);
}
}
catch (error) {
console.error(`Error reading or parsing file ${fullPath}: ${error}`);
}
}
mIdex++;
}
}
catch (error) {
console.error(`Error reading directory ${currentDir}: ${error}`);
}
}
if (mode === "ON") {
await walkDir(resolve(cwd(), ROUTES_DIR));
}
else {
walkDir(resolve(cwd(), ROUTES_DIR));
}
const compileObjectStructureFromSchema = (schema) => {
const obj = {};
if (schema.type === "object") {
for (const key in schema.objectSchema) {
obj[key] = "string";
if (schema.objectSchema[key].type === "object") {
// @ts-ignore
obj[key] = compileObjectStructureFromSchema(schema.objectSchema[key]);
}
}
return obj;
}
const arrayObj = [];
if (schema.type === "array") {
if (schema.arrayType === "object") {
const obj = {};
for (const key in schema.objectSchema) {
obj[key] = "string";
}
arrayObj.push(obj);
}
else {
arrayObj.push(schema.arrayType);
}
return arrayObj;
}
return schema.type;
};
//? Generate the final .d.ts file content
let outputContent = `//? This file is auto-generated by Jetpath. DO NOT MODIFY!\n\n`;
outputContent +=
`// @ts-ignore\nimport { type JetRoute, JetMiddleware } from 'jetpath';\n\n`;
if (typeof generatedRoutesFilePath === "string") {
LOG.log("Generating routes file", "info");
const outputContent = `export const routes = {\n ${Object.keys(_JetPath_paths).reduce((acc, method) => {
const routes = Object.keys(_JetPath_paths[method]);
const obj = _JetPath_paths[method];
if (routes.length > 0) {
for (const route of routes) {
let body;
let response;
let params;
let query;
if (obj[route].body) {
for (const key in obj[route].body) {
if (!body) {
body = {};
}
const type = obj[route].body[key].type;
let val = type === "string"
? "string"
: type === "number"
? 1
: type === "boolean"
? true
: type === "object"
? compileObjectStructureFromSchema(obj[route].body[key])
: type === "array"
? compileObjectStructureFromSchema(obj[route].body[key])
: type === "file"
? "file"
: type;
body[key] = val;
}
}
if (obj[route].query) {
for (const key in obj[route].query) {
if (!query) {
query = {};
}
const type = obj[route].query[key].type;
let val = type === "string"
? "string"
: type === "number"
? 1
: type === "boolean"
? true
: type === "object"
? compileObjectStructureFromSchema(obj[route].query[key])
: type === "array"
? compileObjectStructureFromSchema(obj[route].query[key])
: type;
query[key] = val;
}
}
if (obj[route].response) {
for (const key in obj[route].response) {
if (!response) {
response = {};
}
const type = obj[route].response[key].type;
let val = type === "string"
? "string"
: type === "number"
? 1
: type === "boolean"
? true
: type === "object"
? compileObjectStructureFromSchema(obj[route].response[key])
: type === "array"
? compileObjectStructureFromSchema(obj[route].response[key])
: type === "file"
? "file"
: type;
response[key] = val;
}
}
if (obj[route].params) {
for (const key in obj[route].params) {
if (!params) {
params = {};
}
params[key] = "string";
}
}
acc.push(`${obj[route].name}: {\n path: "${route}",\n method: "${method.toLowerCase()}",\n${body ? ` body: ${JSON.stringify(body || {})},\n` : ""}${response
? ` response: ${JSON.stringify(response || {})},\n`
: ""}${query ? ` query: ${JSON.stringify(query || {})},\n` : ""} title: "${obj[route].title || ""}",\n${params ? ` params: ${JSON.stringify(params || {})},\n` : ""}}`);
}
}
return acc;
}, []).join(",\n ")} \n} as const;\n\n`;
LOG.log("Generated routes file successfully: " + ROUTE_FILE, "success");
await writeFile(ROUTE_FILE, outputContent, "utf-8");
}
//? Add all the generated module declarations
outputContent += declarations.join("\n");
try {
LOG.log("⚙️ StrictMode...\nmode: " + mode, "info");
await writeFile(OUTPUT_FILE, outputContent, "utf-8");
const promisifiedExecFile = () => new Promise((resolve) => {
execFile("tsc", [
"--noEmit",
"--target",
"ESNext",
"--module",
"NodeNext",
"--moduleResolution",
"NodeNext",
"--lib",
"ESNext,DOM",
"--strict",
"--esModuleInterop",
"--allowImportingTsExtensions",
"--skipLibCheck",
OUTPUT_FILE,
], { encoding: "utf8" }, (err, stdout, stderr) => {
if (err) {
if (err.toString().includes("Executable not found")) {
LOG.log("\n🛠️ StrictMode Can't work: Please install typescript using \n'npm install -g typescript' or \n'yarn global add typescript'\n\n", "error");
}
LOG.log("\n🛠️ StrictMode warnings", "warn");
if (typeof stderr === "string") {
LOG.log(stderr.replaceAll("\n", "\n\n"), mode === "WARN" ? "warn" : "error");
}
if (typeof stdout === "string") {
LOG.log(stdout.replaceAll("\n", "\n\n"), mode === "WARN" ? "warn" : "error");
const errors = (stdout?.split("\n") || []).length - 1;
LOG.log(errors +
` Problem${errors === 1 ? "" : "s"} 🐞\n\nYou are seeing these warnings because you have strict mode enabled\n`, "info");
}
}
resolve(undefined);
});
});
await promisifiedExecFile();
}
catch (error) {
LOG.log(`Error writing output file apis-types.d.ts: ${error}`, "error");
}
}
export function getLocalIP() {
const interfaces = networkInterfaces() || [];
for (const name of Object.keys(interfaces)) {
for (const iface of interfaces[name]) {
if ("IPv4" !== iface.family || iface.internal !== false) {
continue;
}
return iface.address;
}
}
}