├── .babelrc ├── .editorconfig ├── .eslintrc ├── .github ├── dependabot.yml └── workflows │ └── nodejs.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin └── build ├── demos ├── .DS_Store ├── layer-arn │ ├── handler.js │ ├── package.json │ └── serverless.yml ├── local-folders-layers │ ├── handler.js │ ├── myLocalLibSource │ │ ├── main.js │ │ └── package.json │ ├── package.json │ └── serverless.yml ├── multiple-layers │ ├── handler.js │ ├── package.json │ └── serverless.yml └── simple │ ├── handler.py │ ├── package-lock.json │ ├── package.json │ ├── requirements.txt │ └── serverless.yml ├── package-lock.json ├── package.json ├── src ├── AbstractService.js ├── aws │ ├── BucketService.js │ ├── CloudFormationService.js │ ├── LayersService.js │ └── S3Key.js ├── index.js ├── package │ ├── Dependencies.js │ ├── LocalFolders.js │ └── ZipService.js └── runtimes │ ├── index.js │ ├── nodejs.js │ ├── python.js │ └── ruby.js ├── tests ├── fixtures │ ├── nodejsConfig.js │ ├── package.json │ └── pythonConfig.js └── runtime.test.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"], 3 | "plugins": [ 4 | "@babel/plugin-transform-runtime" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | # All Files (Defaults) 4 | 5 | [*] 6 | 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | # Markdown Files 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "rules": { 4 | "consistent-return": 0, 5 | "func-names": 0, 6 | "arrow-body-style": 0, 7 | "arrow-parens": 0, 8 | "global-require": 0, 9 | "import/no-dynamic-require": 0, 10 | "no-await-in-loop": 0, 11 | "brace-style": 0, 12 | "comma-dangle": 0, 13 | "class-methods-use-this": 0, 14 | "key-spacing": 0, 15 | "no-multi-assign": 0, 16 | "one-var": 0, 17 | "prefer-rest-params": 0, 18 | "strict": 0, 19 | "no-console": 0, 20 | "guard-for-in": 0, 21 | "no-restricted-syntax": 0, 22 | "no-underscore-dangle": 0, 23 | "no-param-reassign": 0 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [16.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm ci 28 | - run: npm run build --if-present 29 | - run: npm test 30 | env: 31 | CI: true 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .serverless 3 | demos/simple/.serverless/* 4 | lib/* 5 | .DS_Store 6 | dist/* 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .travis.yml 2 | .eslintrc 3 | .serverless/* 4 | .editorconfig 5 | .babelrc 6 | bin/* 7 | src/* 8 | demos/* 9 | tests/* 10 | .github 11 | dist/* 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'v14.20.0' 4 | before_install: 5 | - npm install -g npm@latest 6 | cache: 7 | npm: true 8 | directories: 9 | - node_modules/ 10 | script: npm run test 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.8.5 (2024-03-09) 2 | - [Bugfix: Look for matching layer](https://github.com/agutoli/serverless-layers/pull/157) 3 | - [Bugfix: Remove aws-sdk from nodejs layerOptimization.cleanupPatterns](https://github.com/agutoli/serverless-layers/pull/151) 4 | 5 | # 2.6.1 (2022-12-23) 6 | - [dependabot mocha](https://github.com/agutoli/serverless-layers/commit/4aca58091e6bf0472e12814513fd800590ae5705) 7 | - [Bugfix: Unable to attach layers to the existing lambda](https://github.com/agutoli/serverless-layers/issues/97) 8 | - [Feature: Bucket ServerSideEncryption](https://github.com/agutoli/serverless-layers/commit/f4770bfb68bc12047e42e545bc1061cdb10accbe) 9 | 10 | # 2.3.3 (2020-12-02) 11 | - [Improvement: Layers output beautified](https://github.com/agutoli/serverless-layers/issues/48) 12 | 13 | # 2.3.2 (2020-12-02) 14 | - [Bugfix: TypeError: arn.replace is not a function.](https://github.com/agutoli/serverless-layers/issues/48) 15 | 16 | # 2.3.1 (2020-11-23) 17 | - [Bugfix Error: "undefined" is not supported (yet).](https://github.com/agutoli/serverless-layers/issues/41) 18 | 19 | # 2.0.2-alpha (2020-02-23) 20 | - [Added support to Python and Ruby](https://github.com/agutoli/serverless-layers) 21 | 22 | # 1.5.0 (2020-02-05) 23 | - [Added feature to clean up layers when remove stack](https://github.com/agutoli/serverless-layers/pull/23) 24 | 25 | # 1.4.2 (2019-08-04) 26 | - [PR with code improvements over getStackName function](https://github.com/agutoli/serverless-layers/pull/19) 27 | 28 | # 1.4.0 (2019-07-28) 29 | - Added support for default package options (exclude and individually) 30 | 31 | # 1.3.1 (2019-05-25) 32 | - [Created customInstallationCommand](https://github.com/agutoli/serverless-layers) 33 | 34 | # 1.2.0-alpha (2019-05-05) 35 | - [Added Yarn support](https://github.com/agutoli/serverless-layers/commit/63756c937aa94653008db1cd7ad9f30d876fd464) 36 | - [Added NPM/Yarn support for .lock files](https://github.com/agutoli/serverless-layers/commit/63756c937aa94653008db1cd7ad9f30d876fd464) 37 | - [Bugfix Powershell (windows)](https://github.com/nodejs/help/issues/1881) 38 | 39 | # 1.0.4 (2019-02-19) 40 | - [Improvements: Added node 6.10 support](https://github.com/agutoli/serverless-layers/commit/b13de031d8754591ee64b7e10b6d194156f02964) 41 | 42 | # 1.0.3 (2019-02-19) 43 | - [Bugfix: dependenciesPath was not working](https://github.com/agutoli/serverless-layers/pull/1) 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Bruno Agutoli 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serverless Layers 2 | 3 | [![serverless](http://public.serverless.com/badges/v3.svg)](http://www.serverless.com) 4 | [![Issues](https://img.shields.io/github/issues/agutoli/serverless-layers.svg)](https://github.com/agutoli/serverless-layers/issues) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://www.npmjs.com/package/serverless-layers) 5 | [![NPM](https://img.shields.io/npm/v/serverless-layers.svg)](https://www.npmjs.com/package/serverless-layers) 6 | [![Build Status](https://travis-ci.org/agutoli/serverless-layers.svg?branch=master)](https://travis-ci.org/agutoli/serverless-layers) 7 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](#contributing) 8 | ![Node.js CI](https://github.com/agutoli/serverless-layers/workflows/Node.js%20CI/badge.svg) 9 | 10 | * Automatically attaches layers to the provider and for each function 11 | * Skips functions with no other layers as they will use the layer(s) we added to the provider 12 | * Creates a new layer's version when `dependencies` are updated 13 | * Does not publish a new layer if `dependencies` are unchanged 14 | * Drastically reduces lambda size 15 | * Reduces deployment time 16 | * Allows sharing of the same layers (libraries) among all lambda functions 17 | 18 | ## Options 19 | 20 | * [NodeJS](#nodejs) 21 | * [Ruby](#ruby) 22 | * [Python](#python) 23 | 24 | ## Common Requirements 25 | 26 | * AWS only (sorry) 27 | * Serverless >= 1.34.0 (layers support) 28 | 29 | ## Install 30 | 31 | ```bash 32 | npm install -D serverless-layers 33 | ``` 34 | 35 | or 36 | 37 | ```bash 38 | serverless plugin install --name serverless-layers 39 | ``` 40 | 41 | Add the plugin to your `serverless.yml` file: 42 | 43 | ## Single layer config 44 | 45 | Example: 46 | 47 | ```yaml 48 | plugins: 49 | - serverless-layers 50 | 51 | custom: 52 | serverless-layers: 53 | functions: # optional 54 | - my_func2 55 | dependenciesPath: ./package.json 56 | 57 | functions: 58 | my_func1: 59 | handler: handler.hello 60 | my_func2: 61 | handler: handler.hello 62 | ``` 63 | 64 | ## Multiple layers config 65 | 66 | Example: 67 | 68 | ```yaml 69 | plugins: 70 | - serverless-layers 71 | 72 | custom: 73 | serverless-layers: 74 | # applies for all lambdas 75 | - common: 76 | dependenciesPath: ./my-folder/package.json 77 | # apply for foo only 78 | - foo: 79 | functions: 80 | - foo 81 | dependenciesPath: my-folder/package-foo.json 82 | - staticArn: 83 | functions: 84 | - foo 85 | - bar 86 | arn: arn:aws:lambda:us-east-1::layer:node-v13-11-0:5 87 | 88 | functions: 89 | foo: 90 | handler: handler.hello 91 | bar: 92 | handler: handler.hello 93 | ``` 94 | 95 | ![Screen Shot 2020-04-05 at 2 04 38 pm](https://user-images.githubusercontent.com/298845/78466747-2fb58f80-7748-11ea-948d-4fce40a753bb.png) 96 | 97 | | Option | Type | Default | Description | 98 | |------------------------------|----------|-----------------|------------------------------------------| 99 | | compileDir | `string` | .serverless | Compilation directory | 100 | | layersDeploymentBucket | `string` | | Specify a bucket to upload lambda layers. `Required if deploymentBucket is not defined.` | 101 | | customInstallationCommand | `string` | | Specify a custom command to install dependencies, e.g., `MY_ENV=1 npm --proxy http://myproxy.com i -P` | 102 | | customHash | `string` | | Specify a custom string that, once changed, will force a new build of the layer | 103 | | retainVersions | `int` | `null` | Number of layer versions to keep; older versions will be removed after deployments | 104 | 105 | ## NodeJS 106 | 107 | ### Requirements 108 | 109 | * Node >= v6.10.3 110 | * NPM >= 3.10.10 111 | * A valid `package.json` file 112 | 113 | ### Options 114 | 115 | | Option | Type | Default | Description | 116 | |-----------------------------|----------|-----------------|--------------------------------------------------------------| 117 | | packageManager | `string` | npm | Possible values: npm, yarn | 118 | | packagePath | `string` | package.json | `(DEPRECATED)`: Available for `<= 1.5.0`; for versions `>= 2.x`, use `compatibleRuntimes` | 119 | | dependenciesPath | `string` | package.json | Note: `>= 2.x` versions. You can specify a custom path for your `package.json` | 120 | | compatibleRuntimes | `array` | `['nodejs']` | Possible values: nodejs, nodejs10.x, nodejs12.x | 121 | | layerOptimization.cleanupPatterns | `array` | [check](https://github.com/agutoli/serverless-layers/blob/master/src/runtimes/nodejs.js) | Pattern of files to cleanup in the layer artifact before uploading it | 122 | 123 | ## Ruby 124 | 125 | ### Requirements 126 | 127 | * Ruby >= 2.5 128 | * A valid `Gemfile` file 129 | 130 | ### Options 131 | 132 | | Option | Type | Default | Description | 133 | |-----------------------------|----------|-----------------|--------------------------------------------------------------| 134 | | packageManager | `string` | bundle | Possible values: bundle | 135 | | dependenciesPath | `string` | Gemfile | Note: Available for `>= 2.x` versions. You can specify a custom path for your `Gemfile` | 136 | | compatibleRuntimes | `array` | `['ruby']` | Possible values: ruby2.5, ruby2.7 | 137 | | layerOptimization.cleanupPatterns | `array` | [check](https://github.com/agutoli/serverless-layers/blob/master/src/runtimes/ruby.js) | Pattern of files to cleanup in the layer artifact before uploading it | 138 | 139 | ## Python 140 | 141 | ### Requirements 142 | 143 | * Python >= 2.7 144 | * A valid `requirements.txt` file 145 | 146 | ### Options 147 | 148 | | Option | Type | Default | Description | 149 | |-----------------------------|----------|-----------------|--------------------------------------------------------------| 150 | | packageManager | `string` | pip | Possible values: pip | 151 | | dependenciesPath | `string` | requirements.txt | Note: Available for `>= 2.x` versions. You can specify a custom path for your `requirements.txt` | 152 | | compatibleRuntimes | `array` | `['python']` | Possible values: python2.7, python3.x | 153 | | layerOptimization.cleanupPatterns | `array` | [check](https://github.com/agutoli/serverless-layers/blob/master/src/runtimes/python.js) | Pattern of files to cleanup in the layer artifact before uploading it | 154 | 155 | ## Default Serverless Setup 156 | 157 | This plugin will set up the following options automatically if not specified in `serverless.yml`. 158 | 159 | | Option | Type | Default | 160 | |-----------------------------|----------|-----------------| 161 | | package.individually | `bool` | false | 162 | | package.patterns | `array`| `['node_modules/**']` | 163 | | package.excludeDevDependencies | `bool` | false | 164 | 165 | 166 | ## Minimal Policy permissions for CI/CD IAM users 167 | 168 | `serverless-layers-policy.json` 169 | 170 | ```json 171 | { 172 | "Version":"2012-10-17", 173 | "Statement":[ 174 | { 175 | "Effect":"Allow", 176 | "Action":[ 177 | "s3:PutObject", 178 | "s3:GetObject" 179 | ], 180 | "Resource": "arn:aws:s3:::examplebucket" 181 | }, 182 | { 183 | "Effect":"Allow", 184 | "Action":[ 185 | "cloudformation:DescribeStacks" 186 | ], 187 | "Resource": "*" 188 | }, 189 | { 190 | "Effect":"Allow", 191 | "Action":[ 192 | "lambda:PublishLayerVersion" 193 | ], 194 | "Resource": "*" 195 | } 196 | ] 197 | } 198 | ``` 199 | 200 | ## License 201 | 202 | MIT 203 | 204 | ## Contributors 205 | 206 | Yes, thank you! This plugin is community-driven, and most of its features are from different authors. Please update the docs and tests and add your name to the `package.json` file. We try to follow [Airbnb's JavaScript Style Guide](https://github.com/airbnb/javascript). 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | Made with [contributors-img](https://contrib.rocks). 215 | -------------------------------------------------------------------------------- /bin/build: -------------------------------------------------------------------------------- 1 | babel --copy-files \ 2 | src/ \ 3 | --ignore *.test.js --out-dir ./lib --source-maps 4 | -------------------------------------------------------------------------------- /demos/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agutoli/serverless-layers/842eb7ce374af7a400e6cec2176bcc6f1882919c/demos/.DS_Store -------------------------------------------------------------------------------- /demos/layer-arn/handler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.hello = async event => { 4 | return { 5 | statusCode: 200, 6 | body: JSON.stringify( 7 | { 8 | message: 'Go Serverless v1.0! Your function executed successfully!', 9 | input: event, 10 | }, 11 | null, 12 | 2 13 | ), 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /demos/layer-arn/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-layer-arn", 3 | "description": "", 4 | "version": "0.1.0", 5 | "dependencies": {}, 6 | "devDependencies": { 7 | "serverless-layers": "2.1.3" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /demos/layer-arn/serverless.yml: -------------------------------------------------------------------------------- 1 | service: demo-layer-arn 2 | 3 | provider: 4 | name: aws 5 | region: us-east-1 6 | runtime: nodejs12.x 7 | deploymentBucket: 8 | name: your-bucket 9 | 10 | custom: 11 | serverless-layers: 12 | arn: arn:aws:lambda:us-east-1::layer:node-v13-11-0:5 13 | 14 | functions: 15 | hello: 16 | handler: handler.hello 17 | 18 | plugins: 19 | - serverless-layers 20 | -------------------------------------------------------------------------------- /demos/local-folders-layers/handler.js: -------------------------------------------------------------------------------- 1 | const myLibName = require('my-lib-name'); 2 | 3 | module.exports.hello = async event => { 4 | 5 | myLibName.doSomething(); 6 | 7 | return { 8 | statusCode: 200, 9 | body: 'foo' 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /demos/local-folders-layers/myLocalLibSource/main.js: -------------------------------------------------------------------------------- 1 | module.exports = () => { 2 | console.log('It works!'); 3 | return {} 4 | } 5 | -------------------------------------------------------------------------------- /demos/local-folders-layers/myLocalLibSource/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-lib-name", 3 | "main": "./main.js", 4 | "description": "", 5 | "version": "0.1.0", 6 | "dependencies": { 7 | "express": "^4.17.0" 8 | }, 9 | "devDependencies": { 10 | "slugify": "^1.4.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /demos/local-folders-layers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "local-folders-layers", 3 | "description": "", 4 | "version": "0.1.0", 5 | "dependencies": {}, 6 | "devDependencies": { 7 | "serverless-layers": "2.1.3" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /demos/local-folders-layers/serverless.yml: -------------------------------------------------------------------------------- 1 | service: local-folders-layers 2 | 3 | provider: 4 | name: aws 5 | region: us-east-1 6 | runtime: nodejs12.x 7 | deploymentBucket: 8 | name: your-bucket 9 | 10 | custom: 11 | serverless-layers: 12 | - my-local-folder: 13 | dependenciesPath: ./package.json 14 | localDir: 15 | name: my-lib-name 16 | path: ./myLocalLibSource 17 | folders: 18 | include: 19 | - '*.js' 20 | exclude: 21 | - node_modules 22 | 23 | functions: 24 | hello: 25 | handler: handler.hello 26 | 27 | plugins: 28 | - serverless-layers 29 | -------------------------------------------------------------------------------- /demos/multiple-layers/handler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.foo = async event => { 4 | return { 5 | statusCode: 200, 6 | body: 'foo' 7 | }; 8 | }; 9 | 10 | 11 | module.exports.bar = async event => { 12 | return { 13 | statusCode: 200, 14 | body: 'bar' 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /demos/multiple-layers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multiple-layers", 3 | "description": "", 4 | "version": "0.1.0", 5 | "dependencies": {}, 6 | "devDependencies": { 7 | "serverless-layers": "2.1.3" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /demos/multiple-layers/serverless.yml: -------------------------------------------------------------------------------- 1 | service: multiple-layers 2 | 3 | provider: 4 | name: aws 5 | region: us-east-1 6 | runtime: nodejs12.x 7 | deploymentBucket: 8 | name: your-bucket 9 | 10 | custom: 11 | serverless-layers: 12 | - common: 13 | dependenciesPath: ./my-folder/package.json 14 | - myLayerArn: 15 | functions: 16 | - foo 17 | arn: arn:aws:lambda:us-east-1::layer:node-v13-11-0:5 18 | 19 | functions: 20 | foo: 21 | handler: handler.foo 22 | bar: 23 | handler: handler.bar 24 | 25 | plugins: 26 | - serverless-layers 27 | -------------------------------------------------------------------------------- /demos/simple/handler.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | def main(event, context): 4 | print(requests) 5 | 6 | if __name__ == "__main__": 7 | main('', '') -------------------------------------------------------------------------------- /demos/simple/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-1", 3 | "version": "0.1.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "demo-1", 9 | "version": "0.1.0", 10 | "devDependencies": { 11 | "serverless-layers": "2.8.0" 12 | } 13 | }, 14 | "node_modules/@babel/runtime": { 15 | "version": "7.20.7", 16 | "dev": true, 17 | "license": "MIT", 18 | "dependencies": { 19 | "regenerator-runtime": "^0.13.11" 20 | }, 21 | "engines": { 22 | "node": ">=6.9.0" 23 | } 24 | }, 25 | "node_modules/@cloudcmd/copy-file": { 26 | "version": "1.1.1", 27 | "dev": true, 28 | "license": "MIT", 29 | "dependencies": { 30 | "es6-promisify": "^6.0.0", 31 | "pipe-io": "^3.0.0", 32 | "wraptile": "^2.0.0", 33 | "zames": "^2.0.0" 34 | }, 35 | "engines": { 36 | "node": ">=4.0.0" 37 | } 38 | }, 39 | "node_modules/ansi-styles": { 40 | "version": "4.3.0", 41 | "dev": true, 42 | "license": "MIT", 43 | "dependencies": { 44 | "color-convert": "^2.0.1" 45 | }, 46 | "engines": { 47 | "node": ">=8" 48 | }, 49 | "funding": { 50 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 51 | } 52 | }, 53 | "node_modules/archiver": { 54 | "version": "3.1.1", 55 | "dev": true, 56 | "license": "MIT", 57 | "dependencies": { 58 | "archiver-utils": "^2.1.0", 59 | "async": "^2.6.3", 60 | "buffer-crc32": "^0.2.1", 61 | "glob": "^7.1.4", 62 | "readable-stream": "^3.4.0", 63 | "tar-stream": "^2.1.0", 64 | "zip-stream": "^2.1.2" 65 | }, 66 | "engines": { 67 | "node": ">= 6" 68 | } 69 | }, 70 | "node_modules/archiver-utils": { 71 | "version": "2.1.0", 72 | "dev": true, 73 | "license": "MIT", 74 | "dependencies": { 75 | "glob": "^7.1.4", 76 | "graceful-fs": "^4.2.0", 77 | "lazystream": "^1.0.0", 78 | "lodash.defaults": "^4.2.0", 79 | "lodash.difference": "^4.5.0", 80 | "lodash.flatten": "^4.4.0", 81 | "lodash.isplainobject": "^4.0.6", 82 | "lodash.union": "^4.6.0", 83 | "normalize-path": "^3.0.0", 84 | "readable-stream": "^2.0.0" 85 | }, 86 | "engines": { 87 | "node": ">= 6" 88 | } 89 | }, 90 | "node_modules/archiver-utils/node_modules/readable-stream": { 91 | "version": "2.3.7", 92 | "dev": true, 93 | "license": "MIT", 94 | "dependencies": { 95 | "core-util-is": "~1.0.0", 96 | "inherits": "~2.0.3", 97 | "isarray": "~1.0.0", 98 | "process-nextick-args": "~2.0.0", 99 | "safe-buffer": "~5.1.1", 100 | "string_decoder": "~1.1.1", 101 | "util-deprecate": "~1.0.1" 102 | } 103 | }, 104 | "node_modules/archiver-utils/node_modules/string_decoder": { 105 | "version": "1.1.1", 106 | "dev": true, 107 | "license": "MIT", 108 | "dependencies": { 109 | "safe-buffer": "~5.1.0" 110 | } 111 | }, 112 | "node_modules/async": { 113 | "version": "2.6.4", 114 | "dev": true, 115 | "license": "MIT", 116 | "dependencies": { 117 | "lodash": "^4.17.14" 118 | } 119 | }, 120 | "node_modules/balanced-match": { 121 | "version": "1.0.2", 122 | "dev": true, 123 | "license": "MIT" 124 | }, 125 | "node_modules/base64-js": { 126 | "version": "1.5.1", 127 | "dev": true, 128 | "funding": [ 129 | { 130 | "type": "github", 131 | "url": "https://github.com/sponsors/feross" 132 | }, 133 | { 134 | "type": "patreon", 135 | "url": "https://www.patreon.com/feross" 136 | }, 137 | { 138 | "type": "consulting", 139 | "url": "https://feross.org/support" 140 | } 141 | ], 142 | "license": "MIT" 143 | }, 144 | "node_modules/bl": { 145 | "version": "4.1.0", 146 | "dev": true, 147 | "license": "MIT", 148 | "dependencies": { 149 | "buffer": "^5.5.0", 150 | "inherits": "^2.0.4", 151 | "readable-stream": "^3.4.0" 152 | } 153 | }, 154 | "node_modules/bluebird": { 155 | "version": "3.7.2", 156 | "dev": true, 157 | "license": "MIT" 158 | }, 159 | "node_modules/brace-expansion": { 160 | "version": "1.1.11", 161 | "dev": true, 162 | "license": "MIT", 163 | "dependencies": { 164 | "balanced-match": "^1.0.0", 165 | "concat-map": "0.0.1" 166 | } 167 | }, 168 | "node_modules/buffer": { 169 | "version": "5.7.1", 170 | "dev": true, 171 | "funding": [ 172 | { 173 | "type": "github", 174 | "url": "https://github.com/sponsors/feross" 175 | }, 176 | { 177 | "type": "patreon", 178 | "url": "https://www.patreon.com/feross" 179 | }, 180 | { 181 | "type": "consulting", 182 | "url": "https://feross.org/support" 183 | } 184 | ], 185 | "license": "MIT", 186 | "dependencies": { 187 | "base64-js": "^1.3.1", 188 | "ieee754": "^1.1.13" 189 | } 190 | }, 191 | "node_modules/buffer-crc32": { 192 | "version": "0.2.13", 193 | "dev": true, 194 | "license": "MIT", 195 | "engines": { 196 | "node": "*" 197 | } 198 | }, 199 | "node_modules/chalk": { 200 | "version": "3.0.0", 201 | "dev": true, 202 | "license": "MIT", 203 | "dependencies": { 204 | "ansi-styles": "^4.1.0", 205 | "supports-color": "^7.1.0" 206 | }, 207 | "engines": { 208 | "node": ">=8" 209 | } 210 | }, 211 | "node_modules/color-convert": { 212 | "version": "2.0.1", 213 | "dev": true, 214 | "license": "MIT", 215 | "dependencies": { 216 | "color-name": "~1.1.4" 217 | }, 218 | "engines": { 219 | "node": ">=7.0.0" 220 | } 221 | }, 222 | "node_modules/color-name": { 223 | "version": "1.1.4", 224 | "dev": true, 225 | "license": "MIT" 226 | }, 227 | "node_modules/compress-commons": { 228 | "version": "2.1.1", 229 | "dev": true, 230 | "license": "MIT", 231 | "dependencies": { 232 | "buffer-crc32": "^0.2.13", 233 | "crc32-stream": "^3.0.1", 234 | "normalize-path": "^3.0.0", 235 | "readable-stream": "^2.3.6" 236 | }, 237 | "engines": { 238 | "node": ">= 6" 239 | } 240 | }, 241 | "node_modules/compress-commons/node_modules/readable-stream": { 242 | "version": "2.3.7", 243 | "dev": true, 244 | "license": "MIT", 245 | "dependencies": { 246 | "core-util-is": "~1.0.0", 247 | "inherits": "~2.0.3", 248 | "isarray": "~1.0.0", 249 | "process-nextick-args": "~2.0.0", 250 | "safe-buffer": "~5.1.1", 251 | "string_decoder": "~1.1.1", 252 | "util-deprecate": "~1.0.1" 253 | } 254 | }, 255 | "node_modules/compress-commons/node_modules/string_decoder": { 256 | "version": "1.1.1", 257 | "dev": true, 258 | "license": "MIT", 259 | "dependencies": { 260 | "safe-buffer": "~5.1.0" 261 | } 262 | }, 263 | "node_modules/concat-map": { 264 | "version": "0.0.1", 265 | "dev": true, 266 | "license": "MIT" 267 | }, 268 | "node_modules/core-util-is": { 269 | "version": "1.0.3", 270 | "dev": true, 271 | "license": "MIT" 272 | }, 273 | "node_modules/crc": { 274 | "version": "3.8.0", 275 | "dev": true, 276 | "license": "MIT", 277 | "dependencies": { 278 | "buffer": "^5.1.0" 279 | } 280 | }, 281 | "node_modules/crc32-stream": { 282 | "version": "3.0.1", 283 | "dev": true, 284 | "license": "MIT", 285 | "dependencies": { 286 | "crc": "^3.4.4", 287 | "readable-stream": "^3.4.0" 288 | }, 289 | "engines": { 290 | "node": ">= 6.9.0" 291 | } 292 | }, 293 | "node_modules/currify": { 294 | "version": "3.0.0", 295 | "dev": true, 296 | "license": "MIT" 297 | }, 298 | "node_modules/debug": { 299 | "version": "4.3.4", 300 | "dev": true, 301 | "license": "MIT", 302 | "dependencies": { 303 | "ms": "2.1.2" 304 | }, 305 | "engines": { 306 | "node": ">=6.0" 307 | }, 308 | "peerDependenciesMeta": { 309 | "supports-color": { 310 | "optional": true 311 | } 312 | } 313 | }, 314 | "node_modules/end-of-stream": { 315 | "version": "1.4.4", 316 | "dev": true, 317 | "license": "MIT", 318 | "dependencies": { 319 | "once": "^1.4.0" 320 | } 321 | }, 322 | "node_modules/es6-promisify": { 323 | "version": "6.1.1", 324 | "dev": true, 325 | "license": "MIT" 326 | }, 327 | "node_modules/folder-hash": { 328 | "version": "3.3.3", 329 | "dev": true, 330 | "license": "MIT", 331 | "dependencies": { 332 | "debug": "^4.1.1", 333 | "graceful-fs": "~4.2.0", 334 | "minimatch": "~3.0.4" 335 | }, 336 | "bin": { 337 | "folder-hash": "bin/folder-hash" 338 | }, 339 | "engines": { 340 | "node": ">=6.0.0" 341 | } 342 | }, 343 | "node_modules/folder-hash/node_modules/minimatch": { 344 | "version": "3.0.8", 345 | "dev": true, 346 | "license": "ISC", 347 | "dependencies": { 348 | "brace-expansion": "^1.1.7" 349 | }, 350 | "engines": { 351 | "node": "*" 352 | } 353 | }, 354 | "node_modules/fs-constants": { 355 | "version": "1.0.0", 356 | "dev": true, 357 | "license": "MIT" 358 | }, 359 | "node_modules/fs-copy-file": { 360 | "version": "2.1.2", 361 | "dev": true, 362 | "license": "MIT", 363 | "dependencies": { 364 | "@cloudcmd/copy-file": "^1.1.0" 365 | }, 366 | "engines": { 367 | "node": ">=4" 368 | } 369 | }, 370 | "node_modules/fs-extra": { 371 | "version": "8.1.0", 372 | "dev": true, 373 | "license": "MIT", 374 | "dependencies": { 375 | "graceful-fs": "^4.2.0", 376 | "jsonfile": "^4.0.0", 377 | "universalify": "^0.1.0" 378 | }, 379 | "engines": { 380 | "node": ">=6 <7 || >=8" 381 | } 382 | }, 383 | "node_modules/fs.realpath": { 384 | "version": "1.0.0", 385 | "dev": true, 386 | "license": "ISC" 387 | }, 388 | "node_modules/glob": { 389 | "version": "7.2.3", 390 | "dev": true, 391 | "license": "ISC", 392 | "dependencies": { 393 | "fs.realpath": "^1.0.0", 394 | "inflight": "^1.0.4", 395 | "inherits": "2", 396 | "minimatch": "^3.1.1", 397 | "once": "^1.3.0", 398 | "path-is-absolute": "^1.0.0" 399 | }, 400 | "engines": { 401 | "node": "*" 402 | }, 403 | "funding": { 404 | "url": "https://github.com/sponsors/isaacs" 405 | } 406 | }, 407 | "node_modules/graceful-fs": { 408 | "version": "4.2.10", 409 | "dev": true, 410 | "license": "ISC" 411 | }, 412 | "node_modules/has-flag": { 413 | "version": "4.0.0", 414 | "dev": true, 415 | "license": "MIT", 416 | "engines": { 417 | "node": ">=8" 418 | } 419 | }, 420 | "node_modules/ieee754": { 421 | "version": "1.2.1", 422 | "dev": true, 423 | "funding": [ 424 | { 425 | "type": "github", 426 | "url": "https://github.com/sponsors/feross" 427 | }, 428 | { 429 | "type": "patreon", 430 | "url": "https://www.patreon.com/feross" 431 | }, 432 | { 433 | "type": "consulting", 434 | "url": "https://feross.org/support" 435 | } 436 | ], 437 | "license": "BSD-3-Clause" 438 | }, 439 | "node_modules/inflight": { 440 | "version": "1.0.6", 441 | "dev": true, 442 | "license": "ISC", 443 | "dependencies": { 444 | "once": "^1.3.0", 445 | "wrappy": "1" 446 | } 447 | }, 448 | "node_modules/inherits": { 449 | "version": "2.0.4", 450 | "dev": true, 451 | "license": "ISC" 452 | }, 453 | "node_modules/isarray": { 454 | "version": "1.0.0", 455 | "dev": true, 456 | "license": "MIT" 457 | }, 458 | "node_modules/jsonfile": { 459 | "version": "4.0.0", 460 | "dev": true, 461 | "license": "MIT", 462 | "optionalDependencies": { 463 | "graceful-fs": "^4.1.6" 464 | } 465 | }, 466 | "node_modules/lazystream": { 467 | "version": "1.0.1", 468 | "dev": true, 469 | "license": "MIT", 470 | "dependencies": { 471 | "readable-stream": "^2.0.5" 472 | }, 473 | "engines": { 474 | "node": ">= 0.6.3" 475 | } 476 | }, 477 | "node_modules/lazystream/node_modules/readable-stream": { 478 | "version": "2.3.7", 479 | "dev": true, 480 | "license": "MIT", 481 | "dependencies": { 482 | "core-util-is": "~1.0.0", 483 | "inherits": "~2.0.3", 484 | "isarray": "~1.0.0", 485 | "process-nextick-args": "~2.0.0", 486 | "safe-buffer": "~5.1.1", 487 | "string_decoder": "~1.1.1", 488 | "util-deprecate": "~1.0.1" 489 | } 490 | }, 491 | "node_modules/lazystream/node_modules/string_decoder": { 492 | "version": "1.1.1", 493 | "dev": true, 494 | "license": "MIT", 495 | "dependencies": { 496 | "safe-buffer": "~5.1.0" 497 | } 498 | }, 499 | "node_modules/lodash": { 500 | "version": "4.17.21", 501 | "dev": true, 502 | "license": "MIT" 503 | }, 504 | "node_modules/lodash.defaults": { 505 | "version": "4.2.0", 506 | "dev": true, 507 | "license": "MIT" 508 | }, 509 | "node_modules/lodash.difference": { 510 | "version": "4.5.0", 511 | "dev": true, 512 | "license": "MIT" 513 | }, 514 | "node_modules/lodash.flatten": { 515 | "version": "4.4.0", 516 | "dev": true, 517 | "license": "MIT" 518 | }, 519 | "node_modules/lodash.isplainobject": { 520 | "version": "4.0.6", 521 | "dev": true, 522 | "license": "MIT" 523 | }, 524 | "node_modules/lodash.union": { 525 | "version": "4.6.0", 526 | "dev": true, 527 | "license": "MIT" 528 | }, 529 | "node_modules/lru-cache": { 530 | "version": "6.0.0", 531 | "dev": true, 532 | "license": "ISC", 533 | "dependencies": { 534 | "yallist": "^4.0.0" 535 | }, 536 | "engines": { 537 | "node": ">=10" 538 | } 539 | }, 540 | "node_modules/minimatch": { 541 | "version": "3.1.2", 542 | "dev": true, 543 | "license": "ISC", 544 | "dependencies": { 545 | "brace-expansion": "^1.1.7" 546 | }, 547 | "engines": { 548 | "node": "*" 549 | } 550 | }, 551 | "node_modules/minimist": { 552 | "version": "1.2.7", 553 | "dev": true, 554 | "license": "MIT", 555 | "funding": { 556 | "url": "https://github.com/sponsors/ljharb" 557 | } 558 | }, 559 | "node_modules/mkdirp": { 560 | "version": "0.5.6", 561 | "dev": true, 562 | "license": "MIT", 563 | "dependencies": { 564 | "minimist": "^1.2.6" 565 | }, 566 | "bin": { 567 | "mkdirp": "bin/cmd.js" 568 | } 569 | }, 570 | "node_modules/ms": { 571 | "version": "2.1.2", 572 | "dev": true, 573 | "license": "MIT" 574 | }, 575 | "node_modules/normalize-path": { 576 | "version": "3.0.0", 577 | "dev": true, 578 | "license": "MIT", 579 | "engines": { 580 | "node": ">=0.10.0" 581 | } 582 | }, 583 | "node_modules/once": { 584 | "version": "1.4.0", 585 | "dev": true, 586 | "license": "ISC", 587 | "dependencies": { 588 | "wrappy": "1" 589 | } 590 | }, 591 | "node_modules/path-is-absolute": { 592 | "version": "1.0.1", 593 | "dev": true, 594 | "license": "MIT", 595 | "engines": { 596 | "node": ">=0.10.0" 597 | } 598 | }, 599 | "node_modules/pipe-io": { 600 | "version": "3.0.12", 601 | "dev": true, 602 | "license": "MIT", 603 | "engines": { 604 | "node": ">=4" 605 | } 606 | }, 607 | "node_modules/process-nextick-args": { 608 | "version": "2.0.1", 609 | "dev": true, 610 | "license": "MIT" 611 | }, 612 | "node_modules/readable-stream": { 613 | "version": "3.6.0", 614 | "dev": true, 615 | "license": "MIT", 616 | "dependencies": { 617 | "inherits": "^2.0.3", 618 | "string_decoder": "^1.1.1", 619 | "util-deprecate": "^1.0.1" 620 | }, 621 | "engines": { 622 | "node": ">= 6" 623 | } 624 | }, 625 | "node_modules/regenerator-runtime": { 626 | "version": "0.13.11", 627 | "dev": true, 628 | "license": "MIT" 629 | }, 630 | "node_modules/safe-buffer": { 631 | "version": "5.1.2", 632 | "dev": true, 633 | "license": "MIT" 634 | }, 635 | "node_modules/semver": { 636 | "version": "7.3.8", 637 | "dev": true, 638 | "license": "ISC", 639 | "dependencies": { 640 | "lru-cache": "^6.0.0" 641 | }, 642 | "bin": { 643 | "semver": "bin/semver.js" 644 | }, 645 | "engines": { 646 | "node": ">=10" 647 | } 648 | }, 649 | "node_modules/serverless-layers": { 650 | "version": "2.8.0", 651 | "resolved": "git+ssh://git@github.com/agutoli/serverless-layers.git#eee69080b04292fbd703ed92b9a8e8ca8beed79a", 652 | "dev": true, 653 | "license": "MIT", 654 | "dependencies": { 655 | "@babel/runtime": "^7.3.1", 656 | "archiver": "^3.0.0", 657 | "bluebird": "^3.5.3", 658 | "chalk": "^3.0.0", 659 | "folder-hash": "^3.3.0", 660 | "fs-copy-file": "^2.1.2", 661 | "fs-extra": "^8.1.0", 662 | "glob": "^7.1.6", 663 | "mkdirp": "^0.5.6", 664 | "semver": "^7.3.2", 665 | "slugify": "^1.4.0" 666 | } 667 | }, 668 | "node_modules/slugify": { 669 | "version": "1.6.5", 670 | "dev": true, 671 | "license": "MIT", 672 | "engines": { 673 | "node": ">=8.0.0" 674 | } 675 | }, 676 | "node_modules/string_decoder": { 677 | "version": "1.3.0", 678 | "dev": true, 679 | "license": "MIT", 680 | "dependencies": { 681 | "safe-buffer": "~5.2.0" 682 | } 683 | }, 684 | "node_modules/string_decoder/node_modules/safe-buffer": { 685 | "version": "5.2.1", 686 | "dev": true, 687 | "funding": [ 688 | { 689 | "type": "github", 690 | "url": "https://github.com/sponsors/feross" 691 | }, 692 | { 693 | "type": "patreon", 694 | "url": "https://www.patreon.com/feross" 695 | }, 696 | { 697 | "type": "consulting", 698 | "url": "https://feross.org/support" 699 | } 700 | ], 701 | "license": "MIT" 702 | }, 703 | "node_modules/supports-color": { 704 | "version": "7.2.0", 705 | "dev": true, 706 | "license": "MIT", 707 | "dependencies": { 708 | "has-flag": "^4.0.0" 709 | }, 710 | "engines": { 711 | "node": ">=8" 712 | } 713 | }, 714 | "node_modules/tar-stream": { 715 | "version": "2.2.0", 716 | "dev": true, 717 | "license": "MIT", 718 | "dependencies": { 719 | "bl": "^4.0.3", 720 | "end-of-stream": "^1.4.1", 721 | "fs-constants": "^1.0.0", 722 | "inherits": "^2.0.3", 723 | "readable-stream": "^3.1.1" 724 | }, 725 | "engines": { 726 | "node": ">=6" 727 | } 728 | }, 729 | "node_modules/universalify": { 730 | "version": "0.1.2", 731 | "dev": true, 732 | "license": "MIT", 733 | "engines": { 734 | "node": ">= 4.0.0" 735 | } 736 | }, 737 | "node_modules/util-deprecate": { 738 | "version": "1.0.2", 739 | "dev": true, 740 | "license": "MIT" 741 | }, 742 | "node_modules/wrappy": { 743 | "version": "1.0.2", 744 | "dev": true, 745 | "license": "ISC" 746 | }, 747 | "node_modules/wraptile": { 748 | "version": "2.0.0", 749 | "dev": true, 750 | "license": "MIT" 751 | }, 752 | "node_modules/yallist": { 753 | "version": "4.0.0", 754 | "dev": true, 755 | "license": "ISC" 756 | }, 757 | "node_modules/zames": { 758 | "version": "2.0.1", 759 | "dev": true, 760 | "license": "MIT", 761 | "dependencies": { 762 | "currify": "^3.0.0", 763 | "es6-promisify": "^6.0.0" 764 | } 765 | }, 766 | "node_modules/zip-stream": { 767 | "version": "2.1.3", 768 | "dev": true, 769 | "license": "MIT", 770 | "dependencies": { 771 | "archiver-utils": "^2.1.0", 772 | "compress-commons": "^2.1.1", 773 | "readable-stream": "^3.4.0" 774 | }, 775 | "engines": { 776 | "node": ">= 6" 777 | } 778 | } 779 | } 780 | } 781 | -------------------------------------------------------------------------------- /demos/simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-1", 3 | "description": "", 4 | "version": "0.1.0", 5 | "dependencies": {}, 6 | "devDependencies": { 7 | "serverless-layers": "agutoli/serverless-layers#master" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /demos/simple/requirements.txt: -------------------------------------------------------------------------------- 1 | requests -------------------------------------------------------------------------------- /demos/simple/serverless.yml: -------------------------------------------------------------------------------- 1 | service: demo-simple 2 | 3 | provider: 4 | name: aws 5 | region: us-east-1 6 | runtime: python3.8 7 | deploymentBucket: 8 | name: agutoli-my-test-bucket 9 | 10 | custom: 11 | serverless-layers: 12 | dependenciesPath: requirements.txt 13 | packageManagerExtraArgs: '--no-color' 14 | compatibleRuntimes: ["python3.8"] 15 | 16 | functions: 17 | hello: 18 | handler: handler.hello 19 | layers: 20 | - { Ref: MyLayerLambdaLayer } 21 | 22 | plugins: 23 | - serverless-layers 24 | 25 | layers: 26 | myLayer: 27 | package: 28 | artifact: .serverless/demo-simple.zip 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-layers", 3 | "version": "2.8.5", 4 | "description": "", 5 | "main": "lib/index.js", 6 | "bugs": { 7 | "url": "https://github.com/agutoli/serverless-layers/issues" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/agutoli/serverless-layers.git" 12 | }, 13 | "homepage": "https://github.com/agutoli/serverless-layers", 14 | "scripts": { 15 | "build": "BABEL_ENV=production ./bin/build", 16 | "test": "NODE_ENV=test NODE_PATH=./src mocha 'tests/**/*.test.js' --colors --reporter spec -unhandled-rejections=strict", 17 | "lint": "eslint src/**/*.js", 18 | "dev:watch": "nodemon --watch ./src --exec 'npm run lint; npm run build'", 19 | "lint:watch": "nodemon --watch ./src --exec 'npm run lint'" 20 | }, 21 | "keywords": [ 22 | "Serverless", 23 | "Amazon Web Services", 24 | "AWS", 25 | "Lambda", 26 | "python", 27 | "nodejs", 28 | "plugin", 29 | "API Gateway", 30 | "environment", 31 | "layers" 32 | ], 33 | "author": "Bruno Agutoli (https://github.com/agutoli)", 34 | "license": "MIT", 35 | "dependencies": { 36 | "@babel/runtime": "^7.3.1", 37 | "archiver": "^3.0.0", 38 | "bluebird": "^3.5.3", 39 | "chalk": "^3.0.0", 40 | "folder-hash": "^3.3.0", 41 | "fs-copy-file": "^2.1.2", 42 | "fs-extra": "^8.1.0", 43 | "glob": "^7.1.6", 44 | "mkdirp": "^0.5.6", 45 | "semver": "^7.3.2", 46 | "slugify": "^1.4.0" 47 | }, 48 | "devDependencies": { 49 | "@babel/cli": "^7.14.5", 50 | "@babel/core": "^7.13.0", 51 | "@babel/plugin-transform-runtime": "^7.2.0", 52 | "@babel/preset-env": "^7.3.1", 53 | "aws-sdk": "^2.402.0", 54 | "bl": ">=3.0.1", 55 | "chai": "^4.2.0", 56 | "dot-prop": ">=4.2.1", 57 | "eslint": "^5.3.0", 58 | "eslint-config-airbnb": "^17.1.0", 59 | "eslint-plugin-import": "^2.16.0", 60 | "eslint-plugin-jsx-a11y": "^6.2.1", 61 | "eslint-plugin-react": "^7.12.4", 62 | "lodash": "^4.17.21", 63 | "mocha": "^10.2.0", 64 | "nodemon": "^3.1.0", 65 | "pre-commit": "^1.2.2", 66 | "sinon": "^9.0.1" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/AbstractService.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const path = require('path'); 3 | 4 | class AbstractService { 5 | constructor(plugin) { 6 | this.plugin = plugin; 7 | 8 | this.functionName = plugin.currentLayerName; 9 | this.stackName = plugin.getStackName(); 10 | this.layerName = plugin.getLayerName(); 11 | this.bucketName = plugin.getBucketName(); 12 | this.bucketEncryption = plugin.getBucketEncryptiom(); 13 | this.provider = this.plugin.provider; 14 | 15 | this.dependenceFilename = path.join(plugin.getBucketLayersPath(), this.plugin.settings.dependenciesPath); 16 | this.zipFileKeyName = `${path.join(this.plugin.getBucketLayersPath(), this.layerName)}.zip`; 17 | 18 | if (/^win/.test(process.platform)) { 19 | this.zipFileKeyName = this.zipFileKeyName.replace(/\\/g, '/'); 20 | this.dependenceFilename = this.dependenceFilename.replace(/\\/g, '/'); 21 | } 22 | } 23 | 24 | async awsRequest(serviceAction, params, opts={}) { 25 | const [service, action] = serviceAction.split(':'); 26 | if (!opts.checkError) { 27 | return this.provider.request(service, action, params); 28 | } 29 | 30 | try { 31 | const resp = await this.provider.request(service, action, params); 32 | return resp; 33 | }catch(e) { 34 | console.log(chalk.red(`ServerlessLayers error:`)); 35 | console.log(` Action: ${serviceAction}`); 36 | console.log(` Params: ${JSON.stringify(params)}`); 37 | console.log(chalk.red(`AWS SDK error:`)); 38 | console.log(` ${e.message}`); 39 | process.exit(1); 40 | } 41 | } 42 | 43 | getLayerPackageDir() { 44 | const { compileDir, runtimeDir } = this.plugin.settings; 45 | return path.join(process.cwd(), compileDir, 'layers', runtimeDir);; 46 | } 47 | } 48 | 49 | module.exports = AbstractService 50 | -------------------------------------------------------------------------------- /src/aws/BucketService.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const S3Key = require('./S3Key'); 5 | const AbstractService = require('../AbstractService'); 6 | 7 | class UploadService extends AbstractService { 8 | keyPath(filename) { 9 | let value = path.join(this.plugin.getBucketLayersPath(), filename); 10 | if (/^win/.test(process.platform)) { 11 | value = value.replace(/\\/g, '/'); 12 | } 13 | return value; 14 | } 15 | 16 | async uploadZipFile() { 17 | this.plugin.log('Uploading layer package...'); 18 | 19 | const params = { 20 | Bucket: this.bucketName, 21 | Key: this.zipFileKeyName, 22 | ServerSideEncryption: this.plugin.getBucketEncryptiom(), 23 | Body: fs.createReadStream(this.plugin.getPathZipFileName()) 24 | }; 25 | 26 | return this.awsRequest('S3:putObject', params, { checkError: true }) 27 | .then((result) => { 28 | this.plugin.log('OK...'); 29 | return result; 30 | }); 31 | } 32 | 33 | async putFile(filename, body) { 34 | const file = new S3Key(filename); 35 | this.plugin.log(`Uploading remote ${filename}...`); 36 | 37 | let Body = body; 38 | 39 | if (!body) { 40 | Body = file.getStream(); 41 | } 42 | 43 | const params = { 44 | Body, 45 | Bucket: this.bucketName, 46 | Key: this.keyPath(file.getKey()), 47 | ServerSideEncryption: this.plugin.getBucketEncryptiom() 48 | }; 49 | 50 | return this.awsRequest('S3:putObject', params, { checkError: true }) 51 | .then((result) => { 52 | this.plugin.log('OK...'); 53 | return result; 54 | }); 55 | } 56 | 57 | async getFile(filename) { 58 | const file = new S3Key(filename); 59 | this.plugin.log(`Downloading ${file.getKey()} from bucket...`); 60 | 61 | const params = { 62 | Bucket: this.bucketName, 63 | Key: this.keyPath(file.getKey()) 64 | }; 65 | 66 | return this.awsRequest('S3:getObject', params) 67 | .then((result) => result.Body.toString()) 68 | .catch((e) => { 69 | this.plugin.log(`${filename} ${e.message}.`); 70 | return null; 71 | }); 72 | } 73 | 74 | async uploadDependencesFile() { 75 | const { dependenciesPath } = this.plugin.settings; 76 | 77 | this.plugin.log(`Uploading remote ${dependenciesPath}...`); 78 | 79 | const params = { 80 | Bucket: this.bucketName, 81 | Key: this.dependenceFilename, 82 | ServerSideEncryption: this.plugin.getBucketEncryptiom(), 83 | Body: fs.createReadStream(this.plugin.settings.dependenciesPath) 84 | }; 85 | 86 | return this.awsRequest('S3:putObject', params, { checkError: true }) 87 | .then((result) => { 88 | this.plugin.log('OK...'); 89 | return result; 90 | }); 91 | } 92 | 93 | async downloadDependencesFile() { 94 | const { dependenciesPath } = this.plugin.settings; 95 | this.plugin.log(`Downloading ${dependenciesPath} from bucket...`); 96 | 97 | const params = { 98 | Bucket: this.bucketName, 99 | Key: this.dependenceFilename 100 | }; 101 | 102 | return this.awsRequest('S3:getObject', params) 103 | .then((result) => result.Body.toString()) 104 | .catch((e) => { 105 | this.plugin.log(`${dependenciesPath} ${e.message}.`); 106 | return null; 107 | }); 108 | } 109 | } 110 | 111 | module.exports = UploadService; 112 | -------------------------------------------------------------------------------- /src/aws/CloudFormationService.js: -------------------------------------------------------------------------------- 1 | const AbstractService = require('../AbstractService'); 2 | 3 | class CloudFormationService extends AbstractService { 4 | getOutputs() { 5 | const params = { 6 | StackName: this.stackName 7 | }; 8 | 9 | return this.awsRequest('CloudFormation:describeStacks', params) 10 | .then(({ Stacks }) => Stacks && Stacks[0].Outputs) 11 | .catch(() => []); 12 | } 13 | } 14 | 15 | module.exports = CloudFormationService; 16 | -------------------------------------------------------------------------------- /src/aws/LayersService.js: -------------------------------------------------------------------------------- 1 | const AbstractService = require('../AbstractService'); 2 | 3 | class LayersService extends AbstractService { 4 | 5 | descriptionWithVersionKey(versionKey) { 6 | return 'created by serverless-layers plugin (' + versionKey + ')' 7 | } 8 | 9 | async publishVersion(versionKey) { 10 | const params = { 11 | Content: { 12 | S3Bucket: this.bucketName, 13 | S3Key: this.zipFileKeyName 14 | }, 15 | LayerName: this.layerName, 16 | Description: this.descriptionWithVersionKey(versionKey), 17 | 18 | CompatibleRuntimes: this.plugin.settings.compatibleRuntimes, 19 | CompatibleArchitectures: this.plugin.settings.compatibleArchitectures 20 | }; 21 | 22 | return this.awsRequest('Lambda:publishLayerVersion', params, { checkError: true }) 23 | .then((result) => { 24 | this.plugin.log('New layer version published...'); 25 | this.plugin.cacheObject.LayerVersionArn = result.LayerVersionArn; 26 | return result; 27 | }); 28 | } 29 | 30 | 31 | async findVersionChecksumInList(versionKey, marker) { 32 | const params = { 33 | LayerName: this.layerName, 34 | // TODO: Question: is layer name specific enough? Is there a way to deploy multiple runtime architectures per name? 35 | // CompatibleRuntime: this.plugin.settings.compatibleRuntimes, 36 | // CompatibleArchitecture: this.plugin.settings.compatibleArchitectures 37 | }; 38 | 39 | if (marker) { 40 | params.Marker = marker; 41 | } 42 | 43 | const result = await this.awsRequest('Lambda:listLayerVersions', params, { checkError: true }); 44 | 45 | const description = this.descriptionWithVersionKey(versionKey); 46 | 47 | const matchingLayerVersion = result.LayerVersions.find((layer) => layer.Description === description); 48 | if (matchingLayerVersion) { 49 | return matchingLayerVersion.LayerVersionArn; 50 | } else if (result.NextMarker) { 51 | return this.findVersionChecksumInList(versionKey, result.NextMarker); 52 | } else { 53 | return null; 54 | } 55 | } 56 | 57 | async checkLayersForVersionKey(versionKey) { 58 | this.plugin.log('Looking for version with "' + versionKey + '"'); 59 | const layerVersionArn = await this.findVersionChecksumInList(versionKey); 60 | 61 | if (layerVersionArn) { 62 | return layerVersionArn; 63 | // TODO: double-check to confirm layer content is as expected 64 | // const params = { arn: layerVersionArn } 65 | // const matchingLayerWithContent = await this.awsRequest('Lambda:getLayerVersionByArn', params, { checkError: true }); 66 | // if (matchingLayerWithContent) { 67 | // matchingLayerWithContent.Content.Location // A link to the layer archive in Amazon S3 that is valid for 10 minutes. 68 | // } 69 | } 70 | } 71 | 72 | async cleanUpLayers(retainVersions = 0) { 73 | const params = { 74 | LayerName: this.layerName 75 | }; 76 | 77 | const response = await this.awsRequest('Lambda:listLayerVersions', params, { checkError: true }); 78 | 79 | if (response.LayerVersions.length <= retainVersions) { 80 | this.plugin.log('Layers removal finished.\n'); 81 | return; 82 | } 83 | 84 | if (this.plugin.settings.retainVersions) { 85 | const deletionCandidates = this.selectVersionsToDelete(response.LayerVersions, retainVersions); 86 | 87 | const deleteQueue = deletionCandidates.map((layerVersion) => { 88 | this.plugin.log(`Removing layer version: ${layerVersion.Version}`); 89 | return this.awsRequest('Lambda:deleteLayerVersion', { 90 | LayerName: this.layerName, 91 | VersionNumber: layerVersion.Version 92 | }, { checkError: true }); 93 | }); 94 | 95 | await Promise.all(deleteQueue); 96 | 97 | await this.cleanUpLayers(retainVersions); 98 | } 99 | } 100 | 101 | selectVersionsToDelete(versions, retainVersions) { 102 | return versions 103 | .sort((a, b) => parseInt(a.Version) === parseInt(b.Version) ? 0 : parseInt(a.Version) > parseInt(b.Version) ? -1 : 1) 104 | .slice(retainVersions); 105 | } 106 | } 107 | 108 | module.exports = LayersService; 109 | -------------------------------------------------------------------------------- /src/aws/S3Key.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | class S3Key { 4 | constructor(filename) { 5 | this.filename = filename; 6 | } 7 | 8 | getKey() { 9 | let SLASH = '/'; 10 | if (/^win/.test(process.platform)) { 11 | SLASH = '\\'; 12 | } 13 | return this.filename.replace(`${process.cwd()}${SLASH}`, ''); 14 | } 15 | 16 | getPath() { 17 | return this.filename; 18 | } 19 | 20 | getStream() { 21 | return fs.createReadStream(this.filename); 22 | } 23 | } 24 | 25 | module.exports = S3Key; 26 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const BbPromise = require('bluebird'); 2 | const path = require('path'); 3 | const slugify = require('slugify'); 4 | const chalk = require('chalk'); 5 | const semver = require('semver'); 6 | const Runtimes = require('./runtimes'); 7 | const LayersService = require('./aws/LayersService'); 8 | const BucketService = require('./aws/BucketService'); 9 | const CloudFormationService = require('./aws/CloudFormationService'); 10 | const ZipService = require('./package/ZipService'); 11 | const LocalFolders = require('./package/LocalFolders'); 12 | const Dependencies = require('./package/Dependencies'); 13 | 14 | class ServerlessLayers { 15 | constructor(serverless, options) { 16 | this.cacheObject = {}; 17 | this.options = options; 18 | this.serverless = serverless; 19 | this.initialized = false; 20 | 21 | // hooks 22 | this.hooks = { 23 | 'before:package:function:package': () => BbPromise.bind(this) 24 | .then(() => { 25 | return this.init() 26 | .then(() => this.deployLayers()) 27 | }), 28 | 'before:package:initialize': () => BbPromise.bind(this) 29 | .then(() => { 30 | return this.init() 31 | .then(() => this.deployLayers()) 32 | }), 33 | 'aws:info:displayLayers': () => BbPromise.bind(this) 34 | .then(() => this.init()) 35 | .then(() => this.finalizeDeploy()), 36 | 'after:deploy:function:deploy': () => BbPromise.bind(this) 37 | .then(() => this.init()) 38 | .then(() => this.finalizeDeploy()), 39 | 'after:deploy:deploy': () => BbPromise.bind(this) 40 | .then(() => this.init()) 41 | .then(() => this.cleanUpLayerVersions()), 42 | 'plugin:uninstall:uninstall': () => BbPromise.bind(this) 43 | .then(() => { 44 | return this.init() 45 | .then(() => this.cleanUpAllLayers()) 46 | }), 47 | 'remove:remove': () => BbPromise.bind(this) 48 | .then(() => { 49 | return this.init() 50 | .then(() => this.cleanUpAllLayers()) 51 | }) 52 | }; 53 | } 54 | 55 | async init() { 56 | if (this.initialized) { 57 | return; 58 | } 59 | 60 | this.provider = this.serverless.getProvider('aws'); 61 | this.service = this.serverless.service; 62 | this.options.region = this.provider.getRegion(); 63 | 64 | // bindings 65 | this.log = this.log.bind(this); 66 | this.main = this.main.bind(this); 67 | 68 | const version = this.serverless.getVersion(); 69 | 70 | if (semver.lt(version, '1.34.0')) { 71 | this.log(`Error: Please install serverless >= 1.34.0 (current ${this.serverless.getVersion()})`); 72 | process.exit(1); 73 | } 74 | } 75 | 76 | async deployLayers() { 77 | this.runtimes = new Runtimes(this); 78 | const settings = this.getSettings(); 79 | 80 | const cliOpts = this.provider.options; 81 | 82 | for (const layerName in settings) { 83 | const currentSettings = settings[layerName]; 84 | const enabledFuncs = currentSettings.functions; 85 | 86 | // deploying a single function 87 | const deploySingle = !!(cliOpts.function && enabledFuncs); 88 | 89 | // skip layers that is not related with specified function 90 | if (deploySingle && enabledFuncs.indexOf(cliOpts.function) === -1) { 91 | continue; 92 | } 93 | 94 | this.logGroup(layerName); 95 | await this.initServices(layerName, currentSettings); 96 | await this.main(); 97 | } 98 | 99 | this.breakLine(); 100 | } 101 | 102 | async cleanUpAllLayers() { 103 | this.runtimes = new Runtimes(this); 104 | const settings = this.getSettings(); 105 | for (const layerName in settings) { 106 | const currentSettings = settings[layerName]; 107 | this.logGroup(layerName); 108 | 109 | if (currentSettings.arn) { 110 | this.warn(` (skipped) arn: ${currentSettings.arn}`); 111 | continue; 112 | } 113 | 114 | await this.initServices(layerName, currentSettings); 115 | await this.cleanUpLayers(); 116 | } 117 | } 118 | 119 | async cleanUpLayerVersions() { 120 | this.runtimes = new Runtimes(this); 121 | const settings = this.getSettings(); 122 | 123 | for (const layerName in settings) { 124 | const currentSettings = settings[layerName]; 125 | this.logGroup(layerName); 126 | 127 | if (currentSettings.arn) { 128 | this.warn(` (skipped) arn: ${currentSettings.arn}`); 129 | continue; 130 | } 131 | 132 | if (!currentSettings.retainVersions) { 133 | continue; 134 | } 135 | 136 | this.log('Cleaning up layer versions...'); 137 | 138 | await this.initServices(layerName, currentSettings); 139 | await this.cleanUpLayers(currentSettings.retainVersions); 140 | } 141 | } 142 | 143 | async initServices(layerName, settings) { 144 | this.currentLayerName = layerName; 145 | this.settings = settings; 146 | this.zipService = new ZipService(this); 147 | this.dependencies = new Dependencies(this); 148 | this.localFolders = new LocalFolders(this); 149 | this.layersService = new LayersService(this); 150 | this.bucketService = new BucketService(this); 151 | this.cloudFormationService = new CloudFormationService(this); 152 | this.initialized = true; 153 | } 154 | 155 | mergeCommonSettings(inboundSetting) { 156 | const { deploymentBucketObject } = this.service.provider; 157 | 158 | let layersDeploymentBucketEncryption; 159 | if (deploymentBucketObject) { 160 | layersDeploymentBucketEncryption = deploymentBucketObject.serverSideEncryption; 161 | } 162 | 163 | return { 164 | path: '.', 165 | functions: null, 166 | forceInstall: false, 167 | retainVersions: null, 168 | dependencyInstall: true, 169 | compileDir: '.serverless', 170 | customInstallationCommand: null, 171 | layersDeploymentBucket: this.service.provider.deploymentBucket, 172 | layersDeploymentBucketEncryption, 173 | ...this.runtimes.getDefaultSettings(inboundSetting) 174 | }; 175 | } 176 | 177 | getSettings() { 178 | const inboundSettings = (this.serverless.service.custom || {})[ 179 | 'serverless-layers' 180 | ]; 181 | 182 | if (Array.isArray(inboundSettings)) { 183 | const settings = {}; 184 | inboundSettings.forEach(inboundSetting => { 185 | const layerName = Object.keys(inboundSetting)[0]; 186 | settings[layerName] = this.mergeCommonSettings(inboundSetting[layerName]); 187 | }); 188 | return settings; 189 | } 190 | 191 | return { 192 | default: this.mergeCommonSettings(inboundSettings) 193 | } 194 | } 195 | 196 | hasSettingsChanges() { 197 | // don't check settings changes twice 198 | if (this.hasSettingsVerified) { 199 | return false; 200 | } 201 | 202 | const manifest = '__meta__/manifest-settings.json'; 203 | const currentSettings = JSON.stringify({ 204 | ...this.settings, 205 | patterns: this.service.package.patterns 206 | }); 207 | 208 | // settings checked 209 | this.hasSettingsVerified = true; 210 | 211 | return this.bucketService.getFile(manifest).then((remoteSettings) => { 212 | 213 | // create and return true (changed) 214 | if (!remoteSettings) { 215 | return this.bucketService.putFile(manifest, currentSettings) 216 | .then(() => true); 217 | } 218 | 219 | if (remoteSettings !== currentSettings) { 220 | return this.bucketService.putFile(manifest, currentSettings) 221 | .then(() => true); 222 | } 223 | 224 | return false; 225 | }); 226 | } 227 | 228 | async hasCustomHashChanged() { 229 | if (!this.settings.customHash) { 230 | return false; 231 | } 232 | 233 | const hashFileName = 'customHash.json'; 234 | const remoteHashFile = await this.bucketService.getFile(hashFileName); 235 | 236 | if (!remoteHashFile) { 237 | this.log('no previous custom hash found, putting new remote hash'); 238 | await this.bucketService.putFile( 239 | hashFileName, JSON.stringify({ hash: this.settings.customHash }) 240 | ); 241 | return true; 242 | } 243 | 244 | const { hash: remoteHash } = JSON.parse(remoteHashFile); 245 | if (remoteHash === this.settings.customHash) { 246 | return false; 247 | } 248 | 249 | await this.bucketService.putFile( 250 | hashFileName, JSON.stringify({ hash: this.settings.customHash }) 251 | ); 252 | this.log('identified custom hash change!'); 253 | return true; 254 | } 255 | 256 | async main() { 257 | const { 258 | arn, 259 | localDir, 260 | artifact, 261 | forceInstall, 262 | dependencyInstall 263 | } = this.settings; 264 | 265 | // static ARN 266 | if (arn) { 267 | this.relateLayerWithFunctions(arn); 268 | return; 269 | } 270 | 271 | await this.runtimes.init(); 272 | await this.dependencies.init(); 273 | await this.localFolders.init(); 274 | 275 | // it avoids issues if user changes some configuration 276 | // which will not be applied till dependencies be changed 277 | let hasSettingsChanges = await this.hasSettingsChanges(); 278 | 279 | // check if directories content has changed 280 | // comparing hash md5 remote with local folder 281 | let hasFoldersChanges = false; 282 | if (localDir) { 283 | hasFoldersChanges = await this.localFolders.hasFoldersChanges(); 284 | } 285 | 286 | // check if dependencies has changed comparing 287 | // remote package.json with local one 288 | let hasDepsChanges = false; 289 | if (dependencyInstall) { 290 | hasDepsChanges = await this.runtimes.hasDependenciesChanges(); 291 | } 292 | 293 | let hasZipChanged = false; 294 | if (artifact) { 295 | hasZipChanged = await this.zipService.hasZipChanged(); 296 | } 297 | 298 | const hasCustomHashChanged = await this.hasCustomHashChanged(); 299 | 300 | // It checks if something has changed 301 | let verifyChanges = [ 302 | hasZipChanged, 303 | hasDepsChanges, 304 | hasFoldersChanges, 305 | hasSettingsChanges, 306 | hasCustomHashChanged, 307 | ].some(x => x === true); 308 | 309 | // merge package default options 310 | this.mergePackageOptions(); 311 | 312 | let existentLayerArn = ''; 313 | const versionKey = 314 | (this.runtimes.getDependenciesChecksum()) + 315 | (this.settings.customHash ? '.' + this.settings.customHash : ''); 316 | 317 | // If nothing has changed, confirm layer with same checksum 318 | if (!verifyChanges) { 319 | this.log('Checking if layer already exists...') 320 | existentLayerArn = await this.layersService.checkLayersForVersionKey(versionKey); 321 | } 322 | 323 | // It improves readability 324 | const skipInstallation = ( 325 | !verifyChanges && !forceInstall && existentLayerArn 326 | ); 327 | 328 | /** 329 | * If no changes, and layer arn available, 330 | * it doesn't require re-installing dependencies. 331 | */ 332 | if (skipInstallation) { 333 | this.log(`${chalk.inverse.green(' No changes ')}! Using same layer arn: ${this.logArn(existentLayerArn)}`); 334 | this.relateLayerWithFunctions(existentLayerArn); 335 | return; 336 | } 337 | 338 | // ENABLED by default 339 | if (dependencyInstall && !artifact) { 340 | await this.dependencies.install(); 341 | } 342 | 343 | if (localDir && !artifact) { 344 | await this.localFolders.copyFolders(); 345 | } 346 | 347 | await this.zipService.package(); 348 | await this.bucketService.uploadZipFile(); 349 | const version = await this.layersService.publishVersion(versionKey); 350 | await this.bucketService.putFile(this.dependencies.getDepsPath()); 351 | 352 | this.relateLayerWithFunctions(version.LayerVersionArn); 353 | } 354 | 355 | getLayerName() { 356 | const stackName = this.getStackName(); 357 | const { runtimeDir } = this.settings; 358 | return slugify(`${stackName}-${runtimeDir}-${this.currentLayerName}`, { 359 | lower: true, 360 | replacement: '-' 361 | }); 362 | } 363 | 364 | getStackName() { 365 | return this.provider.naming.getStackName(); 366 | } 367 | 368 | getBucketEncryptiom() { 369 | return this.settings.layersDeploymentBucketEncryption; 370 | } 371 | 372 | getBucketName() { 373 | if (!this.settings.layersDeploymentBucket) { 374 | throw new Error( 375 | 'Please, you should specify "deploymentBucket" or "layersDeploymentBucket" option for this plugin!\n' 376 | ); 377 | } 378 | return this.settings.layersDeploymentBucket; 379 | } 380 | 381 | getPathZipFileName() { 382 | if (this.settings.artifact) { 383 | return `${path.join(process.cwd(), this.settings.artifact)}`; 384 | } 385 | return `${path.join(process.cwd(), this.settings.compileDir, this.getLayerName())}.zip`; 386 | } 387 | 388 | getBucketLayersPath() { 389 | const serviceStage = `${this.serverless.service.service}/${this.options.stage}`; 390 | 391 | let deploymentPrefix = 'serverless'; 392 | if (this.provider.getDeploymentPrefix) { 393 | deploymentPrefix = this.provider.getDeploymentPrefix(); 394 | } 395 | 396 | return path.join( 397 | deploymentPrefix, 398 | serviceStage, 399 | 'layers' 400 | ).replace(/\\/g, '/'); 401 | } 402 | 403 | async getLayerArn() { 404 | if (!this.cacheObject.layersArn) { 405 | this.cacheObject.layersArn = {}; 406 | } 407 | 408 | // returns cached arn 409 | if (this.cacheObject.layersArn[this.currentLayerName]) { 410 | return this.cacheObject.layersArn[this.currentLayerName]; 411 | } 412 | 413 | const outputs = await this.cloudFormationService.getOutputs(); 414 | 415 | if (!outputs) return null; 416 | 417 | const logicalId = this.getOutputLogicalId(); 418 | 419 | const arn = (outputs.find(x => x.OutputKey === logicalId) || {}).OutputValue; 420 | 421 | // cache arn 422 | this.cacheObject.layersArn[this.currentLayerName] = arn; 423 | 424 | return arn; 425 | } 426 | 427 | getOutputLogicalId() { 428 | return this.provider.naming.getLambdaLayerOutputLogicalId(this.getLayerName()); 429 | } 430 | 431 | mergePackageOptions() { 432 | const { packagePatterns, artifact } = this.settings; 433 | const pkg = this.service.package; 434 | 435 | const opts = { 436 | individually: false, 437 | excludeDevDependencies: false, 438 | patterns: [] 439 | }; 440 | 441 | this.service.package = {...opts, ...pkg}; 442 | 443 | for (const excludeFile of packagePatterns) { 444 | const hasRule = (this.service.package.patterns || '').indexOf(excludeFile); 445 | if (hasRule === -1) { 446 | this.service.package.patterns.push(excludeFile); 447 | } 448 | } 449 | 450 | if (artifact) { 451 | this.service.package.patterns.push(artifact); 452 | } 453 | } 454 | 455 | relateLayerWithFunctions(layerArn) { 456 | this.log('Adding layers...'); 457 | const { functions } = this.service; 458 | const funcs = this.settings.functions; 459 | const cliOpts = this.provider.options; 460 | 461 | // Attaches to provider level when 462 | // no functions available. It happens when 463 | // someone want to use serverless to create all layers 464 | // or resources but not the functions. 465 | if (!functions || Object.keys(functions).length === 0) { 466 | // Simple validations when layers attribute is null. 467 | if (!this.service.provider.layers) { 468 | this.service.provider.layers = [] 469 | } 470 | 471 | this.service.provider.layers.push(layerArn); 472 | 473 | this.log( 474 | `${chalk.magenta.bold('provider')} - ${this.logArn(layerArn)}`, 475 | ' ✓' 476 | ); 477 | } else { 478 | Object.keys(functions).forEach(funcName => { 479 | if (cliOpts.function && cliOpts.function !== funcName) { 480 | return; 481 | } 482 | 483 | let isEnabled = !funcs; 484 | 485 | if (Array.isArray(funcs) && funcs.indexOf(funcName) !== -1) { 486 | isEnabled = true; 487 | } 488 | 489 | if (isEnabled) { 490 | // if this function has other layers add ours too so it applies 491 | functions[funcName].layers = functions[funcName].layers || []; 492 | functions[funcName].layers.push(layerArn); 493 | functions[funcName].layers = Array.from(new Set(functions[funcName].layers)); 494 | this.log(`function.${chalk.magenta.bold(funcName)} - ${this.logArn(layerArn)}`, ' ✓'); 495 | } else { 496 | this.warn(`(Skipped) function.${chalk.magenta.bold(funcName)}`, ` x`); 497 | } 498 | }); 499 | } 500 | 501 | this.service.resources = this.service.resources || {}; 502 | this.service.resources.Outputs = this.service.resources.Outputs || {}; 503 | 504 | const outputName = this.getOutputLogicalId(); 505 | 506 | Object.assign(this.service.resources.Outputs, { 507 | [outputName]: { 508 | Value: layerArn, 509 | Export: { 510 | Name: outputName 511 | } 512 | } 513 | }); 514 | } 515 | 516 | getDependenciesList() { 517 | return Object.keys((this.localPackage.dependencies||[])).map(x => ( 518 | `${x}@${this.localPackage.dependencies[x]}` 519 | )); 520 | } 521 | 522 | async finalizeDeploy() { 523 | const cliOpts = this.provider.options; 524 | this.logGroup("Layers Info"); 525 | Object.keys(this.service.functions).forEach(funcName => { 526 | const lambdaFunc = this.service.functions[funcName]; 527 | const layers = lambdaFunc.layers || []; 528 | 529 | if (!cliOpts.function && layers.length === 0) { 530 | this.warn(`(skipped) function.${chalk.magenta.bold(funcName)}`); 531 | return; 532 | } 533 | 534 | layers.forEach((currentLayerARN) => { 535 | if (cliOpts.function && cliOpts.function === funcName) { 536 | this.log(`function.${chalk.magenta.bold(funcName)} = layers.${this.logArn(currentLayerARN)}`); 537 | return; 538 | } 539 | this.log(`function.${chalk.magenta.bold(funcName)} = layers.${this.logArn(currentLayerARN)}`); 540 | }); 541 | }); 542 | this.breakLine(); 543 | } 544 | 545 | log(msg, signal=' ○') { 546 | console.log('...' + `${chalk.greenBright.bold(signal)} ${chalk.white(msg)}`); 547 | } 548 | 549 | logGroup(msg) { 550 | this.breakLine(); 551 | this.serverless.cli.log(`[ LayersPlugin ]: ${chalk.magenta.bold('=>')} ${chalk.greenBright.bold(msg)}`); 552 | } 553 | 554 | warn(msg, signal=' ∅') { 555 | console.log('...' + chalk.yellowBright(`${chalk.yellowBright.bold(signal)} ${msg}`)); 556 | } 557 | 558 | error(msg, signal=' ⊗') { 559 | console.log('...' + chalk.red(`${signal} ${chalk.white.bold(msg)}`)); 560 | } 561 | 562 | cleanUpLayers(retainVersions) { 563 | return this.layersService.cleanUpLayers(retainVersions); 564 | } 565 | 566 | breakLine() { 567 | console.log('\n'); 568 | } 569 | 570 | logArn(arn) { 571 | let pattern = /arn:aws:lambda:([^:]+):([0-9]+):layer:([^:]+):([0-9]+)/g; 572 | let region = chalk.bold('$1'); 573 | let name = chalk.magenta('$3'); 574 | let formatted = chalk.white(`arn:aws:lambda:${region}:*********:${name}:$4`); 575 | 576 | let text = ""; 577 | switch (typeof arn) { 578 | case 'object': 579 | if (arn.Ref) { 580 | text = `logicalId:[${chalk.bold('Ref')}=`; 581 | text += `${chalk.magenta(arn.Ref)}]`; 582 | } 583 | break; 584 | case 'string': 585 | text = arn; 586 | break; 587 | default: 588 | text = String(arn); 589 | break; 590 | } 591 | return text.replace(pattern, formatted); 592 | } 593 | } 594 | 595 | module.exports = ServerlessLayers; 596 | -------------------------------------------------------------------------------- /src/package/Dependencies.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const chalk = require('chalk'); 3 | const fsExtra = require('fs-extra'); 4 | const glob = require('glob'); 5 | const path = require('path'); 6 | const mkdirp = require('mkdirp'); 7 | const { execSync } = require('child_process'); 8 | const copyFile = require('fs-copy-file'); // node v6.10.3 support 9 | 10 | const AbstractService = require('../AbstractService'); 11 | 12 | function resolveFile(from, opts = {}) { 13 | return new Promise((resolve, reject) => { 14 | glob(from, opts, (err, files) => { 15 | if (err) return reject(); 16 | return resolve(files); 17 | }); 18 | }); 19 | } 20 | 21 | class Dependencies extends AbstractService { 22 | init() { 23 | this.layersPackageDir = this.getLayerPackageDir(); 24 | fs.rmSync(this.layersPackageDir, {force: true, recursive: true}); 25 | return mkdirp.sync(this.layersPackageDir); 26 | } 27 | 28 | getDepsPath() { 29 | const { settings } = this.plugin; 30 | const rooPath = path.join(settings.path, settings.dependenciesPath); 31 | 32 | return path.resolve(rooPath); 33 | } 34 | 35 | /** 36 | * Implementing package pattern ignore 37 | * https://github.com/agutoli/serverless-layers/issues/118 38 | */ 39 | async excludePatternFiles() { 40 | let filesToIgnore = []; 41 | let filesToExclude = []; 42 | 43 | /** 44 | * Patterns allows you to define globs that will be excluded / included from the 45 | * resulting artifact. If you wish to exclude files you can use a glob pattern prefixed 46 | * with ! such as !exclude-me/**. Serverless will run the glob patterns in order so 47 | * you can always re-include previously excluded files and directories. 48 | * 49 | * Reference: https://www.serverless.com/framework/docs/providers/aws/guide/packaging 50 | */ 51 | for (let pattern of this.plugin.settings.layerOptimization.cleanupPatterns) { 52 | if (pattern.startsWith('!')) { 53 | const resolvedFiles = await resolveFile(pattern.substr(1), { 54 | cwd: this.layersPackageDir 55 | }); 56 | filesToIgnore = filesToIgnore.concat(resolvedFiles); 57 | } else { 58 | // change directory 59 | const resolvedFiles = await resolveFile(pattern, { 60 | cwd: this.layersPackageDir 61 | }); 62 | filesToExclude = filesToExclude.concat(resolvedFiles); 63 | } 64 | } 65 | 66 | filesToExclude.forEach((filename) => { 67 | // check if folder or files are being ignored, and shouldn't be removed. 68 | const shouldBeIgnored = filesToIgnore.filter(x => x.startsWith(filename)).length > 0; 69 | 70 | if (!shouldBeIgnored) { 71 | this.plugin.warn(`[layerOptimization.cleanupPatterns] Ignored: ${filename}`); 72 | fs.rmSync(path.join(this.layersPackageDir, filename), {force: true, recursive: true}); 73 | } 74 | }); 75 | } 76 | 77 | async run(cmd) { 78 | const output = execSync(cmd, { 79 | cwd: this.layersPackageDir, 80 | env: process.env, 81 | maxBuffer: 1024 * 1024 * 500 82 | }).toString(); 83 | return output; 84 | } 85 | 86 | copyProjectFile(filePath, fileName = null) { 87 | this.init(); 88 | 89 | if (!fs.existsSync(filePath)) { 90 | this.plugin.warn(`[warning] "${filePath}" file does not exists!`); 91 | return true; 92 | } 93 | 94 | return new Promise((resolve) => { 95 | const destFile = path.join(this.layersPackageDir, fileName || path.basename(filePath)); 96 | copyFile(filePath, destFile, (copyErr) => { 97 | if (copyErr) throw copyErr; 98 | return resolve(); 99 | }); 100 | }); 101 | } 102 | 103 | async install() { 104 | const { copyBeforeInstall, copyAfterInstall } = this.plugin.settings; 105 | 106 | this.init(); 107 | this.plugin.log(`${chalk.inverse.yellow(' Changes identified ')}! Re-installing...`); 108 | 109 | /** 110 | * This is necessary because npm is 111 | * not possible to specify a custom 112 | * name for package.json. 113 | */ 114 | let renameFilename = null; 115 | if (this.plugin.settings.runtimeDir === 'nodejs') { 116 | renameFilename = 'package.json'; 117 | } 118 | 119 | await this.copyProjectFile(this.getDepsPath(), renameFilename); 120 | 121 | for (const index in copyBeforeInstall) { 122 | const filename = copyBeforeInstall[index]; 123 | await this.copyProjectFile(filename); 124 | } 125 | 126 | // custom commands 127 | if (this.plugin.settings.customInstallationCommand) { 128 | console.log(chalk.white(await this.run(this.plugin.settings.customInstallationCommand))); 129 | } else { 130 | const commands = this.plugin.runtimes.getCommands(); 131 | const {packageManagerExtraArgs, packageManager} = this.plugin.settings; 132 | const installCommand = `${commands[packageManager]} ${packageManagerExtraArgs}`; 133 | this.plugin.log(chalk.white.bold(installCommand)); 134 | console.log(chalk.white(await this.run(installCommand))); 135 | } 136 | 137 | for (const index in copyAfterInstall) { 138 | const pathTo = copyAfterInstall[index].to; 139 | const pathFrom = copyAfterInstall[index].from; 140 | 141 | const [from] = await resolveFile(path.join(this.layersPackageDir, pathFrom)); 142 | const to = path.join(this.layersPackageDir, pathTo); 143 | 144 | try { 145 | await fsExtra.copy(from, to); 146 | } catch (e) { 147 | console.log(e); 148 | } 149 | } 150 | 151 | // cleanup files 152 | try { 153 | await this.excludePatternFiles(); 154 | } catch(err) { 155 | if (!this.plugin.service.package.patterns) { 156 | this.plugin.warn(`[warning] package.patterns option is not set. @see https://www.serverless.com/framework/docs/providers/aws/guide/packaging`); 157 | } else { 158 | console.error(err); 159 | process.exit(1); 160 | } 161 | } 162 | } 163 | } 164 | 165 | module.exports = Dependencies; 166 | -------------------------------------------------------------------------------- /src/package/LocalFolders.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fsExtra = require('fs-extra'); 3 | const mkdirp = require('mkdirp'); 4 | const { hashElement } = require('folder-hash'); 5 | 6 | const AbstractService = require('../AbstractService'); 7 | 8 | class LocalFolders extends AbstractService { 9 | init() { 10 | this.layersPackageDir = this.getLayerPackageDir(); 11 | return mkdirp.sync(this.layersPackageDir); 12 | } 13 | 14 | getManifestName(hashName) { 15 | return `__meta__/manifest-localdir-${hashName.toLowerCase()}.json`; 16 | } 17 | 18 | async getHash() { 19 | const { settings } = this.plugin; 20 | 21 | if (!settings.localDir) { 22 | return; 23 | } 24 | 25 | const options = { 26 | folders: settings.localDir.folders, 27 | files: settings.localDir.files 28 | }; 29 | 30 | return hashElement(settings.localDir.path, options); 31 | } 32 | 33 | async hasFoldersChanges() { 34 | return this.getHash().then((hash) => { 35 | const manifest = this.getManifestName(hash.name); 36 | return this.plugin.bucketService.getFile(manifest).then(remoteManifest => { 37 | if (remoteManifest === JSON.stringify(hash)) { 38 | // not changed 39 | return false; 40 | } 41 | return true; 42 | }); 43 | }); 44 | } 45 | 46 | async copyFolders() { 47 | const { settings } = this.plugin; 48 | 49 | if (!settings.localDir) { 50 | return; 51 | } 52 | 53 | await this.getHash() 54 | .then((hash) => { 55 | const manifest = this.getManifestName(hash.name); 56 | const to = path.join(this.layersPackageDir, settings.libraryFolder, settings.localDir.name); 57 | const from = path.resolve(settings.localDir.path); 58 | 59 | return fsExtra.copy(from, to).then(() => { 60 | return this.plugin.bucketService.putFile(manifest, JSON.stringify(hash)); 61 | }); 62 | }); 63 | } 64 | } 65 | 66 | module.exports = LocalFolders; 67 | -------------------------------------------------------------------------------- /src/package/ZipService.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const chalk = require('chalk'); 4 | const mkdirp = require('mkdirp'); 5 | const crypto = require('crypto'); 6 | const fsExtra = require('fs-extra'); 7 | const archiver = require('archiver'); 8 | 9 | const MAX_LAYER_MB_SIZE = 250; 10 | 11 | const AbstractService = require('../AbstractService'); 12 | 13 | class ZipService extends AbstractService { 14 | getManifestName(hashName) { 15 | return `__meta__/manifest-zip-artifact__${this.functionName}.json`; 16 | } 17 | 18 | getChecksum(path) { 19 | return new Promise(function (resolve, reject) { 20 | const hash = crypto.createHash('md5'); 21 | const input = fs.createReadStream(path); 22 | 23 | input.on('error', reject); 24 | 25 | input.on('data', function (chunk) { 26 | hash.update(chunk); 27 | }); 28 | 29 | input.on('close', function () { 30 | resolve(hash.digest('hex')); 31 | }); 32 | }); 33 | } 34 | 35 | async hasZipChanged() { 36 | const { artifact } = this.plugin.settings; 37 | const mName = this.getManifestName(artifact); 38 | 39 | const currentChecksum = await this.getChecksum(artifact); 40 | const remoteChecksum = await this.plugin.bucketService.getFile(mName); 41 | 42 | // check if zip hash changed 43 | if (remoteChecksum === currentChecksum) { 44 | return false; 45 | } 46 | 47 | // It updates remote check sum 48 | this.plugin.log(`${chalk.inverse.yellow(' Artifact changed ')}! Checksum=${currentChecksum}`); 49 | await this.plugin.bucketService.putFile(mName, currentChecksum); 50 | 51 | return true; 52 | } 53 | 54 | package() { 55 | const { compileDir, artifact } = this.plugin.settings; 56 | const zipFileName = this.plugin.getPathZipFileName(); 57 | const layersDir = path.join(process.cwd(), compileDir); 58 | 59 | return new Promise((resolve, reject) => { 60 | // it's a zip already 61 | if (artifact) { 62 | // It checks if file exists 63 | if (!fs.existsSync(zipFileName)) { 64 | throw Error(`Artifact not found "${zipFileName}".`); 65 | } 66 | return resolve(); 67 | } 68 | 69 | const oldCwd = process.cwd(); 70 | const output = fs.createWriteStream(zipFileName); 71 | const zip = archiver.create('zip'); 72 | 73 | output.on('close', () => { 74 | const MB = (zip.pointer() / 1024 / 1024).toFixed(1); 75 | 76 | if (MB > MAX_LAYER_MB_SIZE) { 77 | this.plugin.log('Package error!'); 78 | throw new Error( 79 | 'Layers can\'t exceed the unzipped deployment package size limit of 250 MB! \n' 80 | + 'Read more: https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html\n\n' 81 | ); 82 | } 83 | 84 | this.plugin.log(`Created layer package ${zipFileName} (${MB} MB)`); 85 | resolve(); 86 | }); 87 | 88 | zip.on('error', (err) => { 89 | reject(err); 90 | process.chdir(oldCwd); 91 | }); 92 | 93 | process.chdir(layersDir); 94 | 95 | zip.pipe(output); 96 | 97 | zip.directory('layers', false); 98 | 99 | zip.finalize() 100 | .then(() => { 101 | process.chdir(oldCwd); 102 | }); 103 | }); 104 | } 105 | } 106 | 107 | module.exports = ZipService; 108 | -------------------------------------------------------------------------------- /src/runtimes/index.js: -------------------------------------------------------------------------------- 1 | const { exec } = require('child_process'); 2 | 3 | const ruby = require('./ruby'); 4 | const nodejs = require('./nodejs'); 5 | const python = require('./python'); 6 | 7 | class Runtimes { 8 | constructor(plugin) { 9 | this.plugin = plugin; 10 | this.compatibleArchitectures = ["x86_64","arm64"]; 11 | 12 | const { runtime } = this.plugin.service.provider; 13 | 14 | if (!runtime) { 15 | this.plugin.error('service.provider.runtime is required!'); 16 | return process.exit(1); 17 | } 18 | 19 | const patterns = { 20 | python: [/python/, python], 21 | nodejs: [/node/, nodejs], 22 | ruby: [/ruby/, ruby], 23 | }; 24 | 25 | for (const env in patterns) { 26 | if (patterns[env][0].test(runtime)) { 27 | this._runtime = new patterns[env][1](this, runtime, env); 28 | break; 29 | } 30 | } 31 | 32 | if (!this._runtime) { 33 | this.plugin.log(`"${runtime}" runtime is not supported (yet).`); 34 | return process.exit(1); 35 | } 36 | 37 | this._runtime.isCompatibleVersion(runtime) 38 | .then((data) => { 39 | if (!data.isCompatible) { 40 | this.plugin.warn('============================================================='); 41 | this.plugin.warn(`WARN: The current environment and Lambda runtime don't match (current=${data.version.replace('\n', '')} vs runtime=${runtime}).`); 42 | this.plugin.warn('=============================================================\n'); 43 | } 44 | }); 45 | } 46 | 47 | init() { 48 | this._runtime.init(); 49 | } 50 | 51 | run(cmd) { 52 | return new Promise((resolve, reject) => { 53 | exec(cmd, (err, stdout, out) => { 54 | if (err) return reject(err); 55 | return resolve(stdout || out); 56 | }); 57 | }); 58 | } 59 | 60 | getDefaultSettings(inboundSettings = {}) { 61 | if (inboundSettings.packagePath) { 62 | console.warn('WARN You should use "dependenciesPath" instead of the deprecated "packagePath" param.'); 63 | inboundSettings.dependenciesPath = inboundSettings.packagePath; 64 | } 65 | return { ...this._runtime.default, ...inboundSettings }; 66 | } 67 | 68 | getCommands() { 69 | return this._runtime.commands; 70 | } 71 | 72 | hasDependenciesChanges() { 73 | return this._runtime.hasDependenciesChanges(); 74 | } 75 | 76 | getDependenciesChecksum() { 77 | return this._runtime.getDependenciesChecksum(); 78 | } 79 | } 80 | 81 | module.exports = Runtimes; 82 | -------------------------------------------------------------------------------- /src/runtimes/nodejs.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const crypto = require('crypto'); 3 | 4 | class NodeJSRuntime { 5 | constructor(parent, runtime, runtimeDir) { 6 | this.parent = parent; 7 | this.plugin = parent.plugin; 8 | 9 | this.default = { 10 | runtime, 11 | runtimeDir, 12 | libraryFolder: 'node_modules', 13 | packageManager: 'npm', 14 | packageManagerExtraArgs: '', 15 | dependenciesPath: 'package.json', 16 | compatibleRuntimes: [runtimeDir], 17 | compatibleArchitectures: parent.compatibleArchitectures, 18 | copyBeforeInstall: [ 19 | '.npmrc', 20 | 'yarn.lock', 21 | 'package-lock.json' 22 | ], 23 | packagePatterns: [ 24 | '!node_modules/**', 25 | ], 26 | layerOptimization: { 27 | cleanupPatterns: [ 28 | "node_modules/**/.github", 29 | "node_modules/**/.git/*", 30 | "node_modules/**/.lint", 31 | "node_modules/**/Gruntfile.js", 32 | "node_modules/**/.jshintrc", 33 | "node_modules/**/.nycrc", 34 | "node_modules/**/.nvmrc", 35 | "node_modules/**/.editorconfig", 36 | "node_modules/**/.npmignore", 37 | "node_modules/**/bower.json", 38 | "node_modules/**/.eslint*", 39 | "node_modules/**/.gitignore", 40 | "node_modules/**/README.*", 41 | "node_modules/**/LICENSE", 42 | "node_modules/**/LICENSE.md", 43 | "node_modules/**/CHANGES", 44 | "node_modules/**/HISTORY.md", 45 | "node_modules/**/CHANGES.md", 46 | "node_modules/**/CHANGELOG.md", 47 | "node_modules/**/sponsors.md", 48 | "node_modules/**/license.txt", 49 | "node_modules/**/tsconfig.json", 50 | "node_modules/**/*.test.js", 51 | "node_modules/**/*.spec.js", 52 | "node_modules/**/.travis.y*ml", 53 | "node_modules/**/yarn.lock", 54 | "node_modules/**/.package-lock.json", 55 | "node_modules/**/*.md", 56 | ] 57 | } 58 | }; 59 | 60 | this.commands = { 61 | npm: 'npm install --production --only=prod', 62 | yarn: 'yarn --production', 63 | pnpm: 'pnpm install --prod' 64 | }; 65 | } 66 | 67 | init() { 68 | const { dependenciesPath } = this.plugin.settings; 69 | 70 | const localpackageJson = path.join( 71 | process.cwd(), 72 | dependenciesPath 73 | ); 74 | 75 | try { 76 | this.localPackage = require(localpackageJson); 77 | } catch (e) { 78 | this.plugin.log(`Error: Can not find ${localpackageJson}!`); 79 | process.exit(1); 80 | } 81 | } 82 | 83 | async isCompatibleVersion(runtime) { 84 | const osVersion = await this.parent.run('node --version'); 85 | const [runtimeVersion] = runtime.match(/([0-9]+)\./); 86 | return { 87 | version: osVersion, 88 | isCompatible: osVersion.startsWith(`v${runtimeVersion}`) 89 | }; 90 | } 91 | 92 | isDiff(depsA, depsB) { 93 | if (!depsA) { 94 | return true; 95 | } 96 | 97 | const depsKeyA = Object.keys(depsA); 98 | const depsKeyB = Object.keys(depsB); 99 | const isSizeEqual = depsKeyA.length === depsKeyB.length; 100 | 101 | if (!isSizeEqual) return true; 102 | 103 | let hasDifference = false; 104 | Object.keys(depsA).forEach(dependence => { 105 | if (depsA[dependence] !== depsB[dependence]) { 106 | hasDifference = true; 107 | } 108 | }); 109 | 110 | return hasDifference; 111 | } 112 | 113 | async hasDependenciesChanges() { 114 | const remotePackage = await this.plugin.bucketService.downloadDependencesFile(); 115 | 116 | let isDifferent = true; 117 | 118 | if (remotePackage) { 119 | const parsedRemotePackage = JSON.parse(remotePackage); 120 | const { dependencies } = parsedRemotePackage; 121 | this.plugin.log('Comparing package.json dependencies...'); 122 | isDifferent = await this.isDiff(dependencies, this.localPackage.dependencies); 123 | } 124 | 125 | return isDifferent; 126 | } 127 | 128 | getDependenciesChecksum() { 129 | return crypto.createHash('md5').update(JSON.stringify(this.localPackage.dependencies)).digest('hex'); 130 | } 131 | } 132 | 133 | module.exports = NodeJSRuntime; 134 | -------------------------------------------------------------------------------- /src/runtimes/python.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const crypto = require('crypto'); 4 | 5 | class PythonRuntime { 6 | constructor(parent, runtime, runtimeDir) { 7 | this.parent = parent; 8 | this.plugin = parent.plugin; 9 | 10 | this.default = { 11 | runtime, 12 | runtimeDir, 13 | libraryFolder: 'site-packages', 14 | packageManager: 'pip', 15 | packageManagerExtraArgs: '', 16 | dependenciesPath: 'requirements.txt', 17 | compatibleRuntimes: [runtime], 18 | compatibleArchitectures: parent.compatibleArchitectures, 19 | copyBeforeInstall: [], 20 | packagePatterns: [ 21 | '!package.json', 22 | '!package-lock.json', 23 | '!node_modules/**', 24 | ], 25 | layerOptimization: { 26 | cleanupPatterns: [ 27 | "node_modules/**/*.pyc", 28 | "node_modules/**/*.md", 29 | ] 30 | } 31 | }; 32 | 33 | this.commands = { 34 | pip: `pip install -r ${this.default.dependenciesPath} -t .`, 35 | }; 36 | } 37 | 38 | init() { 39 | const { dependenciesPath } = this.plugin.settings; 40 | 41 | const localpackageJson = path.join( 42 | process.cwd(), 43 | dependenciesPath 44 | ); 45 | 46 | try { 47 | this.localPackage = fs.readFileSync(localpackageJson).toString(); 48 | } catch (e) { 49 | this.plugin.log(`Error: Can not find ${localpackageJson}!`); 50 | process.exit(1); 51 | } 52 | } 53 | 54 | async isCompatibleVersion(runtime) { 55 | const osVersion = await this.parent.run('python --version'); 56 | const [runtimeVersion] = runtime.match(/[0-9].[0-9]/); 57 | return { 58 | version: osVersion, 59 | isCompatible: osVersion.startsWith(`Python ${runtimeVersion}`) 60 | }; 61 | } 62 | 63 | isDiff(depsA, depsB) { 64 | if (!depsA) { 65 | return true; 66 | } 67 | return depsA !== depsB; 68 | } 69 | 70 | async hasDependenciesChanges() { 71 | const remotePackage = await this.plugin.bucketService.downloadDependencesFile(); 72 | 73 | let isDifferent = true; 74 | 75 | if (remotePackage) { 76 | this.plugin.log(`Comparing ${this.default.dependenciesPath} dependencies...`); 77 | isDifferent = await this.isDiff(remotePackage, this.localPackage); 78 | } 79 | 80 | return isDifferent; 81 | } 82 | 83 | getDependenciesChecksum() { 84 | return crypto.createHash('md5').update(JSON.stringify(this.localPackage)).digest('hex'); 85 | } 86 | } 87 | 88 | module.exports = PythonRuntime; 89 | -------------------------------------------------------------------------------- /src/runtimes/ruby.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const crypto = require('crypto'); 4 | 5 | class RubyRuntime { 6 | constructor(parent, runtime, runtimeDir) { 7 | this.parent = parent; 8 | this.plugin = parent.plugin; 9 | 10 | this.default = { 11 | runtime, 12 | runtimeDir, 13 | libraryFolder: 'gems', 14 | packageManager: 'bundle', 15 | packageManagerExtraArgs: '', 16 | dependenciesPath: 'Gemfile', 17 | compatibleRuntimes: [runtime], 18 | compatibleArchitectures: parent.compatibleArchitectures, 19 | copyBeforeInstall: [ 20 | 'Gemfile.lock' 21 | ], 22 | copyAfterInstall: [ 23 | { from: 'ruby', to: 'gems' } 24 | ], 25 | packagePatterns: [ 26 | '!node_modules/**', 27 | '!package.json', 28 | '!package-lock.json', 29 | '!vendor/**', 30 | '!.bundle' 31 | ], 32 | layerOptimization: { 33 | cleanupPatterns: [ 34 | "node_modules/**/*.pyc", 35 | "node_modules/**/*.md", 36 | ] 37 | } 38 | }; 39 | 40 | this.commands = { 41 | bundle: `bundle install --gemfile=${this.default.dependenciesPath} --path=./`, 42 | }; 43 | } 44 | 45 | init() { 46 | const { dependenciesPath } = this.plugin.settings; 47 | 48 | const localpackageJson = path.join( 49 | process.cwd(), 50 | dependenciesPath 51 | ); 52 | 53 | try { 54 | this.localPackage = fs.readFileSync(localpackageJson).toString(); 55 | } catch (e) { 56 | this.plugin.log(`Error: Can not find ${localpackageJson}!`); 57 | process.exit(1); 58 | } 59 | } 60 | 61 | async isCompatibleVersion(runtime) { 62 | const osVersion = await this.parent.run('ruby --version'); 63 | const [runtimeVersion] = runtime.match(/[0-9].[0-9]/); 64 | return { 65 | version: osVersion, 66 | isCompatible: osVersion.startsWith(`ruby ${runtimeVersion}`) 67 | }; 68 | } 69 | 70 | isDiff(depsA, depsB) { 71 | if (!depsA) { 72 | return true; 73 | } 74 | return depsA !== depsB; 75 | } 76 | 77 | async hasDependenciesChanges() { 78 | const remotePackage = await this.plugin.bucketService.downloadDependencesFile(); 79 | 80 | let isDifferent = true; 81 | 82 | if (remotePackage) { 83 | this.plugin.log(`Comparing ${this.default.dependenciesPath} dependencies...`); 84 | isDifferent = await this.isDiff(remotePackage, this.localPackage); 85 | } 86 | 87 | return isDifferent; 88 | } 89 | 90 | getDependenciesChecksum() { 91 | return crypto.createHash('md5').update(JSON.stringify(this.localPackage)).digest('hex'); 92 | } 93 | } 94 | 95 | module.exports = RubyRuntime; 96 | -------------------------------------------------------------------------------- /tests/fixtures/nodejsConfig.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | runtime: 'nodejs12.x', 3 | runtimeDir: 'nodejs', 4 | packageManager: 'yarn', 5 | packageManagerExtraArgs: '', 6 | libraryFolder: 'node_modules', 7 | dependenciesPath: './fixtures/package.json', 8 | layerOptimization: { 9 | cleanupPatterns: [ 10 | "node_modules/**/.github", 11 | "node_modules/**/.git/*", 12 | "node_modules/**/.lint", 13 | "node_modules/**/Gruntfile.js", 14 | "node_modules/**/.jshintrc", 15 | "node_modules/**/.nycrc", 16 | "node_modules/**/.nvmrc", 17 | "node_modules/**/.editorconfig", 18 | "node_modules/**/.npmignore", 19 | "node_modules/**/bower.json", 20 | "node_modules/**/.eslint*", 21 | "node_modules/**/.gitignore", 22 | "node_modules/**/README.*", 23 | "node_modules/**/LICENSE", 24 | "node_modules/**/LICENSE.md", 25 | "node_modules/**/CHANGES", 26 | "node_modules/**/HISTORY.md", 27 | "node_modules/**/CHANGES.md", 28 | "node_modules/**/CHANGELOG.md", 29 | "node_modules/**/sponsors.md", 30 | "node_modules/**/license.txt", 31 | "node_modules/**/tsconfig.json", 32 | "node_modules/**/*.test.js", 33 | "node_modules/**/*.spec.js", 34 | "node_modules/**/.travis.y*ml", 35 | "node_modules/**/yarn.lock", 36 | "node_modules/**/.package-lock.json", 37 | "node_modules/**/*.md" 38 | ] 39 | }, 40 | compatibleRuntimes: [ 'nodejs' ], 41 | compatibleArchitectures: [ 42 | "x86_64", 43 | "arm64" 44 | ], 45 | copyBeforeInstall: [ '.npmrc', 'yarn.lock', 'package-lock.json' ], 46 | packagePatterns: [ '!node_modules/**' ] 47 | } 48 | -------------------------------------------------------------------------------- /tests/fixtures/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-fixture", 3 | "dependencies": { 4 | "express": "^1.2.3" 5 | }, 6 | "devDependencies": { 7 | "anyone-deps": "123" 8 | } 9 | } -------------------------------------------------------------------------------- /tests/fixtures/pythonConfig.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | runtimeDir: 'python', 3 | libraryFolder: 'site-packages', 4 | packageManager: 'pip', 5 | packageManagerExtraArgs: '', 6 | dependenciesPath: 'requirements.txt', 7 | compatibleRuntimes: ["python3.8"], 8 | copyBeforeInstall: [], 9 | packageExclude: [ 10 | 'package.json', 11 | 'package-lock.json', 12 | 'node_modules/**', 13 | ] 14 | } 15 | 16 | -------------------------------------------------------------------------------- /tests/runtime.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const lodash = require('lodash') 3 | const sinon = require('sinon'); 4 | const Runtime = require('runtimes'); 5 | 6 | const nodejsConfig = require('./fixtures/nodejsConfig') 7 | const pythonConfig = require('./fixtures/pythonConfig') 8 | 9 | describe('Runtime', () => { 10 | describe('-> NodeJs', () => { 11 | let plugin; 12 | let runtimes; 13 | beforeEach(() => { 14 | plugin = { 15 | log: sinon.stub(), 16 | error: sinon.stub(), 17 | warn: sinon.stub() 18 | }; 19 | 20 | lodash.set(plugin, 'service.provider.runtime', 'nodejs12.x'); 21 | 22 | runtimes = new Runtime(plugin); 23 | }); 24 | 25 | it('should merge settings with new values', () => { 26 | expect(runtimes.getDefaultSettings({ 27 | packageManager: 'yarn', 28 | dependenciesPath: './fixtures/package.json' 29 | })).to.deep.equal(nodejsConfig); 30 | }) 31 | 32 | describe('-> hasDependenciesChanges', () => { 33 | beforeEach(() => { 34 | const remoteDeps = JSON.stringify({ 35 | dependencies: { 36 | express: '1.2.3' 37 | } 38 | }) 39 | 40 | lodash.set(plugin, 'bucketService.downloadDependencesFile', () => Promise.resolve(remoteDeps)); 41 | 42 | plugin.settings = runtimes.getDefaultSettings({ 43 | packageManager: 'yarn', 44 | dependenciesPath: './tests/fixtures/package.json' 45 | }) 46 | runtimes.init() 47 | runtimes._runtime.parent.run = () => 'v12.20.1'; 48 | }); 49 | 50 | it('checks if version is compatible', () => { 51 | 52 | return runtimes._runtime.isCompatibleVersion('v12.16').then((res) => { 53 | expect(res.isCompatible).to.equal(true); 54 | }) 55 | .then(() => runtimes._runtime.isCompatibleVersion('v12.18.3').then((res) => { 56 | expect(res.isCompatible).to.equal(true); 57 | })); 58 | }) 59 | 60 | it('compares two package json and returns if different', () => { 61 | return runtimes._runtime.hasDependenciesChanges().then((hasChanged) => { 62 | expect(hasChanged).to.equal(true); 63 | }); 64 | }) 65 | }); 66 | }); 67 | 68 | describe('-> Python', () => { 69 | let plugin; 70 | let runtimes; 71 | 72 | beforeEach(() => { 73 | plugin = { 74 | log: sinon.spy(), 75 | error: sinon.spy(), 76 | }; 77 | process.exit = sinon.spy(); 78 | }); 79 | 80 | it('should throw error when undefined runtime', () => { 81 | lodash.set(plugin, 'service.provider.runtime', undefined); 82 | runtimes = new Runtime(plugin); 83 | expect(plugin.error.calledWith('service.provider.runtime is required!')) 84 | .to.equal(true); 85 | expect(process.exit.calledOnce).to.equal(true); 86 | }) 87 | 88 | it('should throw error when invalid runtime', () => { 89 | lodash.set(plugin, 'service.provider.runtime', 'invalid'); 90 | runtimes = new Runtime(plugin); 91 | expect(plugin.log.calledWith('"invalid" runtime is not supported (yet).')) 92 | .to.equal(true); 93 | expect(process.exit.calledOnce).to.equal(true); 94 | }) 95 | }); 96 | }); 97 | --------------------------------------------------------------------------------