@ordojs/cli
Version:
Command-line interface for OrdoJS framework
683 lines (626 loc) • 22.5 kB
JavaScript
/**
* @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