UNPKG

elysia-autoload

Version:

Plugin for Elysia which autoload all routes in directory and code-generate types for Eden with Bun.build support

126 lines (122 loc) 5.18 kB
import fs from 'node:fs'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; import { Elysia } from 'elysia'; function getPath(dir) { if (path.isAbsolute(dir)) return dir; if (path.isAbsolute(process.argv[1])) return path.join(process.argv[1], "..", dir); return path.join(process.cwd(), process.argv[1], "..", dir); } function transformToUrl(path2) { return path2.replace(/\.(ts|tsx|js|jsx|mjs|cjs)$/u, "").replaceAll("\\", "/").replaceAll(/\[\.\.\..*\]/gu, "*").replaceAll(/\[(.*?)\]/gu, (_, match) => `:${match}`).replace(/\/?\((.*)\)/, "").replaceAll("]-[", "-:").replaceAll("]/", "/").replaceAll(/\[|\]/gu, "").replace(/\/?index$/, ""); } function getParamsCount(path2) { return path2.match(/\[(.*?)\]/gu)?.length || 0; } function sortByNestedParams(routes) { return routes.sort((a, b) => getParamsCount(a) - getParamsCount(b)); } function fixSlashes(prefix) { if (!prefix?.endsWith("/")) return prefix; return prefix.slice(0, -1); } function addRelativeIfNotDot(path2) { if (path2.at(0) !== ".") return `./${path2}`; return path2; } const IS_BUN = typeof Bun !== "undefined"; function globSync(globPattern, globOptions) { return IS_BUN ? Array.from(new Bun.Glob(globPattern).scanSync(globOptions)) : fs.globSync(globPattern, globOptions); } const DIR_ROUTES_DEFAULT = "./routes"; const TYPES_OUTPUT_DEFAULT = "./routes-types.ts"; const TYPES_TYPENAME_DEFAULT = "Routes"; const TYPES_OBJECT_DEFAULT = { output: [TYPES_OUTPUT_DEFAULT], typeName: TYPES_TYPENAME_DEFAULT }; async function autoload(options = {}) { const { pattern, prefix, schema } = options; const failGlob = options.failGlob ?? true; const getImportName = options?.import ?? "default"; const dir = options.dir ?? DIR_ROUTES_DEFAULT; const types = options.types ? options.types !== true ? { ...TYPES_OBJECT_DEFAULT, ...options.types, // This code allows you to omit the output data or specify it as an string[] or string. output: !options.types.output ? [TYPES_OUTPUT_DEFAULT] : Array.isArray(options.types.output) ? options.types.output : [options.types.output] } : TYPES_OBJECT_DEFAULT : false; const directoryPath = getPath(dir); if (!fs.existsSync(directoryPath)) throw new Error(`Directory ${directoryPath} doesn't exists`); if (!fs.statSync(directoryPath).isDirectory()) throw new Error(`${directoryPath} isn't a directory`); const plugin = new Elysia({ name: "elysia-autoload", prefix: fixSlashes(prefix), seed: { pattern, dir, prefix, types } }); const globPattern = pattern || "**/*.{ts,tsx,js,jsx,mjs,cjs}"; const globOptions = { cwd: directoryPath }; const files = globSync(globPattern, globOptions); if (failGlob && files.length === 0) throw new Error( `No matches found in ${directoryPath}. You can disable this error by setting the failGlob parameter to false in the options of autoload plugin` ); const paths = []; for (const filePath of sortByNestedParams(files)) { const fullPath = path.join(directoryPath, filePath); const file = await import(pathToFileURL(fullPath).href); const importName = typeof getImportName === "string" ? getImportName : getImportName(file); const importedValue = file[importName]; if (!importedValue) { if (options?.skipImportErrors) continue; throw new Error(`${filePath} don't provide export ${importName}`); } const url = transformToUrl(filePath); const groupOptions = schema ? schema({ path: filePath, url }) : {}; if (typeof importedValue === "function") if (importedValue.length > 0) plugin.group(url, groupOptions, importedValue); else plugin.group(url, groupOptions, (app) => app.use(importedValue())); else if (importedValue instanceof Elysia) plugin.group(url, groupOptions, (app) => app.use(importedValue)); if (types) paths.push([fullPath.replace(directoryPath, ""), importName]); } if (types) { for await (const outputPath of types.output) { const outputAbsolutePath = getPath(outputPath); const imports = paths.map( ([x, exportName], index) => `import type ${exportName === "default" ? `Route${index}` : `{ ${exportName} as Route${index} }`} from "${addRelativeIfNotDot( path.relative( path.dirname(outputAbsolutePath), directoryPath + x.replace(/\.(ts|tsx)$/, "") ).replaceAll("\\", "/") )}";` ); const input = [ `import type { ElysiaWithBaseUrl } from "elysia-autoload";`, imports.join("\n"), "", !types.useExport ? "declare global {" : "", ` export type ${types.typeName} = ${paths.map( ([x], index) => `ElysiaWithBaseUrl<"${((prefix?.endsWith("/") ? prefix.slice(0, -1) : prefix) ?? "") + transformToUrl(x) || "/"}", typeof Route${index}>` ).join("\n & ")}`, !types.useExport ? "}" : "" ].join("\n"); if (typeof Bun === "undefined") { fs.writeFileSync(outputAbsolutePath, input); } else { await Bun.write(outputAbsolutePath, input); } } } return plugin; } export { autoload };