@maravilla-labs/functions
Version:
Maravilla Edge Functions bundler and development tools
154 lines • 6.43 kB
JavaScript
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