UNPKG

@maravilla-labs/functions

Version:

Maravilla Edge Functions bundler and development tools

154 lines 6.43 kB
import { promises as fs } from 'node:fs'; import { join, relative } from 'node:path'; export async function discoverFunctions(functionsDir) { const functions = {}; const routes = {}; await walkDirectory(functionsDir, functionsDir, functions, routes); return { functions, routes }; } async function walkDirectory(baseDir, currentDir, functions, routes) { const entries = await fs.readdir(currentDir, { withFileTypes: true }); for (const entry of entries) { const fullPath = join(currentDir, entry.name); if (entry.isDirectory()) { // Skip dist and node_modules if (entry.name === 'dist' || entry.name === 'node_modules' || entry.name.startsWith('.')) { continue; } // Check if this is a function directory with package.json const packageJsonPath = join(fullPath, 'package.json'); const hasPackageJson = await fs.access(packageJsonPath).then(() => true).catch(() => false); if (hasPackageJson) { // This is a function package - look for src directory const srcPath = join(fullPath, 'src'); const hasSrc = await fs.access(srcPath).then(() => true).catch(() => false); if (hasSrc) { // Walk the src directory for this function await walkDirectory(baseDir, srcPath, functions, routes); } else { // Fallback to the function directory itself await walkDirectory(baseDir, fullPath, functions, routes); } } else { // Regular directory, continue walking await walkDirectory(baseDir, fullPath, functions, routes); } } else if (entry.isFile() && (entry.name.endsWith('.js') || entry.name.endsWith('.ts'))) { // Skip test files and type definition files if (entry.name.includes('.test.') || entry.name.includes('.spec.') || entry.name.endsWith('.d.ts')) { continue; } const relativePath = relative(baseDir, fullPath); const functionInfo = await analyzeFunction(fullPath, relativePath); if (functionInfo) { functions[functionInfo.name] = functionInfo; routes[functionInfo.route] = functionInfo; } } } } async function analyzeFunction(filePath, relativePath) { try { const content = await fs.readFile(filePath, 'utf8'); // Generate function name from file path const name = generateFunctionName(relativePath); // Generate route from file path const route = generateRoute(relativePath); // Analyze supported HTTP methods const methods = analyzeMethods(content); // Skip files that don't export any HTTP methods if (methods.length === 0) { console.log(` Skipping ${relativePath} - no HTTP methods detected`); return null; } return { name, route, methods, filePath, relativePath, importPath: filePath, }; } catch (error) { console.warn(` Failed to analyze function ${relativePath}:`, error); return null; } } function generateFunctionName(relativePath) { // Convert path to camelCase function name // e.g., "auth/login.js" -> "authLogin" // e.g., "src/hello.js" -> "hello" (strip src prefix) let pathWithoutExt = relativePath.replace(/\.(js|ts)$/, ''); // Strip 'src/' prefix if present if (pathWithoutExt.startsWith('src/') || pathWithoutExt.startsWith('src\\')) { pathWithoutExt = pathWithoutExt.substring(4); } const parts = pathWithoutExt.split(/[\/\\]/); return parts .map((part, index) => { // Remove index suffix from filenames if (part === 'index') { return ''; } // Convert kebab-case to camelCase const camelCased = part.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()); // Capitalize first letter of each part except the first return index === 0 ? camelCased : camelCased.charAt(0).toUpperCase() + camelCased.slice(1); }) .filter(Boolean) .join(''); } function generateRoute(relativePath) { // Convert path to API route // e.g., "auth/login.js" -> "/api/auth/login" // e.g., "src/hello.js" -> "/api/hello" (strip src prefix) // e.g., "index.js" -> "/api" let pathWithoutExt = relativePath.replace(/\.(js|ts)$/, ''); // Strip 'src/' prefix if present if (pathWithoutExt.startsWith('src/') || pathWithoutExt.startsWith('src\\')) { pathWithoutExt = pathWithoutExt.substring(4); } // Handle index files if (pathWithoutExt === 'index' || pathWithoutExt === '') { return '/api'; } // Remove index from path parts const parts = pathWithoutExt.split(/[\/\\]/).filter(part => part !== 'index'); // Convert to kebab-case const routeParts = parts.map(part => part.replace(/([A-Z])/g, '-$1').toLowerCase()); return '/api/' + routeParts.join('/'); } function analyzeMethods(content) { const methods = []; const httpMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']; // Look for exported HTTP method handlers for (const method of httpMethods) { // Check for named exports if (content.includes(`export const ${method}`) || content.includes(`export function ${method}`) || content.includes(`export async function ${method}`)) { methods.push(method); } } // Check for default export that handles all methods if (methods.length === 0) { const hasDefaultExport = content.includes('export default'); if (hasDefaultExport) { // Check if it looks like a request handler const looksLikeHandler = content.includes('request') || content.includes('Request') || content.includes('req.method') || content.includes('request.method'); if (looksLikeHandler) { // Default export handles all methods methods.push(...httpMethods); } } } return methods; } //# sourceMappingURL=discover.js.map