├── .eslintignore ├── devcontainer.json ├── .vscode └── settings.json ├── prettier.config.js ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── main.yml ├── src ├── template.ts └── main.ts ├── .eslintrc.js ├── tsconfig.json ├── LICENSE ├── package.json ├── .gitignore └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-dart", 3 | "image": "mcr.microsoft.com/vscode/devcontainers/javascript-node:latest", 4 | "extensions": [] 5 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll": true 4 | }, 5 | "search.exclude": { 6 | "**/node_modules": true, 7 | "**/bower_components": true, 8 | "dist/**/*": true 9 | } 10 | } -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 80, 3 | tabWidth: 2, 4 | useTabs: false, 5 | singleQuote: true, 6 | semi: false, 7 | trailingComma: 'none', 8 | bracketSpacing: true, 9 | arrowParens: 'avoid', 10 | parser: 'typescript' 11 | } 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 💡 3 | about: Suggest a new idea for serverless-rust 4 | --- 5 | 6 | 7 | 8 | ## 💡 Feature description 9 | 10 | 11 | #### 💻 Basic example 12 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | ## What did you implement: 6 | 7 | 10 | 11 | Closes: #xxx 12 | 13 | #### How did you verify your change: 14 | 15 | #### What (if anything) would need to be called out in the CHANGELOG for the next release: -------------------------------------------------------------------------------- /src/template.ts: -------------------------------------------------------------------------------- 1 | function template(strings: any, ...keys: any) { 2 | return function (...values: any) { 3 | const dict = values[values.length - 1] || {} 4 | const result = [strings[0]] 5 | keys.forEach(function (key: any, i: number) { 6 | const value = Number.isInteger(key) ? values[key] : dict[key] 7 | result.push(value, strings[i + 1]) 8 | }) 9 | return result.join('') 10 | } 11 | } 12 | 13 | export const buildSteps = template`cd $(mktemp -d); cp -Rp /app/* .; /usr/lib/dart/bin/pub get; /usr/lib/dart/bin/dart compile exe ${'libPath'}/${'script'}.dart -o bootstrap; mv bootstrap /target/${'script'};` 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 🐛 3 | about: Did something not work as expected? 4 | --- 5 | 6 | 7 | 8 | ## 🐛 Bug description 9 | Describe your issue in detail. 10 | 11 | #### 🤔 Expected Behavior 12 | 13 | 14 | #### 👟 Steps to reproduce 15 | 16 | 17 | #### 🌍 Your environment 18 | 19 | 20 | serverless version: 21 | 22 | rust-plugin version: 23 | 24 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | extends: [ 4 | 'plugin:@typescript-eslint/recommended', 5 | 'prettier/@typescript-eslint', 6 | 'plugin:prettier/recommended' 7 | ], 8 | settings: { 9 | react: { 10 | version: 'detect' // Tells eslint-plugin-react to automatically detect the version of React to use 11 | } 12 | }, 13 | globals: { 14 | Atomics: 'readonly', 15 | SharedArrayBuffer: 'readonly' 16 | }, 17 | parserOptions: { 18 | ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features 19 | sourceType: 'module', // Allows for the use of imports 20 | ecmaFeatures: { 21 | jsx: true // Allows for the parsing of JSX 22 | } 23 | }, 24 | rules: {} 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - 'v**' 9 | pull_request: 10 | branches: 11 | - main 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v2 18 | - name: Install 19 | run: npm ci 20 | - name: Lint 21 | run: npm run lint 22 | publish: 23 | needs: [test] 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v2 28 | - name: Install 29 | run: npm ci 30 | - name: Build 31 | run: npm run build 32 | - name: Publish 33 | if: startsWith(github.ref, 'refs/tags/') 34 | run: | 35 | npm config set //registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN 36 | npm publish 37 | env: 38 | CI: true 39 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 40 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 4 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 5 | "outDir": "./lib", /* Redirect output structure to the directory. */ 6 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 7 | "strict": true, /* Enable all strict type-checking options. */ 8 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 9 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 10 | }, 11 | "exclude": [ 12 | "node_modules", 13 | "**/*.test.ts" 14 | ] 15 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Sebastian Doell 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. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-dart", 3 | "version": "0.0.2-beta.9", 4 | "description": "Serverless framework plugin for Dart applications", 5 | "main": "lib/main.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "lint": "npx eslint --fix", 9 | "watch": "tsc -w" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/katallaxie/serverless-dart.git" 14 | }, 15 | "keywords": [ 16 | "dart", 17 | "aws", 18 | "serverless", 19 | "lambda" 20 | ], 21 | "author": { 22 | "email": "sebastian@katallaxie.me", 23 | "name": "Sebastian Doell" 24 | }, 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/katallaxie/serverless-dart/issues" 28 | }, 29 | "homepage": "https://github.com/katallaxie/serverless-dart#readme", 30 | "dependencies": { 31 | "fs-copy-file-sync": "^1.1.1", 32 | "fs-extra": "^9.1.0", 33 | "serverless": "^2.25.1", 34 | "yazl": "^2.5.1" 35 | }, 36 | "devDependencies": { 37 | "@types/jest": "^26.0.20", 38 | "@types/node": "^14.14.28", 39 | "@types/serverless": "^1.78.20", 40 | "@types/yazl": "^2.4.2", 41 | "@typescript-eslint/eslint-plugin": "^4.15.1", 42 | "@typescript-eslint/parser": "^4.15.1", 43 | "eslint": "^7.20.0", 44 | "eslint-config-prettier": "^7.2.0", 45 | "eslint-plugin-github": "^4.1.1", 46 | "eslint-plugin-jest": "^24.1.5", 47 | "eslint-plugin-prettier": "^3.3.1", 48 | "jest": "^26.6.3", 49 | "jest-circus": "^26.6.3", 50 | "prettier": "^2.2.1", 51 | "ts-jest": "^26.5.1", 52 | "typescript": "^4.1.5" 53 | }, 54 | "files": [ 55 | "lib/main.js", 56 | "lib/template.js", 57 | "package.json", 58 | "README.md" 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | node_modules 3 | 4 | # Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | .env.test 71 | 72 | # parcel-bundler cache (https://parceljs.org/) 73 | .cache 74 | 75 | # next.js build output 76 | .next 77 | 78 | # nuxt.js build output 79 | .nuxt 80 | 81 | # vuepress build output 82 | .vuepress/dist 83 | 84 | # Serverless directories 85 | .serverless/ 86 | 87 | # FuseBox cache 88 | .fusebox/ 89 | 90 | # DynamoDB Local files 91 | .dynamodb/ 92 | 93 | # OS metadata 94 | .DS_Store 95 | Thumbs.db 96 | 97 | # Ignore built ts files 98 | __tests__/runner/* 99 | lib/**/* 100 | 101 | # Intellij IDEA metadata 102 | .idea/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | ⚡ 🎯 3 |
4 | 5 |

6 | serverless-dart 7 |

8 | 9 |

10 | A ⚡ Serverless framework ⚡ plugin for Dart applications 11 |

12 | 13 |
14 | 15 | GitHub actions build badge 16 | 17 | 18 | npm release badge 19 | 20 |
21 | 22 |
23 | 24 | ## 📦 Install 25 | 26 | Install the plugin inside your serverless project with npm. 27 | 28 | ```sh 29 | $ npm i -D serverless-dart 30 | ``` 31 | 32 | 💡 The `-D` flag adds it to your development dependencies in npm speak 33 | 34 | 💡 This plugin assumes you are using [Dart Runtime for AWS Lambda](https://github.com/awslabs/aws-lambda-dart-runtime) coding your applications. 35 | 36 | Add the following to your serverless project's `serverless.yml` file 37 | 38 | ```yaml 39 | service: hello 40 | provider: 41 | name: aws 42 | runtime: dart 43 | plugins: 44 | # this registers the plugin 45 | # with serverless 46 | - serverless-dart 47 | # creates one artifact for each function 48 | package: 49 | individually: true 50 | functions: 51 | hello: 52 | # the first part of the handler refers to the script lib/main.dart. 53 | # main.hello identifies the handler to execute in the Dart runtime. 54 | # The runtime supports multiple handlers 55 | # The plugin is smart to not rebuild those scripts with multiple handlers. 56 | handler: main.hello 57 | events: 58 | - http: 59 | path: /hello 60 | method: GET 61 | ``` 62 | 63 | > 💡 The Dart Runtime for AWS Lambda requires a binary named `bootstrap`. This plugin renames the binary that is generated to `bootstrap` for you and zips that file. 64 | 65 | The default behavior is to build your Lambda inside a Docker container. Make sure you [get Docker](https://docs.docker.com/get-docker/). 66 | 67 | ## 🤸 Usage 68 | 69 | Every [serverless workflow command](https://serverless.com/framework/docs/providers/aws/guide/workflow/) should work out of the box. 70 | 71 | ### package your Lambdas 72 | 73 | ```sh 74 | $ npx serverless deploy 75 | ``` 76 | 77 | ### deploy your Lambdas 78 | 79 | ```sh 80 | $ npx serverless deploy 81 | ``` 82 | 83 | ## 👨‍💻 Development 84 | 85 | Clone the repository 86 | 87 | ```bash 88 | $ git clone https://github.com/katallaxie/serverless-dart 89 | ``` 90 | 91 | Link the package 92 | 93 | ```bash 94 | $ npm link 95 | ``` 96 | 97 | Link the package to your testing environment 98 | 99 | ```bash 100 | $ npm link serverless-dart 101 | ``` 102 | 103 | ## 📃 License 104 | 105 | [Apache 2.0](/LICENSE) 106 | 107 | We :blue_heart: Dart. 108 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import Serverless, { Options } from 'serverless' 2 | import * as process from 'process' 3 | import * as child from 'child_process' 4 | import * as path from 'path' 5 | import * as fs from 'fs' 6 | import Yazl from 'yazl' 7 | import { buildSteps } from './template' 8 | 9 | interface Custom { 10 | dockerImage: string 11 | dockerTag: string 12 | dockerPath: string 13 | libPath: string 14 | } 15 | 16 | class DartPlugin { 17 | private readonly runtime = 'dart' 18 | private readonly sanitizedRuntime = 'provided.al2' 19 | 20 | public servicePath: string 21 | public hooks: { [index: string]: unknown } 22 | public custom: Custom 23 | public srcPath: string 24 | public buildPath: string 25 | public stagePath: string 26 | 27 | constructor(public serverless: Serverless, public options: Options) { 28 | this.servicePath = this.serverless.config.servicePath || '' 29 | this.hooks = { 30 | 'before:package:createDeploymentArtifacts': this.build.bind(this), 31 | 'before:deploy:function:packageFunction': this.build.bind(this), 32 | 'before:offline:start': this.build.bind(this), 33 | 'before:offline:start:init': this.build.bind(this) 34 | } 35 | this.custom = { 36 | ...{ dockerImage: 'dart', dockerTag: 'latest', libPath: 'lib' }, 37 | ...this.serverless.service?.custom?.dart 38 | } 39 | 40 | this.srcPath = path.resolve(this.custom.dockerPath || this.servicePath) 41 | this.buildPath = path.resolve(this.srcPath, 'target') 42 | this.stagePath = path.resolve(this.buildPath, this.options.stage || 'dev') 43 | 44 | const service = this.serverless.service as any 45 | service.package.excludeDevDependencies = false 46 | } 47 | 48 | public funcs() { 49 | return this.options.function 50 | ? [this.options.function] 51 | : this.serverless.service.getAllFunctions() 52 | } 53 | 54 | public dockerBuildArgs(script: string) { 55 | const defaultArgs = [ 56 | 'run', 57 | '-v', 58 | `${this.srcPath}:/app`, 59 | '-v', 60 | `${this.stagePath}:/target`, 61 | '-i' 62 | ] 63 | const imageArgs = [ 64 | `${this.custom.dockerImage}:${this.custom.dockerTag}`, 65 | 'sh', 66 | '-c', 67 | buildSteps({ ...this.custom, ...{ script } }) 68 | ] 69 | 70 | return [...defaultArgs, ...imageArgs] 71 | } 72 | 73 | public dockerBuild(script: string) { 74 | const cli = process.env['SLS_DOCKER_CLI'] || 'docker' 75 | const args = [...this.dockerBuildArgs(script)] 76 | 77 | this.serverless.cli.log(`Running containerized build ...`) 78 | 79 | return child.spawnSync(cli, args, { 80 | stdio: ['ignore', process.stdout, process.stderr] 81 | }) 82 | } 83 | 84 | public cleanup() { 85 | return fs.rmdirSync(this.stagePath, { recursive: true }) 86 | } 87 | 88 | public mkdir() { 89 | return fs.mkdirSync(this.stagePath, { recursive: true }) 90 | } 91 | 92 | public run() { 93 | const service = this.serverless.service 94 | 95 | this.cleanup() // first clean-up 96 | this.mkdir() // mkdir 97 | 98 | return this.funcs().reduce((prev, curr) => { 99 | const func = service.getFunction( 100 | curr 101 | ) as Serverless.FunctionDefinitionHandler 102 | const [script] = func.handler.split('.') 103 | const runtime = func.runtime || service.provider.runtime 104 | const bootstrap = path.resolve(this.stagePath, `${script}`) 105 | const artifact = path.resolve(this.stagePath, `${script}.zip`) 106 | 107 | if (runtime != this.runtime) { 108 | return prev || false 109 | } 110 | 111 | this.serverless.cli.log(`Building Dart ${func.name} func...`) 112 | 113 | if (!fs.existsSync(artifact)) { 114 | const { error, status } = this.dockerBuild(script) 115 | if (error || (status && status > 0)) { 116 | this.serverless.cli.log( 117 | `Dart build encountered an error: ${error} ${status}.` 118 | ) 119 | throw error 120 | } 121 | 122 | try { 123 | this.package(bootstrap, artifact) 124 | } catch (err) { 125 | this.serverless.cli.log(`Error zipping artifact ${err}`) 126 | 127 | throw new Error(err) 128 | } 129 | } 130 | 131 | func.package = func.package || { include: [], exclude: [] } 132 | func.package = { ...func?.package, artifact } 133 | 134 | return true 135 | }, false) 136 | } 137 | 138 | public async package(bootstrap: string, target: string) { 139 | const zipFile = new Yazl.ZipFile() 140 | const output = fs.createWriteStream(target) 141 | 142 | output.on('error', function (err) { 143 | throw err 144 | }) 145 | 146 | zipFile.addFile(bootstrap, 'bootstrap', { mode: 0o755, compress: false }) 147 | zipFile.outputStream.pipe(output) 148 | zipFile.end() 149 | } 150 | 151 | public build() { 152 | const service = this.serverless.service 153 | 154 | if (service.provider.name != 'aws') { 155 | return 156 | } 157 | 158 | if (!this.run()) { 159 | throw new Error( 160 | `Error: no Dart functions found. Use 'runtime: ${this.runtime}' in global or function configuration to use this plugin` 161 | ) 162 | } 163 | 164 | if (service.provider.runtime === this.runtime) { 165 | service.provider.runtime = this.sanitizedRuntime 166 | } 167 | } 168 | } 169 | 170 | module.exports = DartPlugin 171 | --------------------------------------------------------------------------------