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
JavaScript
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 };