UNPKG

jetpath

Version:

A performance-first cross-runtime API framework without the boilerplate

1,321 lines (1,320 loc) 53.8 kB
// type imports import {} from 'node:http'; import { ArraySchema, BooleanSchema, Context, DateSchema, FileSchema, JetPlugin, JetSocket, LOG, NumberSchema, ObjectSchema, SchemaBuilder, SchemaCompiler, StringSchema, Trie, } from './classes.js'; /** * 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 const _JetPath_paths = { GET: {}, POST: {}, HEAD: {}, PUT: {}, PATCH: {}, DELETE: {}, OPTIONS: {}, CONNECT: {}, TRACE: {}, }; export const _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, cloudflare_worker: false, aws_lambda: 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 { return false; } }; (() => { //? check for bun runtime const bun = ae(() => Bun); //? check for deno runtime // @ts-expect-error to avoid the Deno keyword const deno = ae(() => Deno); let cloudflare_worker = false; let aws_lambda = false; //? check if running in Cloudflare Worker if (typeof globalThis.WebSocketPair !== 'undefined' && typeof globalThis.caches !== 'undefined' && typeof globalThis.Response !== 'undefined') { cloudflare_worker = true; } // AWS Lambda if (typeof process !== 'undefined' && process.env?.['AWS_LAMBDA_FUNCTION_NAME']) { aws_lambda = true; } runtime = { bun, deno, node: !bun && !deno, aws_lambda, cloudflare_worker, edge: cloudflare_worker || aws_lambda, }; })(); // ? isNode export const isNode = runtime['node']; // ? server export const server = (plugs, options) => { let server; let server_else; if (runtime['node']) { server = fs().createServer({ keepAliveTimeout: options.keepAliveTimeout || 120_000, keepAlive: true, }, (x, y) => { // @ts-expect-error to avoid the any error Jetpath(x, y); }); } if (runtime['deno']) { server = { listen(port) { // @ts-expect-error to avoid the Deno keyword server_else = Deno.serve({ port: port }, Jetpath); }, edge: false, }; } if (runtime['cloudflare_worker']) { server = { listen() { // Cloudflare Worker uses `addEventListener("fetch", ...)` addEventListener('fetch', (event) => { // @ts-expect-error to avoid the FetchEvent error event.respondWith(Jetpath(event.request)); }); }, edge: true, }; } if (runtime['aws_lambda']) { server = { listen() { // AWS Lambda requires exporting a handler function // We'll wrap to Lambda-compatible handler const awsHandler = async (event) => { const req = new Request(event.rawPath || '/', { method: event.requestContext?.http?.method || 'GET', headers: event.headers, body: event.body, }); // @ts-expect-error to avoid the extra arguments error const res = await Jetpath(req); const text = await res.text(); return { statusCode: res.status, headers: Object.fromEntries(res.headers), body: text, }; }; module.exports.handler = awsHandler; }, edge: true, }; } if (runtime['bun']) { if (options.upgrade && options.upgrade === true) { server = { listen(port) { server_else = Bun.serve({ port, // @ts-expect-error to avoid the extra arguments error fetch: Jetpath, websocket: { message(...p) { p[1] = { // @ts-expect-error to avoid the type errorm ensuring we pass the opionated data prop. 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 to avoid the extra arguments error fetch: Jetpath, }); }, edge: false, }; } } // ? yes a plugin can bring it's own server? good for edge //? 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 CContext 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 to avoid .then error on stream type if (runtime['deno'] && ctx._3.then) { ctxPool.push(ctx); // @ts-expect-error same 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 if (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', () => { res.statusCode = 400; 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); if (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); const returned = []; if (ctx) { const r = ctx.handler; try { //? pre-request middlewares here if (r.jet_middleware?.length) { for (let m = 0; m < r.jet_middleware.length; m++) { const callback = await r.jet_middleware[m](ctx); if (typeof callback === 'function') { returned.unshift(callback); } } } //? 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 for (let r = 0; r < returned.length; r++) { await returned[r](ctx); } return makeRes(res, ctx); } catch (error) { try { //? report error to error middleware if (returned.length) { for (let r = 0; r < returned.length; r++) { await returned[r](ctx, error); } } else { console.log(error); } } catch (error) { console.log(error); } finally { if (!returned.length && ctx.code < 400) { ctx.code = 500; } return makeRes(res, ctx); } } } const ctx404 = optionsCtx; ctx404.code = 404; return makeRes(res, ctx404); }; const handlersPath = (path) => { path = path.replaceAll('__', '-'); // ? convert __ to - const [method, ...segments] = path.split('_'); let route = '/' + segments.join('/'); // eslint-disable-next-line no-useless-escape 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) => { const absolutePath = fs().resolve(src + '/' + name); //? Gets native OS path try { const fileUrl = fs().pathToFileURL(absolutePath).href; const mod = await import(fileUrl); return mod; } catch (error) { LOG.log('Error at ' + absolutePath + ' loading failed!', 'info'); LOG.log(String(error), 'error'); return String(error); } }; export async function getHandlers(source, print, errorsCount = undefined, again = false) { const curr_d = fs().cwd(); const error_source = source; source = source || ''; if (!again) { source = fs().resolve(fs().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 = fs().resolve(curr_d, source); } const dir = await fs().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 + '/', '') + fs().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 async function getHandlersEdge(modules) { for (const p in modules) { const params = handlersPath(p); if (params) { if (p.startsWith('MIDDLEWARE')) { _jet_middleware[params[1]] = modules[p]; } else { // ! HTTP handler if (typeof params !== 'string') { // ? set the method modules[p].method = params[0]; // ? set the path modules[p].path = params[1]; _JetPath_paths[params[0]][params[1]] = modules[p]; _JetPath_paths_trie[params[0]].insert(params[1], modules[p]); } } } } } 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)) { const { RegExp, arrayType, err, objectSchema, required, type, validator: validate, } = defs; const value = data[key]; // Required check // eslint-disable-next-line eqeqeq if (required && value == null) { errors.push(`${key} is required`); continue; } // Skip if optional and undefined // eslint-disable-next-line eqeqeq 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 }', 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; const compiledAPIArray = []; const 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 (let route of routes) { if (!Array.isArray(route.jet_middleware)) { route.jet_middleware = []; } else { route = (...c) => route(...c); route.jet_middleware = []; } } for (const route of routes) { // If middleware is defined for the route, ensure it has exactly one middleware function. const allMiddlewaresSorted = Object.keys(_jet_middleware) .filter((m) => route.path.startsWith(m)) .sort((a, b) => a.length - b.length); for (const key of allMiddlewaresSorted) { const middleware = _jet_middleware[key]; // Assign the middleware function to the route handler. if (Array.isArray(middleware)) { route.jet_middleware.push(...middleware.flat()); } else { 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 { 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()) { // eslint-disable-next-line no-prototype-builtins 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, connectionLinks, 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 = fs().resolve(fs().join(fs().cwd(), 'node_modules', '@jetpath', 'index.ts')); const ROUTE_FILE = fs().resolve(generatedRoutesFilePath ? generatedRoutesFilePath : fs().join(fs().cwd(), 'definitions.ts')); fs().mkdirSync(fs().dirname(OUTPUT_FILE), { recursive: true }); const declarations = []; let mIdex = 0; async function walkDir(currentDir) { try { const entries = await fs().readdir(currentDir, { withFileTypes: true, }); for (const entry of entries) { const fullPath = fs().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 fs().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(fs().resolve(fs().cwd(), ROUTES_DIR)); } else { walkDir(fs().resolve(fs().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') { 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 connectionInfo = `export const connectionInfo = { local: '${connectionLinks.local}', external: '${connectionLinks.external}' };`; const outputContent = `//This file is autogenerated by Jetpath\n\n${connectionInfo}\n\nexport 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; const 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; const 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; const 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`; try { await fs().writeFile(ROUTE_FILE, outputContent, 'utf-8'); LOG.log('Generated routes file successfully: ' + ROUTE_FILE, 'success'); } catch (error) { LOG.log(`Error writing routes file ${ROUTE_FILE}: ${error}`, 'error'); } } //? Add all the generated module declarations outputContent += declarations.join('\n'); try { LOG.log('⚙️ StrictMode...\nmode: ' + mode, 'info'); await fs().writeFile(OUTPUT_FILE, outputContent, 'utf-8'); const promisifiedExecFile = () => new Promise((resolve) => {