UNPKG

@ordojs/cli

Version:

Command-line interface for OrdoJS framework

683 lines (626 loc) 22.5 kB
/** * @fileoverview OrdoJS CLI - AWS Lambda deployment adapter * Adapter for deploying to AWS Lambda and API Gateway */ import path from 'path'; import { AssetOptimizer, OptimizationResults } from '../../asset-optimizer.js'; import { mkdir, writeFile } from '../../fs.js'; import { logger } from '../../logger.js'; import { DeploymentAdapter, DeploymentConfig, DeploymentResult } from '../adapter-interface.js'; /** * AWS Lambda deployment adapter */ export class AWSLambdaAdapter { /** * Adapter name */ name = 'aws-lambda'; /** * Adapter description */ description = 'Deploy to AWS Lambda and API Gateway'; /** * Validate AWS Lambda deployment configuration * @param config Deployment configuration * @returns Validation result */ validateConfig(config) { const errors = []; // Check required fields if (!config.outputDir) { errors.push('outputDir is required'); } // Validate AWS Lambda-specific settings const awsConfig = config; if (awsConfig.settings) { // Validate region if provided if (awsConfig.settings.region && !/^[a-z]{2}-[a-z]+-\d$/.test(awsConfig.settings.region)) { errors.push('Invalid AWS region format (e.g., us-east-1)'); } // Validate function name if provided if (awsConfig.settings.functionName && !/^[a-zA-Z0-9-_]+$/.test(awsConfig.settings.functionName)) { errors.push('Function name must contain only letters, numbers, hyphens, and underscores'); } // Validate memory size if provided if (awsConfig.settings.memorySize && (awsConfig.settings.memorySize < 128 || awsConfig.settings.memorySize > 10240)) { errors.push('Memory size must be between 128 MB and 10240 MB'); } // Validate timeout if provided if (awsConfig.settings.timeout && (awsConfig.settings.timeout < 1 || awsConfig.settings.timeout > 900)) { errors.push('Timeout must be between 1 and 900 seconds'); } } return { valid: errors.length === 0, errors: errors.length > 0 ? errors : undefined }; } /** * Prepare AWS Lambda deployment * @param config Deployment configuration * @returns Deployment result */ async prepareDeployment(config) { const awsConfig = config; const generatedFiles = []; try { // Create build output directory if it doesn't exist await mkdir(config.outputDir, { recursive: true }); // Generate serverless.yml configuration const serverlessYamlContent = this.generateServerlessConfig(awsConfig); const serverlessYamlPath = path.join(config.outputDir, 'serverless.yml'); await writeFile(serverlessYamlPath, serverlessYamlContent); generatedFiles.push({ path: serverlessYamlPath, content: serverlessYamlContent }); // Generate AWS SAM template const samTemplateContent = this.generateSAMTemplate(awsConfig); const samTemplatePath = path.join(config.outputDir, 'template.yaml'); await writeFile(samTemplatePath, samTemplateContent); generatedFiles.push({ path: samTemplatePath, content: samTemplateContent }); // Generate CloudFormation template const cfnTemplateContent = this.generateCloudFormationTemplate(awsConfig); const cfnTemplatePath = path.join(config.outputDir, 'cloudformation.yaml'); await writeFile(cfnTemplatePath, cfnTemplateContent); generatedFiles.push({ path: cfnTemplatePath, content: cfnTemplateContent }); // Generate Lambda handler const handlerContent = this.generateLambdaHandler(awsConfig); const handlerDir = path.join(config.outputDir, 'src'); const handlerPath = path.join(handlerDir, 'handler.js'); await mkdir(handlerDir, { recursive: true }); await writeFile(handlerPath, handlerContent); generatedFiles.push({ path: handlerPath, content: handlerContent }); // Generate package.json for Lambda const packageJsonContent = this.generatePackageJson(awsConfig); const packageJsonPath = path.join(config.outputDir, 'package.json'); await writeFile(packageJsonPath, packageJsonContent); generatedFiles.push({ path: packageJsonPath, content: packageJsonContent }); // Generate deployment script const deployScriptContent = this.generateDeployScript(awsConfig); const deployScriptPath = path.join(config.outputDir, 'deploy.sh'); await writeFile(deployScriptPath, deployScriptContent); generatedFiles.push({ path: deployScriptPath, content: deployScriptContent }); // Optimize assets for AWS Lambda const optimizationResults = await this.optimizeForDeployment(config, config.outputDir); return { success: true, generatedFiles, instructions: this.getDeploymentInstructions(awsConfig), optimizationResults }; } catch (error) { logger.error(`AWS Lambda deployment preparation failed: ${error instanceof Error ? error.message : String(error)}`); return { success: false, error: `AWS Lambda deployment preparation failed: ${error instanceof Error ? error.message : String(error)}`, generatedFiles, instructions: 'An error occurred during AWS Lambda deployment preparation. Please check the logs for details.' }; } } /** * Generate Serverless Framework configuration * @param config AWS Lambda configuration * @returns Serverless Framework configuration YAML */ generateServerlessConfig(config) { const functionName = config.settings?.functionName || 'ordojs-app'; const region = config.settings?.region || 'us-east-1'; const memorySize = config.settings?.memorySize || 1024; const timeout = config.settings?.timeout || 30; const runtime = config.settings?.runtime || 'nodejs18.x'; const stageName = config.settings?.stageName || 'prod'; return `# Serverless Framework configuration for OrdoJS service: ${functionName} provider: name: aws runtime: ${runtime} stage: ${stageName} region: ${region} memorySize: ${memorySize} timeout: ${timeout} environment: ${Object.entries(config.env || {}).map(([key, value]) => ` ${key}: ${value}`).join('\n')} functions: app: handler: src/handler.handler events: - http: path: / method: ANY cors: true - http: path: /{proxy+} method: ANY cors: true plugins: - serverless-offline - serverless-webpack ${config.settings?.useCloudWatch ? ' - serverless-plugin-aws-alerts\n' : ''} ${config.settings?.useXRay ? ' - serverless-plugin-tracing\n' : ''} custom: webpack: webpackConfig: webpack.config.js includeModules: true packager: 'npm' ${config.settings?.useCloudWatch ? ` alerts: stages: - ${stageName} topics: alarm: topic: \${self:service}-\${self:provider.stage}-alerts notifications: - protocol: email endpoint: your-email@example.com alarms: - functionErrors - functionThrottles - functionInvocations - functionDuration ` : ''} `; } /** * Generate AWS SAM template * @param config AWS Lambda configuration * @returns AWS SAM template YAML */ generateSAMTemplate(config) { const functionName = config.settings?.functionName || 'ordojs-app'; const memorySize = config.settings?.memorySize || 1024; const timeout = config.settings?.timeout || 30; const runtime = config.settings?.runtime || 'nodejs18.x'; return `# AWS SAM template for OrdoJS AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: OrdoJS application deployed to AWS Lambda Globals: Function: Timeout: ${timeout} Runtime: ${runtime} MemorySize: ${memorySize} Environment: Variables: ${Object.entries(config.env || {}).map(([key, value]) => ` ${key}: ${value}`).join('\n')} Resources: OrdoJSFunction: Type: AWS::Serverless::Function Properties: CodeUri: ./ Handler: src/handler.handler Events: ApiEvent: Type: Api Properties: Path: / Method: ANY ProxyApiEvent: Type: Api Properties: Path: /{proxy+} Method: ANY ${config.settings?.useXRay ? ' Tracing: Active\n' : ''} ApiGatewayApi: Type: AWS::Serverless::Api Properties: StageName: ${config.settings?.stageName || 'prod'} Cors: AllowMethods: "'*'" AllowHeaders: "'*'" AllowOrigin: "'*'" Outputs: OrdoJSFunction: Description: OrdoJS Lambda Function ARN Value: !GetAtt OrdoJSFunction.Arn ApiGatewayApi: Description: API Gateway endpoint URL Value: !Sub https://\${ApiGatewayApi}.execute-api.\${AWS::Region}.amazonaws.com/${config.settings?.stageName || 'prod'}/ OrdoJSFunctionRole: Description: Implicit IAM Role created for the function Value: !GetAtt OrdoJSFunctionRole.Arn `; } /** * Generate AWS CloudFormation template * @param config AWS Lambda configuration * @returns AWS CloudFormation template YAML */ generateCloudFormationTemplate(config) { const functionName = config.settings?.functionName || 'ordojs-app'; const memorySize = config.settings?.memorySize || 1024; const timeout = config.settings?.timeout || 30; const runtime = config.settings?.runtime || 'nodejs18.x'; const stageName = config.settings?.stageName || 'prod'; return `# AWS CloudFormation template for OrdoJS AWSTemplateFormatVersion: '2010-09-09' Description: OrdoJS application deployed to AWS Lambda Resources: OrdoJSLambdaFunction: Type: AWS::Lambda::Function Properties: FunctionName: ${functionName} Handler: src/handler.handler Role: !GetAtt LambdaExecutionRole.Arn Runtime: ${runtime} MemorySize: ${memorySize} Timeout: ${timeout} Environment: Variables: ${Object.entries(config.env || {}).map(([key, value]) => ` ${key}: ${value}`).join('\n')} ${config.settings?.useXRay ? ' TracingConfig:\n Mode: Active\n' : ''} LambdaExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: 'sts:AssumeRole' ManagedPolicyArns: - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' ${config.settings?.useXRay ? ' - \'arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess\'\n' : ''} ApiGateway: Type: AWS::ApiGateway::RestApi Properties: Name: ${functionName}-api Description: API Gateway for OrdoJS application ApiGatewayRootMethod: Type: AWS::ApiGateway::Method Properties: RestApiId: !Ref ApiGateway ResourceId: !GetAtt ApiGateway.RootResourceId HttpMethod: ANY AuthorizationType: NONE Integration: Type: AWS_PROXY IntegrationHttpMethod: POST Uri: !Sub arn:aws:apigateway:\${AWS::Region}:lambda:path/2015-03-31/functions/\${OrdoJSLambdaFunction.Arn}/invocations ApiGatewayProxyResource: Type: AWS::ApiGateway::Resource Properties: RestApiId: !Ref ApiGateway ParentId: !GetAtt ApiGateway.RootResourceId PathPart: '{proxy+}' ApiGatewayProxyMethod: Type: AWS::ApiGateway::Method Properties: RestApiId: !Ref ApiGateway ResourceId: !Ref ApiGatewayProxyResource HttpMethod: ANY AuthorizationType: NONE Integration: Type: AWS_PROXY IntegrationHttpMethod: POST Uri: !Sub arn:aws:apigateway:\${AWS::Region}:lambda:path/2015-03-31/functions/\${OrdoJSLambdaFunction.Arn}/invocations ApiGatewayDeployment: Type: AWS::ApiGateway::Deployment DependsOn: - ApiGatewayRootMethod - ApiGatewayProxyMethod Properties: RestApiId: !Ref ApiGateway StageName: ${stageName} LambdaApiGatewayPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref OrdoJSLambdaFunction Principal: apigateway.amazonaws.com SourceArn: !Sub arn:aws:execute-api:\${AWS::Region}:\${AWS::AccountId}:\${ApiGateway}/*/*/* Outputs: ApiGatewayUrl: Description: URL of the API Gateway endpoint Value: !Sub https://\${ApiGateway}.execute-api.\${AWS::Region}.amazonaws.com/${stageName}/ LambdaFunctionArn: Description: ARN of the Lambda function Value: !GetAtt OrdoJSLambdaFunction.Arn `; } /** * Generate Lambda handler * @param config AWS Lambda configuration * @returns Lambda handler JavaScript code */ generateLambdaHandler(config) { return `/** * AWS Lambda handler for OrdoJS application */ const serverless = require('serverless-http'); const express = require('express'); const path = require('path'); const fs = require('fs'); // Create Express app const app = express(); // Serve static files app.use(express.static(path.join(__dirname, '../public'))); // API routes app.get('/api/hello', (req, res) => { res.json({ message: 'Hello from OrdoJS on AWS Lambda!', timestamp: new Date().toISOString() }); }); // Handle all other routes with the main HTML file app.get('*', (req, res) => { res.sendFile(path.join(__dirname, '../public/index.html')); }); // Create serverless handler const handler = serverless(app); // Export the handler function module.exports.handler = async (event, context) => { // Enable AWS X-Ray tracing if configured ${config.settings?.useXRay ? ` const AWSXRay = require('aws-xray-sdk'); AWSXRay.captureHTTPsGlobal(require('http')); AWSXRay.captureHTTPsGlobal(require('https')); ` : ''} return await handler(event, context); }; `; } /** * Generate package.json for Lambda * @param config AWS Lambda configuration * @returns package.json content */ generatePackageJson(config) { const functionName = config.settings?.functionName || 'ordojs-app'; return JSON.stringify({ name: functionName, version: '1.0.0', description: 'OrdoJS application deployed to AWS Lambda', main: 'src/handler.js', scripts: { deploy: 'bash ./deploy.sh', 'deploy:serverless': 'serverless deploy', 'deploy:sam': 'sam deploy', 'deploy:cloudformation': 'aws cloudformation deploy --template-file cloudformation.yaml --stack-name ' + functionName }, dependencies: { express: '^4.18.2', 'serverless-http': '^3.2.0', ...(config.settings?.useXRay ? { 'aws-xray-sdk': '^3.5.0' } : {}) }, devDependencies: { 'serverless': '^3.34.0', 'serverless-offline': '^12.0.4', 'serverless-webpack': '^5.13.0', ...(config.settings?.useCloudWatch ? { 'serverless-plugin-aws-alerts': '^1.7.5' } : {}), ...(config.settings?.useXRay ? { 'serverless-plugin-tracing': '^2.0.0' } : {}) } }, null, 2); } /** * Generate deployment script * @param config AWS Lambda configuration * @returns Deployment script content */ generateDeployScript(config) { return `#!/bin/bash # Deployment script for OrdoJS on AWS Lambda echo "Deploying OrdoJS application to AWS Lambda..." # Check if AWS CLI is installed if ! command -v aws &> /dev/null; then echo "AWS CLI is not installed. Please install it first." exit 1 fi # Check if AWS is configured if ! aws sts get-caller-identity &> /dev/null; then echo "AWS CLI is not configured. Please run 'aws configure' first." exit 1 fi # Choose deployment method echo "Choose deployment method:" echo "1. Serverless Framework" echo "2. AWS SAM" echo "3. AWS CloudFormation" read -p "Enter your choice (1-3): " choice case $choice in 1) echo "Deploying with Serverless Framework..." npm install npx serverless deploy ;; 2) echo "Deploying with AWS SAM..." sam build sam deploy --guided ;; 3) echo "Deploying with AWS CloudFormation..." aws cloudformation package \\ --template-file cloudformation.yaml \\ --s3-bucket your-deployment-bucket \\ --output-template-file packaged.yaml aws cloudformation deploy \\ --template-file packaged.yaml \\ --stack-name ${config.settings?.functionName || 'ordojs-app'} \\ --capabilities CAPABILITY_IAM ;; *) echo "Invalid choice. Exiting." exit 1 ;; esac echo "Deployment completed!" `; } /** * Get deployment instructions * @param config AWS Lambda configuration * @returns Deployment instructions */ getDeploymentInstructions(config) { return ` # AWS Lambda Deployment Instructions Your project has been prepared for deployment to AWS Lambda. ## Prerequisites 1. Install the AWS CLI: \`\`\` pip install awscli \`\`\` 2. Configure AWS CLI: \`\`\` aws configure \`\`\` 3. Install required tools based on your preferred deployment method: - Serverless Framework: \`npm install -g serverless\` - AWS SAM: \`pip install aws-sam-cli\` ## Deployment Options You have three options for deploying your application: ### 1. Using Serverless Framework \`\`\` cd ${config.outputDir} npm install npx serverless deploy \`\`\` ### 2. Using AWS SAM \`\`\` cd ${config.outputDir} sam build sam deploy --guided \`\`\` ### 3. Using AWS CloudFormation \`\`\` cd ${config.outputDir} aws cloudformation package \\ --template-file cloudformation.yaml \\ --s3-bucket your-deployment-bucket \\ --output-template-file packaged.yaml aws cloudformation deploy \\ --template-file packaged.yaml \\ --stack-name ${config.settings?.functionName || 'ordojs-app'} \\ --capabilities CAPABILITY_IAM \`\`\` ## Quick Deployment For convenience, a deployment script has been generated. Run: \`\`\` cd ${config.outputDir} chmod +x deploy.sh ./deploy.sh \`\`\` ## Configuration The following files have been generated: - \`serverless.yml\`: Serverless Framework configuration - \`template.yaml\`: AWS SAM template - \`cloudformation.yaml\`: AWS CloudFormation template - \`src/handler.js\`: Lambda handler function - \`package.json\`: Node.js package configuration - \`deploy.sh\`: Deployment script You can customize these files to adjust your deployment settings. ## Environment Variables ${config.env && Object.keys(config.env).length > 0 ? 'The following environment variables have been configured:\n\n' + Object.entries(config.env).map(([key, value]) => `- ${key}: ${value}`).join('\n') : 'No environment variables have been configured. You can add them in the AWS Lambda console or in the configuration files.'} ## Custom Domain ${config.domain ? `Your application will be available at: ${config.domain.name} (after configuring API Gateway custom domain)` : 'You can configure a custom domain in the API Gateway console after deployment.'} `; } /** * Optimize assets for AWS Lambda deployment * @param config Deployment configuration * @param outputDir Output directory * @returns Optimization results */ async optimizeForDeployment(config, outputDir) { logger.info('Optimizing assets for AWS Lambda deployment...'); // Create public directory for static assets const publicDir = path.join(outputDir, 'public'); await mkdir(publicDir, { recursive: true }); // Initialize asset optimizer with AWS Lambda-specific options const optimizer = new AssetOptimizer({ minifyJs: true, minifyCss: true, brotli: false, // AWS Lambda doesn't support Brotli out of the box gzip: true, sizeReport: true, terserOptions: { compress: { passes: 2, drop_console: false, // Keep console logs for CloudWatch drop_debugger: true }, mangle: true, format: { comments: false } } }); // Optimize all assets in the public directory const optimizationResults = await optimizer.optimizeDirectory(publicDir); // Generate and save size report const sizeReport = optimizer.generateSizeReport(optimizationResults); await writeFile(path.join(outputDir, 'size-report.txt'), sizeReport); return optimizationResults; } /** * Get AWS Lambda-specific environment variables * @param config Deployment configuration * @returns Environment variables */ getEnvironmentVariables(config) { const env = { AWS_LAMBDA_FUNCTION_NAME: config.settings?.functionName || 'ordojs-app', NODE_ENV: 'production', ...config.env }; // Add AWS-specific environment variables const awsConfig = config; if (awsConfig.settings?.region) { env.AWS_REGION = awsConfig.settings.region; } return env; } /** * Get AWS Lambda deployment command * @param config Deployment configuration * @returns Deployment command */ getDeployCommand(config) { return `cd ${config.outputDir} && ./deploy.sh`; } } //# sourceMappingURL=aws-lambda-adapter.js.map