├── .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 | [](https://github.com/theBenForce/serverless-rollup-plugin/actions/workflows/release.yml)
4 | [](https://github.com/theBenForce/serverless-rollup-plugin/actions/workflows/testing.yml)
5 | [](https://codeclimate.com/github/theBenForce/serverless-rollup-plugin/maintainability)
6 | [](https://badge.fury.io/js/serverless-rollup-plugin)
7 | [](https://app.fossa.com/projects/git%2Bgithub.com%2FtheBenForce%2Fserverless-rollup-plugin?ref=badge_shield)
8 | [](#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 |
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 |
--------------------------------------------------------------------------------