├── bun.lockb ├── tsconfig.json ├── index.ts ├── package.json ├── LICENSE ├── logSymbols.mjs ├── README.md ├── .gitignore └── deploy.mjs /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TotalTechGeek/elysia-lambda/HEAD/bun.lockb -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "ESNext" 5 | ], 6 | "module": "esnext", 7 | "target": "esnext", 8 | "moduleResolution": "bundler", 9 | "moduleDetection": "force", 10 | "strict": true, 11 | "downlevelIteration": true, 12 | "skipLibCheck": true, 13 | "jsx": "preserve", 14 | "allowSyntheticDefaultImports": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "allowJs": true, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from 'elysia' 2 | 3 | let current: Elysia | null = null 4 | export const lambda = (/* Potential Configuration Here */) => (app: Elysia) => { 5 | return app 6 | } 7 | 8 | /** 9 | * @private 10 | * @hidden 11 | */ 12 | export const hijack = () => (app: Elysia) => { 13 | app.listen = () => { 14 | // @ts-ignore 15 | app.server = { 16 | ...app.server, 17 | hostname: 'lambda', 18 | port: -1, 19 | } 20 | return app 21 | } 22 | current = app 23 | return app 24 | } 25 | 26 | export function instance(): Elysia { 27 | if (!current) throw new Error('Elysia instance not found') 28 | return current 29 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elysia-lambda", 3 | "author": "Jesse Daniel Mitchell", 4 | "license": "MIT", 5 | "version": "0.0.7", 6 | "repository": { 7 | "type": "https+git", 8 | "url": "https://github.com/TotalTechGeek/elysia-lambda" 9 | }, 10 | "description": "A plugin for Elysia to deploy to AWS Lambda", 11 | "module": "index.ts", 12 | "type": "module", 13 | "devDependencies": {}, 14 | "peerDependencies": { 15 | "typescript": "^5.0.0" 16 | }, 17 | "dependencies": { 18 | "@aws-sdk/client-lambda": "^3.322.0", 19 | "adm-zip": "^0.5.10", 20 | "chalk": "^5.2.0", 21 | "commander": "^10.0.1", 22 | "deep-equal": "^2.2.1", 23 | "elysia": "^0.4.13", 24 | "inquirer": "^9.2.0", 25 | "superagent": "^8.0.9", 26 | "yaml": "^2.2.2" 27 | }, 28 | "bin": { 29 | "elysia-lambda": "./deploy.mjs" 30 | } 31 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Jesse Mitchell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /logSymbols.mjs: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | 3 | /** 4 | * Copied from: https://github.com/sindresorhus/is-unicode-supported 5 | * Replicated to avoid bundling issues & allow for stricter control. 6 | * MIT License 7 | * Copyright (c) Sindre Sorhus (https://sindresorhus.com) * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: * 9 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. * 10 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | */ 12 | export function isUnicodeSupported () { 13 | if (process.platform !== 'win32') { 14 | return process.env.TERM !== 'linux' // Linux console (kernel) 15 | } 16 | 17 | return Boolean(process.env.CI) || 18 | Boolean(process.env.WT_SESSION) || // Windows Terminal 19 | Boolean(process.env.TERMINUS_SUBLIME) || // Terminus (<0.2.27) 20 | process.env.ConEmuTask === '{cmd::Cmder}' || // ConEmu and cmder 21 | process.env.TERM_PROGRAM === 'Terminus-Sublime' || 22 | process.env.TERM_PROGRAM === 'vscode' || 23 | process.env.TERM === 'xterm-256color' || 24 | process.env.TERM === 'alacritty' || 25 | process.env.TERMINAL_EMULATOR === 'JetBrains-JediTerm' 26 | } 27 | 28 | const main = { 29 | info: chalk.blue('ℹ'), 30 | success: chalk.green('✔'), 31 | warning: chalk.yellow('⚠'), 32 | error: chalk.red('✖') 33 | } 34 | 35 | const fallback = { 36 | info: chalk.blue('i'), 37 | success: chalk.green('√'), 38 | warning: chalk.yellow('‼'), 39 | error: chalk.red('×') 40 | } 41 | 42 | const logSymbols = isUnicodeSupported() ? main : fallback 43 | export default logSymbols -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # elysia-lambda 2 | 3 | This plugin for Elysia attempts to make it easier to deploy your Elysia project to AWS Lambda. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | bun install elysia-lambda 9 | ``` 10 | 11 | In your application, you will add the following to your code: 12 | 13 | ```typescript 14 | import { Elysia } from 'elysia' 15 | import { lambda } from 'elysia-lambda' 16 | 17 | const app = new Elysia() 18 | .use(lambda()) 19 | ``` 20 | 21 | This is all you need to do to install the plugin into your codebase. 22 | 23 | ## Usage 24 | 25 | We strongly recommend running the init command, 26 | 27 | ```bash 28 | bunx elysia-lambda --init 29 | ``` 30 | 31 | This will guide you through the process of setting up your project for deployment to AWS Lambda, and setting up the official Bun layer. 32 | 33 | This application expects that you are configured with sufficient AWS CLI Permissions. 34 | 35 | After the init command has been performed, you will have a yaml file for your lambda deployment and a `deploy` script added to your `package.json`. 36 | 37 | 38 | The configuration file will look like the following: 39 | 40 | ```yaml 41 | deploy: src/index.ts 42 | name: ElysiaLambda 43 | region: us-east-1 44 | arch: arm64 45 | description: An Elysia Lambda app. 46 | role: arn:aws:iam::672112969134:role/TestLambda 47 | layers: 48 | - arn:aws:lambda:us-east-1:672112969134:layer:bun:8 49 | environment: 50 | SomeEnvVariable: SomeValue 51 | memory: 256 52 | ``` 53 | 54 | Alternatively, it is possible to run the tool exclusively from the CLI: 55 | 56 | ```txt 57 | Usage: elysia-lambda [options] 58 | 59 | Options: 60 | --init Begin the setup to deploy to Lambda. 61 | --deploy Deploy to AWS 62 | --build Build for AWS 63 | -o, --out Where to output the build zip. 64 | -r, --region AWS region to deploy to. 65 | --role AWS role to attach to the lambda. 66 | --name Name of the lambda. 67 | --layers AWS layers to attach to the lambda. 68 | -c , --config Path to a yaml/json config file. 69 | --description Description of the lambda. 70 | --memory Memory to allocate to the lambda, in MiB. (default: "128") 71 | --arch AWS architecture to deploy to. (choices: "arm64", "x64") 72 | -h, --help display help for command 73 | ``` 74 | 75 | After adding a deploy script to your `package.json`, you can run the following command to deploy your application: 76 | 77 | ```bash 78 | bun run deploy 79 | ``` 80 | 81 | This will build your application and deploy it to AWS Lambda. 82 | 83 | ### Demo 84 | 85 | https://user-images.githubusercontent.com/2261916/235324906-17f1fdcf-d61a-487e-b498-595e7f94c836.mp4 86 | 87 | ### Notes 88 | 89 | The deployment script requires Bun versions newer than `0.6.0`. 90 | 91 | I may add support for ESBuild & the Elysia Node.js Polyfills in the future. 92 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | 15 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 16 | 17 | # Runtime data 18 | 19 | pids 20 | _.pid 21 | _.seed 22 | \*.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | 30 | coverage 31 | \*.lcov 32 | 33 | # nyc test coverage 34 | 35 | .nyc_output 36 | 37 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 38 | 39 | .grunt 40 | 41 | # Bower dependency directory (https://bower.io/) 42 | 43 | bower_components 44 | 45 | # node-waf configuration 46 | 47 | .lock-wscript 48 | 49 | # Compiled binary addons (https://nodejs.org/api/addons.html) 50 | 51 | build/Release 52 | 53 | # Dependency directories 54 | 55 | node_modules/ 56 | jspm_packages/ 57 | 58 | # Snowpack dependency directory (https://snowpack.dev/) 59 | 60 | web_modules/ 61 | 62 | # TypeScript cache 63 | 64 | \*.tsbuildinfo 65 | 66 | # Optional npm cache directory 67 | 68 | .npm 69 | 70 | # Optional eslint cache 71 | 72 | .eslintcache 73 | 74 | # Optional stylelint cache 75 | 76 | .stylelintcache 77 | 78 | # Microbundle cache 79 | 80 | .rpt2_cache/ 81 | .rts2_cache_cjs/ 82 | .rts2_cache_es/ 83 | .rts2_cache_umd/ 84 | 85 | # Optional REPL history 86 | 87 | .node_repl_history 88 | 89 | # Output of 'npm pack' 90 | 91 | \*.tgz 92 | 93 | # Yarn Integrity file 94 | 95 | .yarn-integrity 96 | 97 | # dotenv environment variable files 98 | 99 | .env 100 | .env.development.local 101 | .env.test.local 102 | .env.production.local 103 | .env.local 104 | 105 | # parcel-bundler cache (https://parceljs.org/) 106 | 107 | .cache 108 | .parcel-cache 109 | 110 | # Next.js build output 111 | 112 | .next 113 | out 114 | 115 | # Nuxt.js build / generate output 116 | 117 | .nuxt 118 | dist 119 | 120 | # Gatsby files 121 | 122 | .cache/ 123 | 124 | # Comment in the public line in if your project uses Gatsby and not Next.js 125 | 126 | # https://nextjs.org/blog/next-9-1#public-directory-support 127 | 128 | # public 129 | 130 | # vuepress build output 131 | 132 | .vuepress/dist 133 | 134 | # vuepress v2.x temp and cache directory 135 | 136 | .temp 137 | .cache 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.\* 170 | -------------------------------------------------------------------------------- /deploy.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import path from 'path' 3 | import fs from 'fs' 4 | import { execSync } from 'child_process' 5 | import AdmZip from 'adm-zip' 6 | 7 | // Only packages not compatible with Bun directly 8 | import * as AWS from '@aws-sdk/client-lambda' 9 | import superagent from 'superagent' 10 | import { Option, program } from 'commander' 11 | import inquirer from 'inquirer' 12 | import chalk from 'chalk' 13 | import { parse, stringify } from 'yaml' 14 | import equal from 'deep-equal' 15 | import logSymbols from './logSymbols.mjs' 16 | 17 | program 18 | .option('--init', 'Begin the setup to deploy to Lambda.') 19 | .option('--deploy ', 'Deploy to AWS') 20 | .option('--build ', 'Build for AWS') 21 | .option('-o, --out ', 'Where to output the build zip.') 22 | .option('-r, --region ', 'AWS region to deploy to.') 23 | .option('--role ', 'AWS role to attach to the lambda.') 24 | .option('--name ', 'Name of the lambda.') 25 | .option('--layers ', 'AWS layers to attach to the lambda.') 26 | .option('-c , --config ', 'Path to a yaml/json config file.') 27 | .option('--description ', 'Description of the lambda.') 28 | .option('--memory ', 'Memory to allocate to the lambda, in MiB.', '128') 29 | .addOption(new Option('--arch ', 'AWS architecture to deploy to.',).choices(['arm64', 'x86_64'])) 30 | 31 | program.parse(process.argv) 32 | 33 | const options = program.opts() 34 | 35 | if (options.init) { 36 | console.log(chalk.blue('Welcome to the Elysia Lambda Deployer!')) 37 | console.log(chalk.blue('This will walk you through the setup to deploy to AWS Lambda.')) 38 | 39 | let layer = '' 40 | 41 | // Ask for the region 42 | const { region } = await inquirer.prompt({ 43 | type: 'input', 44 | name: 'region', 45 | message: 'What region do you want to deploy to?', 46 | default: 'us-east-1' 47 | }) 48 | 49 | // Ask for the architecture 50 | const { architecture } = await inquirer.prompt({ 51 | type: 'list', 52 | name: 'architecture', 53 | message: 'What architecture do you want to deploy to?', 54 | choices: ['arm64', 'x86_64'], 55 | default: 'x86_64' 56 | }) 57 | 58 | // Yes / No; have you created the bun layer yet? 59 | const { bunLayer } = await inquirer.prompt({ 60 | type: 'confirm', 61 | name: 'bunLayer', 62 | message: 'Have you created the Bun layer yet?', 63 | default: false 64 | }) 65 | 66 | if (!bunLayer) { 67 | console.log(chalk.blue('Ok! We will create that layer together.')) 68 | 69 | // Ask for which version of Bun to use, set a User-Agent header 70 | const { body } = await superagent.get('https://api.github.com/repos/oven-sh/bun/tags') 71 | .set('User-Agent', 'elysia-lambda-deployer') 72 | const versions = body.map(tag => tag.name).filter(i => i.startsWith('bun-v')).map(i => i.substring(5)) 73 | 74 | // order the versions 75 | // this is copilot code 76 | versions.sort((a, b) => { 77 | const aParts = a.split('.') 78 | const bParts = b.split('.') 79 | for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) { 80 | if (aParts[i] === undefined) return -1 81 | if (bParts[i] === undefined) return 1 82 | if (parseInt(aParts[i]) > parseInt(bParts[i])) return -1 83 | if (parseInt(aParts[i]) < parseInt(bParts[i])) return 1 84 | } 85 | return 0 86 | }) 87 | 88 | const { bunVersion } = await inquirer.prompt({ 89 | type: 'list', 90 | name: 'bunVersion', 91 | message: 'What version of Bun do you want to use?', 92 | choices: ['latest', 'canary', ...versions], 93 | default: 'latest' 94 | }) 95 | 96 | execSync('git clone https://github.com/oven-sh/bun') 97 | 98 | const output = execSync(`cd bun/packages/bun-lambda && bun install && bun run publish-layer --arch ${architecture === 'x86_64' ? 'x64' : 'aarch64'} --region ${region} --release ${bunVersion}`) 99 | 100 | // use a regex to get the arn of the layer, starts with "arn:aws:lambda:" all the way to the next whitespace character 101 | layer = output.toString().replace(/\r?\n/g, ' ').match(/arn:aws:lambda:[^\s]+/)[0] 102 | 103 | console.log(chalk.blue(`Created the layer with ARN ${layer}`)) 104 | 105 | // delete the bun folder 106 | fs.rm('bun', { recursive: true }, () => {}) 107 | } 108 | else { 109 | // Ask for the ARN of the layer 110 | layer = (await inquirer.prompt({ 111 | type: 'input', 112 | name: 'layerArn', 113 | message: 'What is the ARN of the Bun layer?', 114 | })).layerArn 115 | } 116 | 117 | // Ask for the ARN of the role 118 | const { roleArn } = await inquirer.prompt({ 119 | type: 'input', 120 | name: 'roleArn', 121 | message: 'What is the ARN of the role you want to use?', 122 | }) 123 | 124 | 125 | 126 | // Ask for the name 127 | const { name } = await inquirer.prompt({ 128 | type: 'input', 129 | name: 'name', 130 | message: 'What do you want to name the lambda?', 131 | default: 'elysia-lambda' 132 | }) 133 | 134 | // Ask for the memory 135 | const { memory } = await inquirer.prompt({ 136 | type: 'input', 137 | name: 'memory', 138 | message: 'How much memory do you want to allocate to the lambda, in MiB?', 139 | default: '128' 140 | }) 141 | 142 | // Ask for the description 143 | const { description } = await inquirer.prompt({ 144 | type: 'input', 145 | name: 'description', 146 | message: 'What do you want the description to be?', 147 | default: 'Elysia Lambda' 148 | }) 149 | 150 | // Ask for the entry point 151 | const { entry } = await inquirer.prompt({ 152 | type: 'input', 153 | name: 'entry', 154 | message: 'What is the entry point?', 155 | default: 'index.js' 156 | }) 157 | 158 | // Ask to name the yaml file 159 | const { yamlName } = await inquirer.prompt({ 160 | type: 'input', 161 | name: 'yamlName', 162 | message: 'What do you want to name the yaml file?', 163 | default: 'elysia-lambda.yaml' 164 | }) 165 | 166 | // Output the configuration 167 | fs.writeFileSync( 168 | yamlName, 169 | stringify({ 170 | deploy: entry, 171 | name, 172 | region, 173 | memory, 174 | arch: architecture, 175 | description, 176 | role: roleArn, 177 | layers: [layer] 178 | }) 179 | ) 180 | 181 | 182 | try { 183 | const pkg = JSON.parse(fs.readFileSync('package.json')) 184 | 185 | // if there is a "deploy" script in the package, 186 | if (!pkg.scripts.deploy) { 187 | // ask if they want to add a deploy script 188 | const { addDeployScript } = await inquirer.prompt({ 189 | type: 'confirm', 190 | name: 'addDeployScript', 191 | message: 'Do you want to add a deploy script to your package.json?', 192 | default: true 193 | }) 194 | if (addDeployScript) { 195 | pkg.scripts.deploy = `elysia-lambda --config ${yamlName}` 196 | fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2)) 197 | } 198 | else { 199 | console.log(logSymbols.success, chalk.green(`Run: elysia-lambda --config ${yamlName}`)) 200 | } 201 | } 202 | } 203 | catch (err) { 204 | console.log(logSymbols.success, chalk.green(`Run: elysia-lambda --config ${yamlName}`)) 205 | } 206 | 207 | process.exit(0) 208 | } 209 | 210 | 211 | if (options.config) Object.assign(options, parse(fs.readFileSync(path.resolve(process.cwd(), options.config), 'utf8'))) 212 | 213 | if (!options.deploy && !options.build) { 214 | console.error(chalk.red('No command specified. Use --deploy or --build.')) 215 | process.exit(1) 216 | } 217 | 218 | if (options.deploy && options.build) { 219 | console.error(chalk.red('Cannot use --deploy and --build together.')) 220 | process.exit(1) 221 | } 222 | 223 | if (options.build && !options.out) { 224 | console.error(chalk.red('Must specify --out when using --build.')) 225 | process.exit(1) 226 | } 227 | 228 | if (options.deploy) { 229 | let err = '' 230 | if (!options.role) err += '- Must specify --role for the lambda when using --deploy.\n' 231 | if (!options.name) err += '- Must specify --name for the lambda when using --deploy.\n' 232 | if (!options.region) err += '- Must specify --region for the lambda when using --deploy.\n' 233 | if (options.layer) options.layers = options.layer 234 | if (!options.layers) err += '- Must specify at least one layer for the lambda with --layers when using --deploy.\n' 235 | if (typeof options.layers === 'string') options.layers = [options.layers] 236 | if (!options.arch) err += '- Must specify --arch for the lambda when using --deploy.\n' 237 | if (options.arch === 'x64') options.arch = 'x86_64' // minor correction from previous configs. 238 | if (err) { 239 | console.error(chalk.red(`Some deployment issues were encountered:\n${err}`)) 240 | process.exit(1) 241 | } 242 | } 243 | 244 | 245 | 246 | const file = path.resolve(process.cwd(), options.build || options.deploy) 247 | const now = new Date().getTime() 248 | const mutatedFile = path.resolve(process.cwd(), options.build || options.deploy, '../', now + '.ts') 249 | 250 | fs.writeFileSync(mutatedFile, fs.readFileSync(file, 'utf8').replace(/([, (])lambda([,() ])/g, i => i[0] + 'hijack' + i[i.length - 1])) 251 | 252 | const script = ` 253 | import { instance } from 'elysia-lambda' 254 | import '${mutatedFile}' // the entry point import 255 | export default { 256 | js: instance().innerHandle || instance().fetch 257 | }` 258 | 259 | 260 | const out = path.resolve(process.cwd(), now + '.js') 261 | fs.writeFileSync(out, script) 262 | 263 | 264 | const bundleFile = path.resolve(process.cwd(), 'bundle.' + now + '.js') 265 | execSync(`bun build --minify --outfile=${bundleFile} --external @elysiajs/fn --target=bun ${out}`) 266 | 267 | fs.unlinkSync(out) 268 | fs.unlinkSync(mutatedFile) 269 | 270 | const zip = new AdmZip() 271 | zip.addLocalFile(bundleFile, '', 'index.js') 272 | fs.unlinkSync(bundleFile) 273 | 274 | const timeout = async (ms) => new Promise(resolve => setTimeout(resolve, ms)) 275 | 276 | const sizes = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'] 277 | 278 | /** 279 | * Performs the deployment to AWS. 280 | */ 281 | async function deploy () { 282 | zip.writeZip(`bundle.${now}.zip`) 283 | 284 | // Get size of zip 285 | const stats = fs.statSync(`bundle.${now}.zip`) 286 | let size = stats.size 287 | let div = 0 288 | while (size > 1024) { 289 | size /= 1024 290 | div++ 291 | } 292 | 293 | const lambda = new AWS.Lambda({ region: 'us-east-1' }) 294 | 295 | // check if lambda exists 296 | const exists = await lambda.getFunction({ 297 | FunctionName: options.name 298 | }).catch(() => false) 299 | 300 | if (!exists) { 301 | console.log(chalk.magenta(`Deploying "${options.name}"!`)) 302 | await lambda.createFunction({ 303 | FunctionName: options.name, 304 | Runtime: 'provided.al2', 305 | Handler: 'index.js', 306 | Layers: options.layers, 307 | Description: options.description || 'Elysia Lambda', 308 | Code: { 309 | ZipFile: fs.readFileSync(`bundle.${now}.zip`) 310 | }, 311 | Role: options.role, 312 | Architectures: [options.arch], 313 | ...(options.environment && { 314 | Environment: { 315 | Variables: options.environment 316 | } 317 | }), 318 | MemorySize: +options.memory || 128, 319 | }) 320 | } 321 | else { 322 | console.log(chalk.magenta(`Lambda "${options.name}" found. Updating code.`)) 323 | 324 | const result = await lambda.updateFunctionCode({ 325 | FunctionName: options.name, 326 | ZipFile: fs.readFileSync(`bundle.${now}.zip`), 327 | }) 328 | 329 | // check if anything is different 330 | if (!equal({ 331 | Architectures: [options.arch], 332 | Handler: 'index.js', 333 | Role: options.role, 334 | MemorySize: +options.memory || 128, 335 | Layers: options.layers, 336 | ...(options.environment && { 337 | Environment: { 338 | Variables: options.environment 339 | } 340 | }) 341 | }, { 342 | Architectures: result.Architectures, 343 | Handler: result.Handler, 344 | Role: result.Role, 345 | MemorySize: result.MemorySize, 346 | Layers: result.Layers.map(i => i.Arn), 347 | ...(result.Environment && {Environment: result.Environment}), 348 | })) { 349 | console.log(chalk.magenta(`Changes detected. Updating configuration.`)) 350 | 351 | // I tried to use AWS.waitUntilFunctionUpdatedV2, but it would pause indefinitely. 352 | await timeout(2000) 353 | 354 | await lambda.updateFunctionConfiguration({ 355 | FunctionName: options.name, 356 | Layers: options.layers, 357 | Description: options.description || 'Elysia Lambda', 358 | Role: options.role, 359 | Architectures: [options.arch], 360 | Handler: 'index.js', 361 | ...(options.environment && { 362 | Environment: { 363 | Variables: options.environment 364 | } 365 | }), 366 | MemorySize: +options.memory || 128, 367 | }) 368 | } 369 | } 370 | 371 | console.log(chalk.magenta('Bundle Size: ', chalk.yellow(size.toFixed(2) + sizes[div] + 'B'))) 372 | console.log(logSymbols.success, chalk.green('Success!')) 373 | fs.unlinkSync(`bundle.${now}.zip`) 374 | } 375 | 376 | if (options.deploy) await deploy() 377 | if (options.build) { 378 | zip.writeZip(options.out) 379 | console.log(logSymbols.success, chalk.green('Zip for Lambda built!')) 380 | } 381 | 382 | 383 | --------------------------------------------------------------------------------