├── .all-contributorsrc ├── .czrc ├── .eslintignore ├── .github ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ ├── release.yml │ └── testing.yml ├── .gitignore ├── .mocharc.json ├── .npmignore ├── .releaserc.yml ├── .vscode ├── extensions.json └── settings.json ├── README.md ├── eslint.config.mjs ├── package-lock.json ├── package.json ├── src ├── customConfiguration.ts ├── index.ts └── utils │ ├── buildRollupConfig.ts │ ├── copyFiles.ts │ ├── getEntryForFunction.ts │ ├── installDependencies.ts │ ├── loadRollupConfig.ts │ ├── rollupFunctionEntry.ts │ └── zipDirectory.ts ├── test ├── fixtures │ ├── default-config │ │ ├── handler.js │ │ ├── rollup.config.js │ │ └── serverless.yml │ ├── multiple-files │ │ ├── hello.js │ │ ├── rollup.config.js │ │ ├── serverless.yml │ │ └── world.js │ ├── multiple-functions-per-file │ │ ├── handler.js │ │ ├── rollup.config.js │ │ └── serverless.yml │ ├── rollup-transpile-commonjs │ │ ├── handler.js │ │ ├── package.json │ │ ├── rollup.config.js │ │ └── serverless.yml │ ├── serverless-basic-esm │ │ ├── handler.js │ │ ├── rollup.config.js │ │ └── serverless.yml │ └── serverless-basic │ │ ├── handler.js │ │ ├── rollup.config.js │ │ └── serverless.yml ├── index.ts └── setup.js ├── tsconfig.dist.json └── tsconfig.json /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "serverless-rollup-plugin", 3 | "projectOwner": "theBenForce", 4 | "files": [ 5 | "README.md" 6 | ], 7 | "imageSize": 100, 8 | "contributors": [ 9 | { 10 | "login": "perrin4869", 11 | "name": "Julian Grinblat", 12 | "avatar_url": "https://avatars.githubusercontent.com/u/5774716?v=4", 13 | "profile": "https://github.com/perrin4869", 14 | "contributions": [ 15 | "code", 16 | "maintenance" 17 | ] 18 | }, 19 | { 20 | "login": "theBenForce", 21 | "name": "Ben Force", 22 | "avatar_url": "https://avatars.githubusercontent.com/u/1892467?v=4", 23 | "profile": "https://theBenForce.com", 24 | "contributions": [ 25 | "code", 26 | "doc" 27 | ] 28 | }, 29 | { 30 | "login": "vinicius73", 31 | "name": "Vinicius Reis", 32 | "avatar_url": "https://avatars.githubusercontent.com/u/1561347?v=4", 33 | "profile": "https://vinicius73.dev/", 34 | "contributions": [ 35 | "code" 36 | ] 37 | }, 38 | { 39 | "login": "eshikerya", 40 | "name": "Jacky Shikerya", 41 | "avatar_url": "https://avatars.githubusercontent.com/u/956884?v=4", 42 | "profile": "https://github.com/eshikerya", 43 | "contributions": [ 44 | "bug" 45 | ] 46 | } 47 | ], 48 | "repoType": "github", 49 | "contributorsPerLine": 7, 50 | "repoHost": "https://github.com", 51 | "commitConvention": "angular", 52 | "skipCi": true, 53 | "commitType": "docs" 54 | } 55 | -------------------------------------------------------------------------------- /.czrc: -------------------------------------------------------------------------------- 1 | { 2 | "path": "cz-conventional-changelog" 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | dist/ 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: / 5 | versioning-strategy: increase 6 | schedule: 7 | interval: monthly 8 | open-pull-requests-limit: 50 9 | groups: 10 | typescript-eslint: 11 | patterns: 12 | - "@typescript-eslint/*" 13 | - package-ecosystem: github-actions 14 | directory: / 15 | schedule: 16 | # Check for updates to GitHub Actions every weekday 17 | interval: daily 18 | ignore: 19 | - dependency-name: LocalStack/setup-localstack 20 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '37 6 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v4 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v3 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v3 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v3 68 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | release: 8 | name: Release 9 | runs-on: ubuntu-22.04 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | - name: Setup Node.js 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version: 20 17 | - name: Install dependencies 18 | run: npm ci --legacy-peer-deps 19 | - name: Release 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 23 | run: npx semantic-release 24 | -------------------------------------------------------------------------------- /.github/workflows/testing.yml: -------------------------------------------------------------------------------- 1 | name: Testing 2 | on: 3 | pull_request: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | node-version: 13 | - 18.x 14 | - 20.x 15 | - 22.x 16 | rollup-version: 17 | - ^2.0.0 18 | - ^3.0.0 19 | - ^4.0.0 20 | 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | 25 | - name: Setup Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | 30 | - name: Install dependencies 31 | run: npm ci --legacy-peer-deps 32 | 33 | - name: Run linter 34 | run: npm run lint 35 | 36 | - name: Install rollup 37 | run: npm install --legacy-peer-deps rollup@${{ matrix.rollup-version }} 38 | 39 | - name: Start LocalStack 40 | uses: LocalStack/setup-localstack@v0.2.2 41 | with: 42 | image-tag: '3.3' # Per https://docs.localstack.cloud/user-guide/integrations/serverless-framework/ 43 | install-awslocal: 'true' 44 | env: 45 | AWS_DEFAULT_REGION: us-east-1 46 | AWS_REGION: us-east-1 47 | AWS_ACCESS_KEY_ID: test 48 | AWS_SECRET_ACCESS_KEY: test 49 | 50 | - name: Run Tests 51 | run: npm test 52 | env: 53 | SERVERLESS_ACCESS_KEY: ${{ secrets.SERVERLESS_ACCESS_KEY }} 54 | SERVERLESS_LICENSE_KEY: ${{ secrets.SERVERLESS_LICENSE_KEY }} 55 | AWS_ACCESS_KEY_ID: test 56 | AWS_SECRET_ACCESS_KEY: test 57 | LAMBDA_NODE_VERSION: nodejs${{ matrix.node-version }} 58 | 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node,visualstudiocode 3 | # Edit at https://www.gitignore.io/?templates=node,visualstudiocode 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # TypeScript v1 declaration files 50 | typings/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Optional REPL history 62 | .node_repl_history 63 | 64 | # Output of 'npm pack' 65 | *.tgz 66 | 67 | # Yarn Integrity file 68 | .yarn-integrity 69 | 70 | # dotenv environment variables file 71 | .env 72 | .env.test 73 | 74 | # parcel-bundler cache (https://parceljs.org/) 75 | .cache 76 | 77 | # next.js build output 78 | .next 79 | 80 | # nuxt.js build output 81 | .nuxt 82 | 83 | # vuepress build output 84 | .vuepress/dist 85 | 86 | # Serverless directories 87 | .serverless/ 88 | 89 | # FuseBox cache 90 | .fusebox/ 91 | 92 | # DynamoDB Local files 93 | .dynamodb/ 94 | 95 | ### VisualStudioCode ### 96 | .vscode/* 97 | !.vscode/settings.json 98 | !.vscode/tasks.json 99 | !.vscode/launch.json 100 | !.vscode/extensions.json 101 | 102 | ### VisualStudioCode Patch ### 103 | # Ignore all local history of files 104 | .history 105 | 106 | # End of https://www.gitignore.io/api/node,visualstudiocode 107 | 108 | dist/ 109 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "reporter": "spec", 3 | "ui": "bdd", 4 | "extension": ["js", "ts"], 5 | "loader": "ts-node/esm", 6 | "timeout": 30000, 7 | "require": ["./test/setup.js"] 8 | } 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | .releaserc.yml 3 | rollup.config.js 4 | tsconfig.json 5 | .travis.yml -------------------------------------------------------------------------------- /.releaserc.yml: -------------------------------------------------------------------------------- 1 | branch: master 2 | plugins: 3 | - "@semantic-release/commit-analyzer" 4 | - "@semantic-release/release-notes-generator" 5 | - "@semantic-release/changelog" 6 | - "@semantic-release/npm" 7 | - "@semantic-release/github" 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "redhat.vscode-yaml" 5 | ] 6 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "yaml.schemas": { 3 | "https://json.schemastore.org/dependabot-2.0.json": "file:///workspaces/serverless-rollup-plugin/dependabot.yml" 4 | } 5 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to serverless-rollup-plugin 👋 2 | 3 | [![Release](https://github.com/theBenForce/serverless-rollup-plugin/actions/workflows/release.yml/badge.svg)](https://github.com/theBenForce/serverless-rollup-plugin/actions/workflows/release.yml) 4 | [![Testing](https://github.com/theBenForce/serverless-rollup-plugin/actions/workflows/testing.yml/badge.svg)](https://github.com/theBenForce/serverless-rollup-plugin/actions/workflows/testing.yml) 5 | [![Maintainability](https://api.codeclimate.com/v1/badges/79e200bf72d884691c7a/maintainability)](https://codeclimate.com/github/theBenForce/serverless-rollup-plugin/maintainability) 6 | [![npm version](https://badge.fury.io/js/serverless-rollup-plugin.svg)](https://badge.fury.io/js/serverless-rollup-plugin) 7 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FtheBenForce%2Fserverless-rollup-plugin.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2FtheBenForce%2Fserverless-rollup-plugin?ref=badge_shield) 8 | [![All Contributors](https://img.shields.io/github/all-contributors/theBenForce/serverless-rollup-plugin?color=ee8449&style=flat-square)](#contributors) 9 | 10 | > A plugin for the serverless framework to bundle lambda code using rollup 11 | 12 | ## Install 13 | 14 | ```sh 15 | npm install --save-dev serverless-rollup-plugin 16 | ``` 17 | 18 | Requires Node.js 18 and serverless 3.2. 19 | 20 | ## Usage 21 | 22 | Add the plugin to your serverless config: 23 | 24 | ```yaml 25 | plugins: 26 | - serverless-rollup-plugin 27 | - ...any other plugins 28 | ``` 29 | 30 | For each function that you would like to use rollup option, just define the handler option as normal. You can 31 | optionally define the `dependencies` property as a list of packages to be installed in the `node_modules` folder 32 | in your lambda. 33 | 34 | ```yaml 35 | testFunction: 36 | handler: src/functions/testFunction/index.handler 37 | dependencies: 38 | - aws-xray-sdk-core 39 | copyFiles: 40 | - some/glob/**/*.pattern 41 | ``` 42 | 43 | ### Config 44 | 45 | By default, `serverless-rollup-plugin` will attempt to load `rollup.config.js`. 46 | In order to override this behavior, you can add the following to configuration options: 47 | 48 | ```yaml 49 | custom: 50 | rollup: 51 | config: ./custom-rollup.config.js 52 | ``` 53 | 54 | ### Using Yarn 55 | 56 | By default if you specify function dependencies `npm` will be used. You can override this by setting the `installCommand` property, like this: 57 | 58 | ```yaml 59 | custom: 60 | rollup: 61 | installCommand: yarn add 62 | ``` 63 | 64 | ### Add Global Dependency 65 | 66 | If you want to add a dependency for every lambda function (for example [adding source map support](#adding-sourcemap-support)), you can add them to the rollup `dependencies` property: 67 | 68 | ```yaml 69 | custom: 70 | rollup: 71 | dependencies: 72 | - some-package-name 73 | ``` 74 | 75 | ### Output Options 76 | 77 | If you don't specify `output` settings in your rollup config, the following defaults will be used: 78 | 79 | ```json 80 | { 81 | "format": "cjs", 82 | "sourcemap": true 83 | } 84 | ``` 85 | 86 | If the `format` is `esm`, the resulting package will use the `mjs` extension to make use of [native lambda esm support](https://aws.amazon.com/blogs/compute/using-node-js-es-modules-and-top-level-await-in-aws-lambda/). 87 | 88 | ### Concurrency 89 | 90 | By default, `serverless-rollup-plugin` will output rollup bundles concurrently. 91 | In systems with low memory, such as small CI instances, it may be necessary to limit the number concurrent outputs so as not to run out of memory. 92 | You can define the number of concurrent outputs by using the `concurrency` option: 93 | 94 | ```yaml 95 | custom: 96 | rollup: 97 | concurrency: 3 98 | ``` 99 | 100 | Any value other than a number will be treated as `Number.POSITIVE_INFINITY`. 101 | 102 | ### Adding Sourcemap Support 103 | 104 | You can easily get your lambda stack traces to show correct file/line information using the `source-map-support` package. 105 | To use this with the `serverless-rollup-plugin`, first install the package and add it to the universal dependencies: 106 | 107 | ```yaml 108 | custom: 109 | rollup: 110 | dependencies: 111 | - source-map-support 112 | ``` 113 | 114 | Then in your rollup config, set the output banner to install it: 115 | 116 | ```typescript 117 | export default { 118 | output: { 119 | format: "cjs", 120 | sourcemap: true, 121 | banner: "require('source-map-support').install();" 122 | } 123 | }; 124 | ``` 125 | 126 | If you do specify `output` settings, they will be used and only the `file` property will be overwritten. 127 | 128 | ### Copying Resource Files 129 | 130 | To copy a static file into your function deployment, use the `copyFiles` parameter. This 131 | parameter is an array of glob pattern strings, or objects with the following properties: 132 | 133 | | Name | Required | Description | 134 | | ----------- | -------- | --------------------------------------------------------------- | 135 | | glob | Yes | A glob pattern | 136 | | srcBase | No | Part of the path that will be removed from the destination path | 137 | | destination | No | Destination path within the lambda's directory structure | 138 | 139 | ## 🧑‍💻 Contributors 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 |
Julian Grinblat
Julian Grinblat

💻 🚧
Ben Force
Ben Force

💻 📖
Vinicius Reis
Vinicius Reis

💻
Jacky Shikerya
Jacky Shikerya

🐛
154 | 155 | 156 | 157 | 158 | 159 | 160 | ## 🤝 Contributing 161 | 162 | Contributions, issues and feature requests are welcome! 163 | 164 | Feel free to check [issues page](https://github.com/drg-adaptive/serverless-rollup-plugin/issues). 165 | 166 | ## Show your support 167 | 168 | Give a ⭐️ if this project helped you! 169 | 170 | --- 171 | 172 | _This README was generated with ❤️ by [readme-md-generator](https://github.com/kefranabg/readme-md-generator)_ 173 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import unicorn from 'eslint-plugin-unicorn'; 2 | import n from 'eslint-plugin-n'; 3 | import globals from 'globals'; 4 | import mocha from 'eslint-plugin-mocha'; 5 | import typescriptEslint from '@typescript-eslint/eslint-plugin'; 6 | import tsParser from '@typescript-eslint/parser'; 7 | import path from 'node:path'; 8 | import { fileURLToPath } from 'node:url'; 9 | import js from '@eslint/js'; 10 | import { FlatCompat } from '@eslint/eslintrc'; 11 | 12 | const __filename = fileURLToPath(import.meta.url); 13 | const __dirname = path.dirname(__filename); 14 | const compat = new FlatCompat({ 15 | baseDirectory: __dirname, 16 | recommendedConfig: js.configs.recommended, 17 | allConfig: js.configs.all, 18 | }); 19 | 20 | export default [{ 21 | ignores: ['**/node_modules/', '**/coverage/', '**/dist/'], 22 | }, ...compat.extends( 23 | 'airbnb-base', 24 | 'plugin:unicorn/recommended', 25 | 'plugin:n/recommended', 26 | 'plugin:@eslint-community/eslint-comments/recommended', 27 | ), { 28 | plugins: { 29 | unicorn, 30 | n, 31 | }, 32 | 33 | languageOptions: { 34 | globals: { 35 | ...globals.node, 36 | }, 37 | 38 | ecmaVersion: 'latest', 39 | sourceType: 'module', 40 | }, 41 | 42 | settings: { 43 | 'import/parsers': { 44 | '@typescript-eslint/parser': ['.ts'], 45 | }, 46 | 47 | 'import/resolver': { 48 | typescript: { 49 | alwaysTryTypes: true, 50 | project: ['./tsconfig.json', './test/fixtures/*/tsconfig.json'], 51 | }, 52 | }, 53 | }, 54 | 55 | rules: { 56 | 'unicorn/prevent-abbreviations': 0, 57 | 'unicorn/no-array-for-each': 0, 58 | 'unicorn/no-array-reduce': 0, 59 | 'unicorn/no-null': 0, 60 | 'unicorn/import-style': 0, 61 | 'unicorn/no-anonymous-default-export': 0, 62 | 63 | 'unicorn/filename-case': ['error', { 64 | case: 'camelCase', 65 | }], 66 | 67 | '@eslint-community/eslint-comments/no-unused-disable': 'error', 68 | 'import/extensions': 0, 69 | 70 | 'n/no-missing-import': ['error', { 71 | ignoreTypeImport: true, 72 | }], 73 | }, 74 | }, ...compat.extends('plugin:mocha/recommended').map((config) => ({ 75 | ...config, 76 | files: ['test/**/*.+(t|j)s'], 77 | })), { 78 | files: ['test/**/*.+(t|j)s'], 79 | 80 | plugins: { 81 | mocha, 82 | }, 83 | 84 | languageOptions: { 85 | globals: { 86 | ...globals.mocha, 87 | }, 88 | }, 89 | 90 | rules: { 91 | 'mocha/no-mocha-arrows': 0, 92 | }, 93 | }, { 94 | files: ['**/*.ts'], 95 | 96 | plugins: { 97 | '@typescript-eslint': typescriptEslint, 98 | }, 99 | 100 | languageOptions: { 101 | parser: tsParser, 102 | }, 103 | }, 104 | { 105 | files: ['eslint.config.mjs'], 106 | rules: { 107 | 'import/no-extraneous-dependencies': 0, 108 | 'no-underscore-dangle': 0, 109 | }, 110 | }, 111 | ]; 112 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-rollup-plugin", 3 | "version": "1.1.0", 4 | "description": "A plugin for the serverless framework to build lambda code using rollup", 5 | "type": "module", 6 | "main": "dist/index.js", 7 | "exports": { 8 | ".": "./dist/index.js", 9 | "./package.json": "./package.json" 10 | }, 11 | "engines": { 12 | "node": ">=18" 13 | }, 14 | "files": [ 15 | "dist" 16 | ], 17 | "author": { 18 | "name": "Ben Force", 19 | "email": "bforce@teamdrg.com" 20 | }, 21 | "homepage": "https://github.com/theBenForce/serverless-rollup-plugin", 22 | "repository": "github:theBenForce/serverless-rollup-plugin", 23 | "bugs": "https://github.com/theBenForce/serverless-rollup-plugin/issues", 24 | "license": "MIT", 25 | "devDependencies": { 26 | "@eslint-community/eslint-plugin-eslint-comments": "^4.4.1", 27 | "@eslint/eslintrc": "^3.2.0", 28 | "@eslint/js": "^9.19.0", 29 | "@semantic-release/changelog": "^6.0.3", 30 | "@semantic-release/commit-analyzer": "^13.0.0", 31 | "@semantic-release/github": "^11.0.1", 32 | "@semantic-release/npm": "^12.0.1", 33 | "@semantic-release/release-notes-generator": "^14.0.3", 34 | "@serverless/test": "^11.1.1", 35 | "@types/archiver": "^6.0.3", 36 | "@types/chai": "^4.3.11", 37 | "@types/chai-as-promised": "^8.0.1", 38 | "@types/dirty-chai": "^2.0.5", 39 | "@types/mocha": "^10.0.10", 40 | "@types/node": "^22.10.5", 41 | "@types/serverless": "^3.12.22", 42 | "@types/tmp": "^0.2.6", 43 | "@typescript-eslint/eslint-plugin": "^8.22.0", 44 | "@typescript-eslint/parser": "^8.22.0", 45 | "chai": "^4.5.0", 46 | "chai-as-promised": "^8.0.1", 47 | "commitizen": "^4.3.1", 48 | "cz-conventional-changelog": "^3.3.0", 49 | "dirty-chai": "^2.0.1", 50 | "eslint": "^9.17.0", 51 | "eslint-config-airbnb-base": "^15.0.0", 52 | "eslint-import-resolver-typescript": "^3.7.0", 53 | "eslint-plugin-import": "^2.31.0", 54 | "eslint-plugin-mocha": "^10.5.0", 55 | "eslint-plugin-n": "^17.15.1", 56 | "eslint-plugin-unicorn": "^56.0.1", 57 | "globals": "^15.14.0", 58 | "log": "^6.3.2", 59 | "mocha": "^11.0.1", 60 | "node-stream-zip": "^1.15.0", 61 | "rollup": "^4.34.0", 62 | "semantic-release": "^24.2.0", 63 | "serverless": "^4.5.3", 64 | "serverless-localstack": "^1.3.1", 65 | "ts-node": "^10.9.2", 66 | "tslib": "^2.8.1", 67 | "typescript": "^5.7.3" 68 | }, 69 | "peerDependencies": { 70 | "rollup": "^2.26.0 || ^3.0.0 || ^4.0.0", 71 | "serverless": "^1.42.2 || ^2.0.0 || ^3.0.0 || ^4.0.0" 72 | }, 73 | "scripts": { 74 | "lint": "eslint --cache .", 75 | "build": "tsc -p tsconfig.dist.json", 76 | "prepare": "npm run build", 77 | "test": "mocha" 78 | }, 79 | "dependencies": { 80 | "archiver": "^7.0.1", 81 | "execa": "^9.5.2", 82 | "fast-glob": "^3.3.1", 83 | "globby": "^14.0.2", 84 | "module-from-string": "^3.3.1", 85 | "p-map": "^7.0.3", 86 | "tmp": "^0.2.3" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/customConfiguration.ts: -------------------------------------------------------------------------------- 1 | import { RollupOptions } from 'rollup'; 2 | 3 | export interface CustomConfiguration { 4 | /** 5 | * Rollup configuration, or a string pointing to the configuration 6 | */ 7 | config?: string | RollupOptions; 8 | 9 | /** 10 | * Glob patterns to match files that should be excluded when bundling build results 11 | */ 12 | excludeFiles?: Array; 13 | 14 | /** 15 | * The command used to install function dependencies, ex: yarn add 16 | */ 17 | installCommand?: string; 18 | 19 | /** 20 | * Optional list of dependencies to install to every lambda 21 | */ 22 | dependencies?: string[]; 23 | 24 | /** 25 | * Number of concurrent operations to perform, such as outputting rollup bundles 26 | */ 27 | concurrency?: number; 28 | } 29 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import map from 'p-map'; 3 | import { RollupOptions, OutputChunk, OutputAsset } from 'rollup'; 4 | import type Serverless from 'serverless'; 5 | import type { FunctionDefinitionHandler } from 'serverless'; 6 | import type Plugin from 'serverless/classes/Plugin.js'; 7 | import type { Logging } from 'serverless/classes/Plugin.js'; 8 | import loadRollupConfig from './utils/loadRollupConfig.js'; 9 | import zipDirectory from './utils/zipDirectory.js'; 10 | import getEntryForFunction, { FunctionEntry } from './utils/getEntryForFunction.js'; 11 | import { CustomConfiguration } from './customConfiguration.js'; 12 | import { buildBundle, outputBundle } from './utils/rollupFunctionEntry.js'; 13 | import installDependencies from './utils/installDependencies.js'; 14 | import copyFiles from './utils/copyFiles.js'; 15 | 16 | export default class ServerlessRollupPlugin implements Plugin { 17 | readonly hooks: { [key: string]: any } = { 18 | 'before:package:createDeploymentArtifacts': () => this.prepare().then(this.rollupFunction.bind(this)), 19 | 'after:package:createDeploymentArtifacts': () => this.cleanup(), 20 | 'before:deploy:function:packageFunction': () => this.prepare().then(this.rollupFunction.bind(this)), 21 | }; 22 | 23 | readonly name: string = 'serverless-rollup'; 24 | 25 | configuration: CustomConfiguration; 26 | 27 | rollupConfig: RollupOptions; 28 | 29 | entries: Map; 30 | 31 | constructor( 32 | private serverless: Serverless, // eslint-disable-line no-unused-vars 33 | private options: Serverless.Options, // eslint-disable-line no-unused-vars 34 | private logging: Logging, // eslint-disable-line no-unused-vars 35 | ) { 36 | this.configuration = this.serverless.service.custom?.rollup ?? {} as CustomConfiguration; 37 | } 38 | 39 | async prepare() { 40 | const functions = this.options.function 41 | ? [this.options.function] 42 | : this.serverless.service.getAllFunctions(); 43 | 44 | const runtime = this.serverless.service.provider?.runtime; 45 | 46 | this.entries = functions 47 | .map((functionName: string) => this.serverless.service.getFunction(functionName)) 48 | .filter((functionDefinition: FunctionDefinitionHandler) => (functionDefinition.runtime ?? runtime)?.toLowerCase().startsWith('node')) 49 | .map((functionDefinition: FunctionDefinitionHandler) => getEntryForFunction( 50 | this.serverless, 51 | this.configuration.excludeFiles, 52 | functionDefinition, 53 | this.logging, 54 | )) 55 | .reduce((entries: Map, entry: FunctionEntry) => { 56 | entries.set(entry.source, [...(entries.get(entry.source) ?? []), entry]); 57 | 58 | return entries; 59 | }, new Map()); 60 | 61 | this.rollupConfig = await loadRollupConfig( 62 | this.serverless, 63 | this.configuration.config ?? 'rollup.config.js', 64 | this.logging, 65 | ); 66 | } 67 | 68 | async rollupFunction() { 69 | const installCommand = this.configuration.installCommand ?? 'npm install'; 70 | const concurrency = typeof this.configuration.concurrency === 'number' 71 | ? this.configuration.concurrency 72 | : Number.POSITIVE_INFINITY; 73 | 74 | // eslint-disable-next-line no-restricted-syntax 75 | for (const [input, functionEntries] of this.entries.entries()) { 76 | this.logging.log.info(`Bundling ${input}`); 77 | // eslint-disable-next-line no-await-in-loop 78 | const bundle = await buildBundle(input, this.rollupConfig); 79 | 80 | // eslint-disable-next-line no-await-in-loop 81 | await map(functionEntries, async (functionEntry) => { 82 | this.logging.log.info(`.: Function ${functionEntry.function.name} :.`); 83 | 84 | this.logging.log.info(`${functionEntry.function.name}: Creating config for ${functionEntry.source}`); 85 | try { 86 | this.logging.log.info(`${functionEntry.function.name}: Outputting bundle to ${functionEntry.destination}`); 87 | 88 | const rollupOutput = await outputBundle( 89 | bundle, 90 | functionEntry, 91 | this.rollupConfig, 92 | ); 93 | 94 | const excludedLibraries = rollupOutput.output.reduce( 95 | (current: Array, output: OutputChunk | OutputAsset) => { 96 | if (output.type === 'chunk' && output.imports) { 97 | current.push(...output.imports); 98 | } 99 | 100 | return current; 101 | }, 102 | [], 103 | ); 104 | 105 | this.logging.log.info(`${functionEntry.function.name}: Excluded the following imports: ${excludedLibraries.join(', ')}`); 106 | 107 | await installDependencies( 108 | functionEntry, 109 | this.configuration.dependencies ?? [], 110 | installCommand, 111 | this.logging, 112 | ); 113 | 114 | if (functionEntry.function.copyFiles) { 115 | await copyFiles(functionEntry, this.logging); 116 | } 117 | 118 | this.logging.log.info(`${functionEntry.function.name}: Creating zip file for ${functionEntry.function.name}`); 119 | const artifactPath = await zipDirectory( 120 | this.serverless, 121 | functionEntry.destination, 122 | functionEntry.function.name, 123 | this.logging, 124 | ); 125 | 126 | this.logging.log.info(`${functionEntry.function.name}: Path to artifact: ${artifactPath}`); 127 | 128 | functionEntry.function.package = { // eslint-disable-line no-param-reassign 129 | artifact: path.relative( 130 | this.serverless.config.servicePath, 131 | artifactPath, 132 | ), 133 | }; 134 | } catch (error) { 135 | this.logging.log.info(`${functionEntry.function.name}: Error while packaging ${functionEntry.source}: ${error.message}`); 136 | 137 | throw error; 138 | } 139 | }, { concurrency }); 140 | } 141 | } 142 | 143 | async cleanup() {} // eslint-disable-line class-methods-use-this,no-empty-function 144 | } 145 | -------------------------------------------------------------------------------- /src/utils/buildRollupConfig.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { RollupOptions, RollupCache } from 'rollup'; 3 | import { FunctionEntry } from './getEntryForFunction.js'; 4 | 5 | export const buildInputConfig = ( 6 | input: string, 7 | rollupConfig: RollupOptions, 8 | cache: RollupCache, 9 | ): RollupOptions => ({ 10 | ...rollupConfig, 11 | input, 12 | cache, 13 | }); 14 | 15 | export const buildOutputConfig = ( 16 | functionEntry: FunctionEntry, 17 | rollupConfig: RollupOptions, 18 | cache: RollupCache, 19 | ): RollupOptions => { 20 | const output: any = rollupConfig?.output ?? { 21 | format: 'cjs', 22 | sourcemap: true, 23 | }; 24 | 25 | return { 26 | ...rollupConfig, 27 | output: { 28 | ...output, 29 | file: path.join(functionEntry.destination, `index.${output.format === 'esm' ? 'mjs' : 'js'}`), 30 | }, 31 | cache, 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /src/utils/copyFiles.ts: -------------------------------------------------------------------------------- 1 | import { join, dirname } from 'node:path'; 2 | import { mkdir, copyFile } from 'node:fs/promises'; 3 | import { globby } from 'globby'; 4 | import { Logging } from 'serverless/classes/Plugin.js'; // eslint-disable-line n/no-missing-import 5 | import { FunctionEntry } from './getEntryForFunction.js'; // eslint-disable-line import/no-cycle 6 | 7 | interface CopyFilesAdvanced { 8 | glob: string; 9 | srcBase?: string; 10 | destination?: string; 11 | } 12 | 13 | export type CopyFilesEntry = string | CopyFilesAdvanced; 14 | 15 | function getCopyFiles(functionEntry: FunctionEntry): Array { 16 | return functionEntry.function.copyFiles?.map((entry) => { 17 | if (typeof entry === 'string') { 18 | return { glob: entry }; 19 | } 20 | return entry; 21 | }); 22 | } 23 | 24 | export default async (functionEntry: FunctionEntry, { log }: Logging) => { 25 | const copyFiles = getCopyFiles(functionEntry); 26 | 27 | await Promise.all(copyFiles.map(async (entry) => { 28 | const files = await globby([entry.glob]); 29 | 30 | log.info(`Copying: ${JSON.stringify(files)}`); 31 | 32 | return Promise.all( 33 | files.map(async (filename) => { 34 | let destination = filename; 35 | if (entry.srcBase) { 36 | destination = filename.replace(entry.srcBase, ''); 37 | } 38 | if (entry.destination) { 39 | destination = join(entry.destination, destination); 40 | } 41 | destination = join(functionEntry.destination, destination); 42 | const destDir = dirname(destination); 43 | 44 | await mkdir(destDir, { recursive: true }); 45 | 46 | log.info(`Copying ${filename} to ${destination}...`); 47 | await copyFile(filename, destination); 48 | }), 49 | ); 50 | })); 51 | }; 52 | -------------------------------------------------------------------------------- /src/utils/getEntryForFunction.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import tmp from 'tmp'; 3 | import glob from 'fast-glob'; 4 | import type Serverless from 'serverless'; 5 | import type { FunctionDefinitionHandler } from 'serverless'; 6 | import { Logging } from 'serverless/classes/Plugin.js'; // eslint-disable-line n/no-missing-import 7 | import { CopyFilesEntry } from './copyFiles.js'; // eslint-disable-line import/no-cycle 8 | 9 | export interface FunctionEntry { 10 | source: string; 11 | destination: string; 12 | handler: string; 13 | handlerFile: string; 14 | function: FunctionDefinitionHandler & { 15 | dependencies: string[]; 16 | copyFiles?: CopyFilesEntry[]; 17 | }; 18 | } 19 | 20 | export const getHandlerEntry = (handler: string) => /.*\.(.*)?$/.exec(handler)?.[1]; 21 | 22 | export const getHandlerFile = (handler: string) => /(.*)\..*?$/.exec(handler)?.[1]; 23 | 24 | function getEntryExtension( 25 | serverless: Serverless, 26 | ignore: Array, 27 | fileName: string, 28 | name: string, 29 | { log }: Logging, 30 | ) { 31 | const preferredExtensions = ['.js', '.ts', '.jsx', '.tsx']; 32 | 33 | const files: Array = glob.sync(`${fileName}.*`, { 34 | cwd: serverless.config.servicePath, 35 | onlyFiles: true, 36 | ignore, 37 | }); 38 | 39 | if (!files?.length) { 40 | // If we cannot find any handler we should terminate with an error 41 | throw new Error( 42 | `No matching handler found for '${fileName}' in '${serverless.config.servicePath}'. Check your service definition (function ${name}).`, 43 | ); 44 | } 45 | 46 | const sortedFiles = files 47 | .filter((file) => preferredExtensions.find((x) => x === path.extname(file))) 48 | .sort((a, b) => a.length - b.length) 49 | .concat(files) // eslint-disable-line unicorn/prefer-spread 50 | .reduce((current: Array, next: string) => { 51 | const nextLower = next.toLowerCase(); 52 | if (!current.some((x) => x.toLowerCase() === nextLower)) { 53 | current.push(next); 54 | } 55 | 56 | return current; 57 | }, []); 58 | 59 | if (sortedFiles.length > 1) { 60 | log.info( 61 | `WARNING: More than one matching handlers found for '${fileName}'. Using '${sortedFiles[0]}'. Function ${name}`, 62 | ); 63 | } 64 | return path.extname(sortedFiles[0]); 65 | } 66 | 67 | export default ( 68 | serverless: Serverless, 69 | ignore: Array, 70 | serverlessFunction: FunctionDefinitionHandler & { 71 | dependencies?: string[]; 72 | }, 73 | logging: Logging, 74 | ): FunctionEntry => { 75 | const baseDir = tmp.dirSync({ prefix: serverlessFunction.name }); 76 | 77 | const handlerFile = getHandlerFile(serverlessFunction.handler); 78 | const handlerEntry = getHandlerEntry(serverlessFunction.handler); 79 | 80 | if (!handlerFile) { 81 | throw new Error( 82 | `\nWARNING: Entry for ${serverlessFunction.name}@${serverlessFunction.handler} could not be retrieved.\nPlease check your service config if you want to use lib.entries.`, 83 | ); 84 | } 85 | const ext = getEntryExtension( 86 | serverless, 87 | ignore, 88 | handlerFile, 89 | serverlessFunction.name, 90 | logging, 91 | ); 92 | serverlessFunction.handler = `index.${handlerEntry}`; // eslint-disable-line no-param-reassign 93 | 94 | return { 95 | source: `./${handlerFile}${ext}`, 96 | destination: baseDir.name, 97 | handler: serverlessFunction.handler, 98 | handlerFile, 99 | function: Object.assign(serverlessFunction, { 100 | dependencies: serverlessFunction.dependencies ?? [], 101 | }), 102 | }; 103 | }; 104 | -------------------------------------------------------------------------------- /src/utils/installDependencies.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'node:path'; 2 | import { cwd } from 'node:process'; 3 | import { createRequire } from 'node:module'; 4 | import { execa } from 'execa'; 5 | import { Logging } from 'serverless/classes/Plugin.js'; // eslint-disable-line n/no-missing-import 6 | import { FunctionEntry } from './getEntryForFunction.js'; 7 | 8 | export default async ( 9 | functionEntry: FunctionEntry, 10 | globalDependencies: Array, 11 | installCommand: string, 12 | { log }: Logging, 13 | ) => { 14 | const functionDependencies = [...functionEntry.function.dependencies, ...globalDependencies] 15 | .reduce((current: Array, next: string) => { 16 | if (!current.includes(next)) { 17 | current.push(next); 18 | } 19 | 20 | return current; 21 | }, []); 22 | 23 | if (!functionDependencies?.length) return; 24 | 25 | log.info(`Installing ${functionDependencies.length} dependencies`); 26 | 27 | const require = createRequire(import.meta.url); 28 | const pkg = require(join(cwd(), 'package.json')); // eslint-disable-line import/no-dynamic-require 29 | const dependencies = { ...pkg.dependencies, ...pkg.devDependencies }; 30 | const missingDeps = functionDependencies.filter( 31 | (dep: string) => !dependencies[dep], 32 | ); 33 | 34 | if (missingDeps.length > 0) { 35 | throw new Error( 36 | `Please install the following dependencies in your project: ${missingDeps.join( 37 | ' ', 38 | )}`, 39 | ); 40 | } 41 | 42 | const finalDependencies = functionDependencies.map( 43 | (dep: string) => `${dep}@${dependencies[dep]}`, 44 | ); 45 | 46 | const finalInstallCommand = [installCommand, ...finalDependencies].join(' '); 47 | log.info(`Executing ${finalInstallCommand} in ${functionEntry.destination}`); 48 | 49 | await execa(finalInstallCommand, { 50 | cwd: functionEntry.destination, 51 | shell: true, 52 | }); 53 | }; 54 | -------------------------------------------------------------------------------- /src/utils/loadRollupConfig.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import type Serverless from 'serverless'; 3 | import { rollup, RollupOptions } from 'rollup'; 4 | import { Logging } from 'serverless/classes/Plugin.js'; // eslint-disable-line n/no-missing-import 5 | 6 | const loadScript = async (filename: string): Promise => { 7 | const bundle = await rollup({ 8 | external: () => true, 9 | input: filename, 10 | treeshake: false, 11 | }); 12 | 13 | const { 14 | output: [{ code }], 15 | } = await bundle.generate({ 16 | exports: 'default', 17 | format: 'cjs', 18 | }); 19 | 20 | // import only if absolutely necessary 21 | const { requireFromString } = await import('module-from-string'); 22 | return requireFromString(code); 23 | }; 24 | 25 | export default async ( 26 | serverless: Serverless, 27 | config: string | RollupOptions, 28 | { log }: Logging, 29 | ): Promise => { 30 | let rollupConfig: RollupOptions; 31 | 32 | if (typeof config === 'string') { 33 | const rollupConfigFilePath = path.join( 34 | serverless.config.servicePath, 35 | config, 36 | ); 37 | if (!serverless.utils.fileExistsSync(rollupConfigFilePath)) { 38 | throw new Error( 39 | `The rollup plugin could not find the configuration file at: ${ 40 | rollupConfigFilePath}`, 41 | ); 42 | } 43 | try { 44 | rollupConfig = await import(rollupConfigFilePath) 45 | .then( 46 | ({ default: rollupConfigExport }) => rollupConfigExport, 47 | (error) => { 48 | if (error instanceof SyntaxError) { 49 | log.warning(`Failed to import ${rollupConfigFilePath}. Will load using commonjs transpilation.`); 50 | log.warning("Please switch to using 'mjs' extension, or 'type': 'module' in 'package.json', since this feature will be removed in a future release."); 51 | 52 | return loadScript(rollupConfigFilePath); 53 | } 54 | throw error; 55 | }, 56 | ); 57 | 58 | if (rollupConfig.input) { 59 | delete rollupConfig.input; 60 | } 61 | 62 | log.info(`Loaded rollup config from ${rollupConfigFilePath}`); 63 | } catch (error) { 64 | log.info( 65 | `Could not load rollup config '${rollupConfigFilePath}'`, 66 | ); 67 | throw error; 68 | } 69 | } else { 70 | rollupConfig = config; 71 | } 72 | 73 | return rollupConfig; 74 | }; 75 | -------------------------------------------------------------------------------- /src/utils/rollupFunctionEntry.ts: -------------------------------------------------------------------------------- 1 | import { 2 | rollup, 3 | RollupOptions, 4 | RollupBuild, 5 | OutputOptions, 6 | RollupOutput, 7 | RollupCache, 8 | } from 'rollup'; 9 | import { buildInputConfig, buildOutputConfig } from './buildRollupConfig.js'; 10 | import { FunctionEntry } from './getEntryForFunction.js'; 11 | 12 | let cache: RollupCache; 13 | export const buildBundle = async ( 14 | input: string, 15 | rollupConfig: RollupOptions, 16 | ) => { 17 | const config: RollupOptions = buildInputConfig( 18 | input, 19 | rollupConfig, 20 | cache, 21 | ); 22 | 23 | const bundle: RollupBuild = await rollup(config); 24 | cache = bundle.cache; 25 | 26 | return bundle; 27 | }; 28 | 29 | export const outputBundle = async ( 30 | bundle: RollupBuild, 31 | functionEntry: FunctionEntry, 32 | rollupConfig: RollupOptions, 33 | ) => { 34 | const config: RollupOptions = buildOutputConfig( 35 | functionEntry, 36 | rollupConfig, 37 | cache, 38 | ); 39 | 40 | const rollupOutput: RollupOutput = await bundle.write( 41 | config.output as OutputOptions, 42 | ); 43 | 44 | if (!rollupOutput.output?.length) { 45 | throw new Error(`No build output for ${functionEntry.function.name}`); 46 | } 47 | 48 | return rollupOutput; 49 | }; 50 | -------------------------------------------------------------------------------- /src/utils/zipDirectory.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | import fs from 'node:fs'; 3 | import archiver from 'archiver'; 4 | import glob from 'fast-glob'; 5 | import type Serverless from 'serverless'; 6 | import { Logging } from 'serverless/classes/Plugin.js'; // eslint-disable-line n/no-missing-import 7 | 8 | export default async ( 9 | serverless: Serverless, 10 | source: string, 11 | name: string, 12 | { log }: Logging, 13 | ): Promise => { 14 | const zip = archiver('zip'); 15 | 16 | const artifactPath = path.join( 17 | serverless.config.servicePath, 18 | '.serverless', 19 | `${name}.zip`, 20 | ); 21 | log.info(`Compressing to ${artifactPath}`); 22 | serverless.utils.writeFileDir(artifactPath); 23 | 24 | const output = fs.createWriteStream(artifactPath); 25 | 26 | const files = await glob('**', { 27 | cwd: source, 28 | dot: true, 29 | suppressErrors: true, 30 | followSymbolicLinks: true, 31 | }); 32 | 33 | if (files.length === 0) { 34 | throw new Error(`Packing ${name}: No files found`); 35 | } 36 | 37 | zip.pipe(output); 38 | 39 | files.forEach((filePath: string) => { 40 | const fullPath = path.resolve(source, filePath); 41 | const stats = fs.statSync(fullPath); 42 | 43 | if (!stats.isDirectory()) { 44 | zip.append(fs.readFileSync(fullPath), { 45 | name: filePath, 46 | mode: stats.mode, 47 | date: new Date(0), // Trick to get the same hash when zipping 48 | }); 49 | } 50 | }); 51 | 52 | zip.finalize(); 53 | 54 | return new Promise((resolve, reject) => { 55 | zip.on('error', reject); 56 | output.on('close', () => resolve(artifactPath)); 57 | }); 58 | }; 59 | -------------------------------------------------------------------------------- /test/fixtures/default-config/handler.js: -------------------------------------------------------------------------------- 1 | export const hello = async () => ({ // eslint-disable-line import/prefer-default-export 2 | statusCode: 200, 3 | body: 'default-config', 4 | }); 5 | -------------------------------------------------------------------------------- /test/fixtures/default-config/rollup.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | output: { 3 | format: 'cjs', 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /test/fixtures/default-config/serverless.yml: -------------------------------------------------------------------------------- 1 | service: default-config 2 | 3 | frameworkVersion: '4' 4 | 5 | licenseKey: ${env:SERVERLESS_LICENSE_KEY} 6 | 7 | plugins: 8 | - serverless-rollup-plugin # self-import 9 | - serverless-localstack 10 | 11 | provider: 12 | name: aws 13 | runtime: ${env:LAMBDA_NODE_VERSION} 14 | deploymentBucket: deployment-bucket 15 | 16 | functions: 17 | hello: 18 | handler: handler.hello 19 | -------------------------------------------------------------------------------- /test/fixtures/multiple-files/hello.js: -------------------------------------------------------------------------------- 1 | export const handle = async () => ({ // eslint-disable-line import/prefer-default-export 2 | statusCode: 200, 3 | body: JSON.stringify( 4 | { 5 | message: 'Hello', 6 | }, 7 | null, 8 | 2, 9 | ), 10 | }); 11 | -------------------------------------------------------------------------------- /test/fixtures/multiple-files/rollup.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | output: { 3 | format: 'cjs', 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /test/fixtures/multiple-files/serverless.yml: -------------------------------------------------------------------------------- 1 | service: multiple-files 2 | 3 | frameworkVersion: '4' 4 | 5 | licenseKey: ${env:SERVERLESS_LICENSE_KEY} 6 | 7 | plugins: 8 | - serverless-rollup-plugin # self-import 9 | - serverless-localstack 10 | 11 | custom: 12 | rollup: 13 | config: ./rollup.config.js 14 | 15 | provider: 16 | name: aws 17 | runtime: ${env:LAMBDA_NODE_VERSION} 18 | deploymentBucket: deployment-bucket 19 | 20 | functions: 21 | hello: 22 | handler: hello.handle 23 | world: 24 | handler: world.handle 25 | -------------------------------------------------------------------------------- /test/fixtures/multiple-files/world.js: -------------------------------------------------------------------------------- 1 | export const handle = async () => ({ // eslint-disable-line import/prefer-default-export 2 | statusCode: 200, 3 | body: JSON.stringify( 4 | { 5 | message: 'World', 6 | }, 7 | null, 8 | 2, 9 | ), 10 | }); 11 | -------------------------------------------------------------------------------- /test/fixtures/multiple-functions-per-file/handler.js: -------------------------------------------------------------------------------- 1 | export const hello = async () => ({ 2 | statusCode: 200, 3 | body: JSON.stringify( 4 | { 5 | message: 'Hello', 6 | }, 7 | null, 8 | 2, 9 | ), 10 | }); 11 | 12 | export const world = async () => ({ 13 | statusCode: 200, 14 | body: JSON.stringify( 15 | { 16 | message: 'World', 17 | }, 18 | null, 19 | 2, 20 | ), 21 | }); 22 | -------------------------------------------------------------------------------- /test/fixtures/multiple-functions-per-file/rollup.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | output: { 3 | format: 'cjs', 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /test/fixtures/multiple-functions-per-file/serverless.yml: -------------------------------------------------------------------------------- 1 | service: multiple-functions-per-file 2 | 3 | frameworkVersion: '4' 4 | 5 | licenseKey: ${env:SERVERLESS_LICENSE_KEY} 6 | 7 | plugins: 8 | - serverless-rollup-plugin # self-import 9 | - serverless-localstack 10 | 11 | custom: 12 | rollup: 13 | config: ./rollup.config.js 14 | 15 | provider: 16 | name: aws 17 | runtime: ${env:LAMBDA_NODE_VERSION} 18 | deploymentBucket: deployment-bucket 19 | 20 | functions: 21 | hello: 22 | handler: handler.hello 23 | world: 24 | handler: handler.world 25 | -------------------------------------------------------------------------------- /test/fixtures/rollup-transpile-commonjs/handler.js: -------------------------------------------------------------------------------- 1 | export const hello = async (event) => ({ // eslint-disable-line import/prefer-default-export 2 | statusCode: 200, 3 | body: JSON.stringify( 4 | { 5 | message: 'Go Serverless v2.0! Your function executed successfully!', 6 | input: event, 7 | }, 8 | null, 9 | 2, 10 | ), 11 | }); 12 | -------------------------------------------------------------------------------- /test/fixtures/rollup-transpile-commonjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/rollup-transpile-commonjs/rollup.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | output: { 3 | format: 'esm', 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /test/fixtures/rollup-transpile-commonjs/serverless.yml: -------------------------------------------------------------------------------- 1 | service: serverless-basic 2 | 3 | frameworkVersion: '4' 4 | 5 | licenseKey: ${env:SERVERLESS_LICENSE_KEY} 6 | 7 | plugins: 8 | - ../../../dist 9 | - serverless-localstack 10 | 11 | custom: 12 | rollup: 13 | config: ./rollup.config.js 14 | 15 | provider: 16 | name: aws 17 | runtime: ${env:LAMBDA_NODE_VERSION} 18 | deploymentBucket: deployment-bucket 19 | 20 | functions: 21 | hello: 22 | handler: handler.hello 23 | -------------------------------------------------------------------------------- /test/fixtures/serverless-basic-esm/handler.js: -------------------------------------------------------------------------------- 1 | export const hello = async (event) => ({ // eslint-disable-line import/prefer-default-export 2 | statusCode: 200, 3 | body: JSON.stringify( 4 | { 5 | message: 'Go Serverless v2.0! Your function executed successfully!', 6 | input: event, 7 | }, 8 | null, 9 | 2, 10 | ), 11 | }); 12 | -------------------------------------------------------------------------------- /test/fixtures/serverless-basic-esm/rollup.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | output: { 3 | format: 'esm', 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /test/fixtures/serverless-basic-esm/serverless.yml: -------------------------------------------------------------------------------- 1 | service: serverless-basic 2 | 3 | frameworkVersion: '4' 4 | 5 | licenseKey: ${env:SERVERLESS_LICENSE_KEY} 6 | 7 | plugins: 8 | - serverless-rollup-plugin # self-import 9 | - serverless-localstack 10 | 11 | custom: 12 | rollup: 13 | config: ./rollup.config.js 14 | 15 | provider: 16 | name: aws 17 | runtime: ${env:LAMBDA_NODE_VERSION} 18 | deploymentBucket: deployment-bucket 19 | 20 | functions: 21 | hello: 22 | handler: handler.hello 23 | -------------------------------------------------------------------------------- /test/fixtures/serverless-basic/handler.js: -------------------------------------------------------------------------------- 1 | export const hello = async (event) => ({ // eslint-disable-line import/prefer-default-export 2 | statusCode: 200, 3 | body: JSON.stringify( 4 | { 5 | message: 'Go Serverless v2.0! Your function executed successfully!', 6 | input: event, 7 | }, 8 | null, 9 | 2, 10 | ), 11 | }); 12 | -------------------------------------------------------------------------------- /test/fixtures/serverless-basic/rollup.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | output: { 3 | format: 'cjs', 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /test/fixtures/serverless-basic/serverless.yml: -------------------------------------------------------------------------------- 1 | service: serverless-basic 2 | 3 | frameworkVersion: '4' 4 | 5 | licenseKey: ${env:SERVERLESS_LICENSE_KEY} 6 | 7 | plugins: 8 | - serverless-rollup-plugin # self-import 9 | - serverless-localstack 10 | 11 | custom: 12 | rollup: 13 | config: ./rollup.config.js 14 | 15 | provider: 16 | name: aws 17 | runtime: ${env:LAMBDA_NODE_VERSION} 18 | deploymentBucket: deployment-bucket 19 | 20 | functions: 21 | hello: 22 | handler: handler.hello 23 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'node:path'; 2 | import { expect } from 'chai'; 3 | import StreamZip from 'node-stream-zip'; 4 | import { execa } from 'execa'; 5 | import { importFromStringSync, requireFromString } from 'module-from-string'; 6 | 7 | const runServerless = (cwd: string) => execa({ preferLocal: true, cwd, lines: true })`sls package --verbose --debug`; 8 | 9 | describe('general', () => { 10 | it('should package function as cjs', async () => { 11 | const cwd = new URL('fixtures/serverless-basic', import.meta.url).pathname; 12 | await runServerless(cwd); 13 | 14 | const zip = new StreamZip.async({ // eslint-disable-line new-cap 15 | file: join(cwd, '.serverless', 'serverless-basic-dev-hello.zip'), 16 | }); 17 | const js = await zip.entryData('index.js'); 18 | 19 | return expect(requireFromString(js.toString('utf8')).hello({ name: 'event' })).to.become({ 20 | body: `{ 21 | "message": "Go Serverless v2.0! Your function executed successfully!", 22 | "input": { 23 | "name": "event" 24 | } 25 | }`, 26 | statusCode: 200, 27 | }); 28 | }); 29 | 30 | it('should package function as esm', async () => { 31 | const cwd = new URL('fixtures/serverless-basic-esm', import.meta.url).pathname; 32 | await runServerless(cwd); 33 | 34 | const zip = new StreamZip.async({ // eslint-disable-line new-cap 35 | file: join(cwd, '.serverless', 'serverless-basic-dev-hello.zip'), 36 | }); 37 | const js = await zip.entryData('index.mjs'); 38 | 39 | return expect(importFromStringSync(js.toString('utf8')).hello({ name: 'event' })).to.become({ 40 | body: `{ 41 | "message": "Go Serverless v2.0! Your function executed successfully!", 42 | "input": { 43 | "name": "event" 44 | } 45 | }`, 46 | statusCode: 200, 47 | }); 48 | }); 49 | 50 | it('should transpile rollup.config.js to commonjs if required', async () => { 51 | const cwd = new URL('fixtures/rollup-transpile-commonjs', import.meta.url).pathname; 52 | const { stderr } = await runServerless(cwd); 53 | 54 | expect(stderr.some((message) => message.includes('Please switch to using \'mjs\' extension'))).to.be.true(); 55 | expect(stderr.some((message) => message.endsWith('Will load using commonjs transpilation.'))).to.be.true(); 56 | 57 | const zip = new StreamZip.async({ // eslint-disable-line new-cap 58 | file: join(cwd, '.serverless', 'serverless-basic-dev-hello.zip'), 59 | }); 60 | const js = await zip.entryData('index.mjs'); 61 | 62 | return expect(importFromStringSync(js.toString('utf8')).hello({ name: 'event' })).to.become({ 63 | body: `{ 64 | "message": "Go Serverless v2.0! Your function executed successfully!", 65 | "input": { 66 | "name": "event" 67 | } 68 | }`, 69 | statusCode: 200, 70 | }); 71 | }); 72 | 73 | it('should reuse rollup bundle when bundling multiple functions in one file', async () => { 74 | const cwd = new URL('fixtures/multiple-functions-per-file', import.meta.url).pathname; 75 | const { stderr } = await runServerless(cwd); 76 | 77 | expect(stderr.filter((message) => message.startsWith('Bundling '))).to.have.lengthOf(1); 78 | expect(stderr.filter((message) => message.startsWith('multiple-functions-per-file-dev-hello: Outputting bundle '))).to.have.lengthOf(1); 79 | expect(stderr.filter((message) => message.startsWith('multiple-functions-per-file-dev-world: Outputting bundle '))).to.have.lengthOf(1); 80 | 81 | const [hello, world] = await Promise.all([ 82 | new StreamZip.async({ // eslint-disable-line new-cap 83 | file: join(cwd, '.serverless', 'multiple-functions-per-file-dev-hello.zip'), 84 | }).entryData('index.js'), 85 | new StreamZip.async({ // eslint-disable-line new-cap 86 | file: join(cwd, '.serverless', 'multiple-functions-per-file-dev-world.zip'), 87 | }).entryData('index.js'), 88 | ]); 89 | 90 | return Promise.all([ 91 | expect(requireFromString(hello.toString('utf8')).hello()).to.become({ 92 | body: `{ 93 | "message": "Hello" 94 | }`, 95 | statusCode: 200, 96 | }), 97 | expect(requireFromString(world.toString('utf8')).world()).to.become({ 98 | body: `{ 99 | "message": "World" 100 | }`, 101 | statusCode: 200, 102 | }), 103 | ]); 104 | }); 105 | 106 | it('should bundle functions from multiple files', async () => { 107 | const cwd = new URL('fixtures/multiple-files', import.meta.url).pathname; 108 | const { stderr } = await runServerless(cwd); 109 | 110 | expect(stderr.filter((message) => message.startsWith('Bundling '))).to.have.lengthOf(2); 111 | expect(stderr.filter((message) => message.startsWith('multiple-files-dev-hello: Outputting bundle '))).to.have.lengthOf(1); 112 | expect(stderr.filter((message) => message.startsWith('multiple-files-dev-world: Outputting bundle '))).to.have.lengthOf(1); 113 | 114 | const [hello, world] = await Promise.all([ 115 | new StreamZip.async({ // eslint-disable-line new-cap 116 | file: join(cwd, '.serverless', 'multiple-files-dev-hello.zip'), 117 | }).entryData('index.js'), 118 | new StreamZip.async({ // eslint-disable-line new-cap 119 | file: join(cwd, '.serverless', 'multiple-files-dev-world.zip'), 120 | }).entryData('index.js'), 121 | ]); 122 | 123 | return Promise.all([ 124 | expect(requireFromString(hello.toString('utf8')).handle()).to.become({ 125 | body: `{ 126 | "message": "Hello" 127 | }`, 128 | statusCode: 200, 129 | }), 130 | expect(requireFromString(world.toString('utf8')).handle()).to.become({ 131 | body: `{ 132 | "message": "World" 133 | }`, 134 | statusCode: 200, 135 | }), 136 | ]); 137 | }); 138 | 139 | it('should default to using rollup.config.js', async () => { 140 | const cwd = new URL('fixtures/default-config', import.meta.url).pathname; 141 | await runServerless(cwd); 142 | 143 | const zip = new StreamZip.async({ // eslint-disable-line new-cap 144 | file: join(cwd, '.serverless', 'default-config-dev-hello.zip'), 145 | }); 146 | const js = await zip.entryData('index.js'); 147 | 148 | return expect(requireFromString(js.toString('utf8')).hello()).to.become({ 149 | body: 'default-config', 150 | statusCode: 200, 151 | }); 152 | }); 153 | }); 154 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | import { use } from 'chai'; 2 | 3 | use((await import('chai-as-promised')).default); // eslint-disable-line unicorn/no-await-expression-member 4 | use((await import('dirty-chai')).default); // eslint-disable-line unicorn/no-await-expression-member 5 | -------------------------------------------------------------------------------- /tsconfig.dist.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist" 6 | }, 7 | "include": ["src/**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "nodenext", 4 | "target": "esnext", 5 | "lib": ["es6", "dom"], 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "sourceMap": true 9 | } 10 | } 11 | --------------------------------------------------------------------------------