UNPKG

tspace-spear

Version:

tspace-spear is a lightweight, high-performance API framework for Node.js that leverages the native HTTP server and supports uWebSockets.js (C++) for maximum speed and efficiency.

512 lines (509 loc) 17 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.generateRoutes = void 0; const ts_morph_1 = require("ts-morph"); const typescript_1 = __importDefault(require("typescript")); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const HTTP_METHODS = { Get: "GET", Post: "POST", Put: "PUT", Patch: "PATCH", Delete: "DELETE", Options: "OPTIONS", Head: "HEAD", }; const normalizeType = (t) => { return t .split("|") .map(v => v.trim()) .filter(v => v !== "null" && v !== "undefined")[0] || "string"; }; const maybeObject = (v) => { const s = v.trim(); return s.startsWith("{") && s.endsWith("}"); }; const maybeArrayObject = (v) => { const s = v.trim(); return s.endsWith("}[]"); }; const normalizePath = (p) => { return ("/" + p .replace(/['"`]/g, "") .replace(/\/+/g, "/") .replace(/\/$/, "") .replace(/^\//, "")); }; const splitTopLevel = (input) => { const parts = []; let current = ""; let depth = 0; for (const char of input) { if (char === "{") depth++; if (char === "}") depth--; if (char === ";" && depth === 0) { parts.push(current.trim()); current = ""; continue; } current += char; } if (current.trim()) { parts.push(current.trim()); } return parts; }; const parseType = (type) => { type = type.trim(); if (type === "never") { return undefined; } if (type.startsWith("Record<")) { return {}; } if (type.startsWith("Partial<")) { const inner = type .replace(/^Partial</, "") .replace(/>$/, ""); const parsed = parseType(inner); if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { for (const key in parsed) { parsed[key] = parsed[key] + " | null"; } } return parsed; } if (type.startsWith("Promise<")) { const inner = type .replace(/^Promise</, "") .replace(/>$/, ""); return parseType(inner); } if (type.endsWith("[]")) { return [parseType(type.slice(0, -2))]; } if (type.startsWith("{") && type.endsWith("}")) { const result = {}; const content = type.slice(1, -1); const fields = splitTopLevel(content) .map((x) => x.trim()) .filter(Boolean); for (const field of fields) { const match = field.match(/^(\w+)(\??):\s*(.+)$/); if (!match) continue; const [, key, optional, value] = match; let parsed; if (value === "string") { parsed = optional ? "string | null" : "string"; } else if (value === "number") { parsed = optional ? "number | null" : "number"; } else if (value === "boolean") { parsed = optional ? "boolean | null" : "boolean"; } else { parsed = parseType(value); } result[key] = parsed; } return result; } if (type === "string") return "string"; if (type === "number") return "number"; if (type === "boolean") return "boolean"; if (type === "date") return "date"; if (type === "Date") return "Date"; if (type === "string | undefined") return "string"; if (type === "number | undefined") return "number"; if (type === "boolean | undefined") return "boolean"; if (type === "date | undefined") return "date"; if (type === "Date | undefined") return "Date"; if (type.includes("| undefined")) { return type; } if (type.includes("| null")) { return type; } if (type.includes(" | ") && !type.startsWith("{")) { return type.replace(/"/g, ""); } // fallback return "string"; }; const resolveType = (type) => { if (type.getSymbol()?.getName() === "Promise") { type = type.getTypeArguments()[0]; } if (type.isString()) return "string"; if (type.isNumber()) return "number"; if (type.isBoolean()) return "boolean"; if (type.isNull()) return "null"; if (type.isUndefined()) return "undefined"; if (type.isAny()) return "any"; if (type.isUnknown()) return "unknown"; if (type.isStringLiteral() || type.isBooleanLiteral() || type.isNumberLiteral()) return type.getText(); if (type.getText() === 'Date' && type.getSymbol()?.getName() === 'Date') { return "Date"; } if (type.getText().includes("ServerResponse") && type.getText().includes("THttpResponder")) { const filtered = type .getIntersectionTypes() .filter((t) => { const text = t.getText(); return (!text.includes("ServerResponse") && !text.includes("THttpResponder")); }); const t = filtered[0]; if (t == null) return 'never'; return resolveType(t); } if (type.isUnion()) { const text = type.getText(); if (text.includes('| null') || text.includes('| undefined')) { const types = type.getUnionTypes(); if (types.length > 1) { const nonSpecial = types.filter(t => !t.isNull() && !t.isUndefined()); const hasNull = types.some(t => t.isNull()); const hasUndefined = types.some(t => t.isUndefined()); const sorted = [ ...nonSpecial, ...(hasNull ? [types.find(t => t.isNull())] : []), ...(hasUndefined ? [types.find(t => t.isUndefined())] : []), ]; return sorted.map(t => resolveType(t)).join(" | "); } } return text; } if (type.isArray()) { const el = type.getArrayElementTypeOrThrow(); return `${resolveType(el)}[]`; } if (type.getProperties().length) { const props = type.getProperties(); const obj = []; for (const prop of props) { const decl = prop.getDeclarations()[0]; if (!decl) continue; const propType = prop.getTypeAtLocation(decl); const text = propType.getText(decl); const key = prop.getName(); let value = resolveType(propType); if (/^\s*(\(.*\)\s*=>|function\b)/.test(value)) { continue; } if (text.includes('[x: string]')) { value = text; } ; let colon = ":"; const maybeOptional = value.includes(" | undefined"); if (maybeOptional) { value = value.replace(" | undefined", ""); colon = "?:"; } obj.push(`${key}${colon} ${value}`); } return `{ ${obj.join("; ")} }`; } return type.getText(); }; const extractPropertyType = (type, key, node) => { const prop = type.getProperty(key); if (!prop) return "never"; const t = prop.getTypeAtLocation(node); const text = t.getText(node); if (text.includes('[x: string]')) return text; if (!text || text.includes("undefined")) return "never"; return resolveType(t) ?? "never"; }; const generateRoutes = async (globalPrefix, options) => { const project = new ts_morph_1.Project({ tsConfigFilePath: path_1.default.resolve(process.cwd(), "tsconfig.json"), }); project.addSourceFilesAtPaths(path_1.default.join(options.folder, "**/*")); const files = project.getSourceFiles(); if (!files.length) { console.log("No controller files found"); return; } const routes = []; for (const file of files) { const filename = file.getBaseName(); if (!options.name.test(filename)) continue; for (const cls of file.getClasses()) { const controller = cls.getDecorator("Controller"); if (!controller) continue; const basePath = controller.getArguments()[0] ?.getText() .replace(/['"`]/g, "") || ""; for (const method of cls.getMethods()) { for (const [decName, http] of Object.entries(HTTP_METHODS)) { const decorator = method.getDecorator(decName); if (!decorator) continue; const methodPath = decorator .getArguments()[0] ?.getText() .replace(/['"`]/g, "") || ""; const fullPath = normalizePath(`${basePath}/${methodPath}`); const response = resolveType(method.getReturnType()); let body = "never"; let params = "never"; let query = "never"; let files = "never"; const firstParam = method.getParameters()[0]; if (firstParam) { const type = firstParam.getType(); params = extractPropertyType(type, "params", firstParam); query = extractPropertyType(type, "query", firstParam); body = extractPropertyType(type, "body", firstParam); files = extractPropertyType(type, "files", firstParam); if (body === 'Record<string, any>') body = "never"; } routes.push({ method: http, path: fullPath, response, body, params, query, files }); } } } } const groupedTypes = routes.reduce((acc, r) => { acc[r.path] ??= {}; acc[r.path][r.method] = { response: r.response, body: r.body, params: r.params, query: r.query, files: r.files, }; return acc; }, {}); const routeMapTypes = Object.entries(groupedTypes) .map(([path, methods]) => { const methodBlock = Object.entries(methods) .map(([method, c]) => ` ${method}: { params: ${c.params} query: ${c.query} body: ${c.body} files: ${c.files} response: ${c.response} }`).join("\n"); return ` "${path}": { ${methodBlock} }`; }) .join("\n"); const formatExampleValue = (v) => { if (v === null) { return "null"; } if (v === undefined) { return "undefined"; } if (typeof v === "string") { const t = normalizeType(v.trim()); if (maybeObject(t)) { const inner = t.trim().slice(1, -1); const result = Object.fromEntries(splitTopLevel(inner) .map(s => s.trim()) .filter(Boolean) .map(pair => { const idx = pair.indexOf(":"); const key = pair.slice(0, idx).trim(); const type = pair.slice(idx + 1).trim(); return [key.replace(/\?/g, ''), type]; })); return formatExampleValue(result); } if (maybeArrayObject(t)) { const s = v.trim(); const output = s.replace(/(\w+):\s*(\{[^}]+\})\[\]/, '$1: [$2]').match(/\{(.*)\}/)?.[1]; if (!output) return `[]`; const result = Object.fromEntries(splitTopLevel(output) .map(s => s.trim()) .filter(Boolean) .map(pair => { const idx = pair.indexOf(":"); const key = pair.slice(0, idx).trim(); const type = pair.slice(idx + 1).trim(); return [key, type]; })); return formatExampleValue(result); } switch (t) { case "string": return `"example"`; case "string[]": return `["example1", "example2", "example3"]`; case "number": return "123"; case "number[]": return "[1 ,2, 3]"; case "boolean": return "true"; case "boolean[]": return "[true, false, true]"; case "null": return "null"; case "null[]": return "[null, null, null]"; case "undefined": return "undefined"; case "undefined[]": return "[undefined, undefined, undefined]"; case "date": case "Date": return `"2000-01-01T00:00:00.000Z"`; case "date[]": case "Date[]": return `["2000-01-01T00:00:00.000Z","2000-01-02T00:00:00.000Z","2000-01-03T00:00:00.000Z"]`; default: return `"${t.replace(/"/g, "")}"`; } } if (Array.isArray(v)) { if (v.length === 0) { return "[]"; } return `[${formatExampleValue(v[0])}]`; } if (typeof v === "object") { const entries = Object.entries(v).map(([key, value]) => { if (key.includes("uuid")) { return `${key}: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"`; } if (key === "id" || key.endsWith("id")) { return `${key}: 123`; } return `${key}: ${formatExampleValue(value)}`; }); return `{ ${entries.map(v => `${v}`).join(", ")} }`; } return JSON.stringify(v); }; const groupedValues = routes.reduce((acc, route) => { if (!acc[route.path]) { acc[route.path] = {}; } acc[route.path][route.method] = { params: parseType(route.params), query: parseType(route.query), body: parseType(route.body), files: parseType(route.files), response: parseType(route.response), }; return acc; }, {}); const routerMapValues = Object.entries(groupedValues) .map(([path, methods]) => { const methodBlock = Object.entries(methods) .map(([method, c]) => ` ${method}: { params: ${formatExampleValue(c.params)}, query: ${formatExampleValue(c.query)}, body: ${formatExampleValue(c.body)}, files: ${formatExampleValue(c.files)}, response: ${formatExampleValue(c.response)} }`).join(",\n"); return ` "${path}": { ${methodBlock} }`; }) .join(",\n"); const output = ` // @ts-nocheck // AUTO GENERATED FILE // DO NOT EDIT // **Response values shown here are examples only. ${globalPrefix ? `// **The App is using the configuration: // globalPrefix: '${globalPrefix}'` : ''} export const appRoutes = { ${routerMapValues} }; export interface AppRoutes { ${routeMapTypes} }; export type AppRoute = keyof AppRoutes; `; const outPath = options.output ? `${__dirname}/${options.output}/pre-routes.ts` : `${__dirname}/pre-routes.ts`; await fs_1.default.promises.mkdir(path_1.default.dirname(outPath), { recursive: true, }); const compiled = typescript_1.default.transpileModule(output, { compilerOptions: { module: typescript_1.default.ModuleKind.CommonJS, target: typescript_1.default.ScriptTarget.ESNext }, }); await Promise.all([ fs_1.default.promises.writeFile(outPath, output), fs_1.default.promises.writeFile(outPath.replace(/\.ts$/, ".js"), compiled.outputText) ]); return routes; }; exports.generateRoutes = generateRoutes; //# sourceMappingURL=generator.js.map