From 1a2349055e56204ff9f7dd4bbb36a10e51ce7a46 Mon Sep 17 00:00:00 2001 From: Pushkar Mishra Date: Sat, 9 Aug 2025 15:09:39 +0530 Subject: [PATCH] feat: add compiler controller and integrate with express - Introduced a new compiler controller for handling compilation and testing of Rust/Soroban code. - Added validation for request bodies and error handling for compilation and test requests. - Updated package.json and bun.lock to include @types/cors for type definitions. - Refactored index.ts to mount the new compiler controller at the /api endpoint. - Enhanced commandExecutor to support optional working directory for command execution. Signed-off-by: Pushkar Mishra --- apps/backend/bun.lock | 5 +- apps/backend/package.json | 1 + .../src/controllers/compilerController.ts | 154 ++++++++++++++++++ apps/backend/src/index.ts | 8 +- apps/backend/src/utils/commandExecutor.ts | 21 ++- 5 files changed, 181 insertions(+), 8 deletions(-) create mode 100644 apps/backend/src/controllers/compilerController.ts diff --git a/apps/backend/bun.lock b/apps/backend/bun.lock index 9773556..b5bffa8 100644 --- a/apps/backend/bun.lock +++ b/apps/backend/bun.lock @@ -13,6 +13,7 @@ "devDependencies": { "@eslint/js": "^9.32.0", "@types/bun": "latest", + "@types/cors": "^2.8.19", "@types/express": "^5.0.3", "@types/node": "^24.1.0", "@typescript-eslint/eslint-plugin": "^8.38.0", @@ -21,7 +22,7 @@ "eslint-config-prettier": "^10.1.8", "prettier": "^3.6.2", "ts-node": "^10.9.2", - "typescript": "^5.9.2", + "typescript": "^5.8.3", "typescript-eslint": "^8.30.0", }, "peerDependencies": { @@ -84,6 +85,8 @@ "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + "@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/express": ["@types/express@5.0.3", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "*" } }, "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw=="], diff --git a/apps/backend/package.json b/apps/backend/package.json index ec357f1..a0b6ecb 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -14,6 +14,7 @@ "devDependencies": { "@eslint/js": "^9.32.0", "@types/bun": "latest", + "@types/cors": "^2.8.19", "@types/express": "^5.0.3", "@types/node": "^24.1.0", "@typescript-eslint/eslint-plugin": "^8.38.0", diff --git a/apps/backend/src/controllers/compilerController.ts b/apps/backend/src/controllers/compilerController.ts new file mode 100644 index 0000000..4a782bf --- /dev/null +++ b/apps/backend/src/controllers/compilerController.ts @@ -0,0 +1,154 @@ +import { Router, type Request, type Response } from 'express'; +import { setupProject, createRustProject } from '../utils/fileManager'; +import { executeCommand } from '../utils/commandExecutor'; + +const router = Router(); + +/** + * Request body interface for compile and test endpoints + */ +interface CompilerRequest { + code: string; +} + +/** + * Response interface for successful operations + */ +interface CompilerResponse { + output: string; +} + +/** + * Error response interface + */ +interface ErrorResponse { + error: string; + message: string; +} + +/** + * Validates the request body to ensure code is a non-empty string + */ +function validateRequest(req: Request): string | null { + const { code } = req.body as CompilerRequest; + + if (!code || typeof code !== 'string') { + return 'Code must be provided as a non-empty string'; + } + + if (code.trim() === '') { + return 'Code cannot be empty'; + } + + return null; +} + +/** + * POST /api/compile + * Compiles Rust/Soroban code using cargo build and stellar contract build + */ +router.post('/compile', async (req: Request, res: Response) => { + try { + // Validate request + const validationError = validateRequest(req); + if (validationError) { + return res.status(400).json({ + error: 'Invalid input', + message: validationError, + }); + } + + const { code } = req.body as CompilerRequest; + + // Set up temporary project + const project = await setupProject({ baseName: 'compile-project' }); + + try { + // Create Rust project structure + await createRustProject(project.tempDir, code); + + // Execute compilation commands + const buildCommand = `cargo build --target wasm32-unknown-unknown --release`; + await executeCommand(buildCommand, 300000, project.tempDir); + + // Execute stellar contract build using full path + const stellarCommand = `stellar contract build`; + const output = await executeCommand(stellarCommand, 300000, project.tempDir); + + // Return successful compilation result + res.status(200).json({ + output: output || 'Compilation successful', + }); + } catch (error) { + // Handle compilation errors + const errorMessage = error instanceof Error ? error.message : 'Unknown compilation error'; + res.status(400).json({ + error: 'Compilation failed', + message: errorMessage, + }); + } finally { + // Always cleanup the temporary directory + await project.cleanup(); + } + } catch { + // Handle server errors + res.status(500).json({ + error: 'Internal server error', + message: 'Failed to process compilation request', + }); + } +}); + +/** + * POST /api/test + * Runs tests for Rust/Soroban code using cargo test + */ +router.post('/test', async (req: Request, res: Response) => { + try { + // Validate request + const validationError = validateRequest(req); + if (validationError) { + return res.status(400).json({ + error: 'Invalid input', + message: validationError, + }); + } + + const { code } = req.body as CompilerRequest; + + // Set up temporary project + const project = await setupProject({ baseName: 'test-project' }); + + try { + // Create Rust project structure + await createRustProject(project.tempDir, code); + + // Execute test command + const testCommand = `cargo test`; + const output = await executeCommand(testCommand, 300000, project.tempDir); + + // Return successful test result + res.status(200).json({ + output: output || 'All tests passed', + }); + } catch (error) { + // Handle test errors + const errorMessage = error instanceof Error ? error.message : 'Unknown test error'; + res.status(400).json({ + error: 'Tests failed', + message: errorMessage, + }); + } finally { + // Always cleanup the temporary directory + await project.cleanup(); + } + } catch { + // Handle server errors + res.status(500).json({ + error: 'Internal server error', + message: 'Failed to process test request', + }); + } +}); + +export default router; diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index 6ad80b8..c54106d 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -1,7 +1,8 @@ -import express from 'express'; +import express, { type Request, type Response, type NextFunction } from 'express'; import helmet from 'helmet'; import cors from 'cors'; import { setupProject, getSanitizedDirName, createRustProject } from './utils/fileManager'; +import compilerController from './controllers/compilerController'; const app = express(); @@ -45,6 +46,9 @@ app.get('/', (_, res) => res.send('Hello from Backend!' + '
' + 'The best online soroban compiler is coming...') ); +// Mount compiler controller at /api +app.use('/api', compilerController); + // Test endpoint for fileManager functionality app.post('/api/test-filemanager', async (req, res) => { try { @@ -83,7 +87,7 @@ app.post('/api/test-filemanager', async (req, res) => { }); // Error handling middleware -app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => { +app.use((err: Error, req: Request, res: Response, next: NextFunction) => { res.status(500).json({ error: 'Internal Server Error', message: process.env.NODE_ENV === 'development' ? err.message : 'Something went wrong', diff --git a/apps/backend/src/utils/commandExecutor.ts b/apps/backend/src/utils/commandExecutor.ts index 95dcdf5..fac9cb8 100644 --- a/apps/backend/src/utils/commandExecutor.ts +++ b/apps/backend/src/utils/commandExecutor.ts @@ -2,16 +2,24 @@ import { spawn, type SpawnOptionsWithoutStdio } from 'child_process'; const DEFAULT_TIMEOUT = 30000; +interface CommandError extends Error { + stderr: string; + stdout: string; + code: number | null; +} + /** * Executes a shell command securely with a timeout * @param command The command to execute * @param timeout Maximum execution time in milliseconds (default: 30000) + * @param cwd Working directory to execute the command in (optional) * @returns Promise that resolves with the command output * @throws Error with stderr content if command fails or times out */ export async function executeCommand( command: string, - timeout: number = DEFAULT_TIMEOUT + timeout: number = DEFAULT_TIMEOUT, + cwd?: string ): Promise { // Validate inputs if (typeof command !== 'string' || command.trim() === '') { @@ -25,6 +33,7 @@ export async function executeCommand( const options: SpawnOptionsWithoutStdio = { shell: '/bin/bash', env: { ...process.env }, + ...(cwd && { cwd }), }; return new Promise((resolve, reject) => { @@ -56,10 +65,12 @@ export async function executeCommand( if (code === 0) { resolve(stdout.trim()); } else { - const error = new Error(stderr.trim() || `Command failed with exit code ${code}`); - (error as any).stderr = stderr.trim(); - (error as any).stdout = stdout.trim(); - (error as any).code = code; + const error = new Error( + stderr.trim() || `Command failed with exit code ${code}` + ) as CommandError; + error.stderr = stderr.trim(); + error.stdout = stdout.trim(); + error.code = code; reject(error); } });