master 46a644c60223 cached
28 files
60.0 KB
15.4k tokens
11 symbols
1 requests
Download .txt
Repository: colynb/serverless-dotenv-plugin
Branch: master
Commit: 46a644c60223
Files: 28
Total size: 60.0 KB

Directory structure:
gitextract_cnyp1whv/

├── .editorconfig
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── breaking-change.md
│   │   ├── bug_report.md
│   │   └── feature-request.md
│   ├── labeler.yml
│   ├── pull_request_template.md
│   └── workflows/
│       ├── ci.yaml
│       ├── issue_comment-created.yml
│       ├── issues-cron.yml
│       ├── labeler.yml
│       ├── release.yml
│       ├── scheduled-node-dependency-updates.yml
│       └── tag-new-npm-package-releases-change.yml
├── .gitignore
├── .ncurc.yml
├── .prettierrc
├── CHANGELOG.md
├── LICENSE
├── README.md
├── examples/
│   └── simple-express-app/
│       ├── .gitignore
│       ├── README.md
│       ├── index.js
│       ├── package.json
│       └── serverless.yml
├── package.json
├── src/
│   └── index.js
└── test/
    └── index.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
root = true

[*.*]
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true


================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms

github: neverendingqs
patreon: # Replace with a single Patreon username
open_collective: # serverless-dotenv-plugin
ko_fi: neverendingqs
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']


================================================
FILE: .github/ISSUE_TEMPLATE/breaking-change.md
================================================
---
name: Breaking Change
about: Breaking Change Definition of Done
title: ''
labels: ''
assignees: ''

---

* [ ] Update README to recommend new safe default
* [ ] Update README to warn about deprecation
* [ ] Add or update toggle to switch behaviour on major version bump


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Use this if you think there is a bug with the plugin
title: ''
labels: bug
assignees: ''

---

Please include a [Minimal, Reproducible Example](https://stackoverflow.com/help/minimal-reproducible-example).


================================================
FILE: .github/ISSUE_TEMPLATE/feature-request.md
================================================
---
name: Feature Request
about: A new feature you would like to see
title: ''
labels: enhancement
assignees: ''

---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen. Please include a sample `serverless.yml` file if possible.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.


================================================
FILE: .github/labeler.yml
================================================
chore:
  - .*
  - .github/**/*
  - LICENSE
  - package.json

documentation:
  - CHANGELOG.md
  - examples/**/*
  - README.md


================================================
FILE: .github/pull_request_template.md
================================================
## Description

<!-- Describe the feature here with examples if possible -->

## Checklist

<!-- Note: not all checklist items are applicable to all pull requests -->

* [ ] CHANGELOG.md
* [ ] README.md
* [ ] Unit tests
* [ ] Consider performance implications
* [ ] Consider security implications

## Exploratory Test Notes



================================================
FILE: .github/workflows/ci.yaml
================================================
name: CI

on:
  pull_request:
  push:
    branches:
      - master

jobs:
  build:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    strategy:
      fail-fast: false
      matrix:
        serverless: ['1', '2', '3']
        node: [ '14', '15', '16', '18', '19', '20', '21' ]
    name: CI (Node v${{ matrix.node }}, serverless@${{ matrix.serverless }})
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          always-auth: true
          node-version: ${{ matrix.node }}
          registry-url: https://registry.npmjs.org
      - run: npm ci
      - run: npm test
      - name: Integration Tests
        run: |
          npm ci

          # Some versions of npm install peer dependencies
          npm i serverless@${{ matrix.serverless }}
          npx serverless --version

          npx serverless package --stage production

          # Must match value of `TEST_VAR` in `.env.production`
          cat .serverless/cloudformation-template-update-stack.json | grep cf5bb467-9052-4a34-b318-f6df31644229
        working-directory: examples/simple-express-app

      - name: publish
        # Only publish (maybe) for one version of Node, and only on 'master'
        if: ${{ matrix.node == 14 && matrix.serverless == '3' && github.ref == 'refs/heads/master' }}
        run: npm run deploy
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}

      - name: Coveralls
        # Only send coverage details for one version of Node
        if: ${{ matrix.node == 14 }}
        uses: coverallsapp/github-action@master
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .github/workflows/issue_comment-created.yml
================================================
name: Issue Comments (Created).

on:
  issue_comment:
    types: [created]

jobs:
  remove-label:
    name: Remove labels used for tracking issues that a contributor is awaiting a response.
    runs-on: ubuntu-latest

    steps:
      - uses: actions/github-script@v3
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            console.log(JSON.stringify({
              labels: context.payload.issue.labels,
              login: context.payload.sender.login
            }));
            
            if(context.payload.sender.login === 'neverendingqs') {
              // Do not remove labels if a contributor wrote the comment
              return;
            }
            
            const labels = [
              'awaiting response'
            ];
            
            labels.forEach(label => {
              const hasLabel = context.payload.issue.labels
                .map(({ name }) => name)
                .includes(label);

              if(hasLabel) {
                github.issues.removeLabel({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  issue_number: context.issue.number,
                  name: label,
                });
              }
            });



================================================
FILE: .github/workflows/issues-cron.yml
================================================
name: Issues (Cron)
on:
  schedule:
    - cron: '0 14 * * *'
  workflow_dispatch:

jobs:
  stale:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/stale@v3
        with:
          close-issue-message: Closed due to inactivity. Please reopen when you get a chance to respond.
          days-before-close: 1
          days-before-stale: 30
          only-labels: 'awaiting response'
          skip-stale-issue-message: true
          skip-stale-pr-message: true
          stale-issue-label: stale
          stale-issue-message: 'Marking issue as stale.'  # Required even if 'skip-stale-issue-message' is true
          stale-pr-message: 'Marking issue as stale.'     # Required even if 'skip-stale-issue-message' is true


================================================
FILE: .github/workflows/labeler.yml
================================================
name: Pull Request Labeler
on: pull_request_target

jobs:
  triage:
    permissions:
      contents: read
      pull-requests: write
    runs-on: ubuntu-latest
    steps:
      - uses: actions/labeler@v4
        with:
          repo-token: ${{ secrets.GITHUB_TOKEN }}
          sync-labels: true


================================================
FILE: .github/workflows/release.yml
================================================
name: Release new version
on:
  workflow_dispatch:
    inputs:
      release-type:
        description: Type of release based on semver (e.g. patch, minor, major)
        required: true
        default: minor

jobs:
  release:
    name: Release new version to NPM
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
      - name: Setup Git
        run: |
          git config user.name github-actions[bot]
          git config user.email 41898282+github-actions[bot]@users.noreply.github.com
      - run: npm version --no-git-tag-version ${{github.event.inputs.release-type}}
      - run: git push
        env:
          GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }}
      # Rely on `ci` workflow to publish new version.


================================================
FILE: .github/workflows/scheduled-node-dependency-updates.yml
================================================
name: Scheduled Node Dependencies Update (npm)
on:
  schedule:
    - cron:  '0 15 1 * *'
  workflow_dispatch:
jobs:
  update-deps:
    name: Update Node dependencies using npm
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v3
        with:
          node-version: 16
      - uses: neverendingqs/gh-action-node-update-deps@v2
        with:
          bump-version: patch
          github-token: ${{ secrets.REPO_GITHUB_TOKEN }}


================================================
FILE: .github/workflows/tag-new-npm-package-releases-change.yml
================================================
name: Tag new npm package releases
on:
  push:
    branches:
      - master
    paths:
      - package.json
jobs:
  tag-npm-release:
    name: Tag new npm package releases
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2
      - uses: neverendingqs/gh-action-tag-on-npm-version@master


================================================
FILE: .gitignore
================================================
.nyc_output/
.serverless/
.vscode/

coverage/
node_modules/

yarn.lock


================================================
FILE: .ncurc.yml
================================================
reject:
  # `serverless@2` still supports Node 10, so we can't switch to ESM
  # https://github.com/chalk/chalk/releases/tag/v5.0.0
  - chalk
  # `mocha>=10.0.0` doesn't support Node 10 or Node 12
  - mocha


================================================
FILE: .prettierrc
================================================
{
  "semi": true,
  "singleQuote": true,
  "trailingComma": "all"
}


================================================
FILE: CHANGELOG.md
================================================
# Changelog

Only major and minor version changes are included in this file. Changes not
included in this log but can be reviewed on GitHub:

* ["Chore"](https://github.com/neverendingqs/serverless-dotenv-plugin/pulls?q=+is%3Apr+label%3Achore+)
* [Documentation](https://github.com/neverendingqs/serverless-dotenv-plugin/pulls?q=+is%3Apr+label%3Adocumentation)
* [Refactor](https://github.com/neverendingqs/serverless-dotenv-plugin/pulls?q=label%3Arefactor+is%3Apr)

## Unreleased

Breaking changes introduced:

* feat: now halts on all errors. ([#139](https://github.com/neverendingqs/serverless-dotenv-plugin/pull/139))
  * Previously, some errors did not cause the plugin to halt, which may silently cause environment variables to not be set.
  * Note: `required.file` continues to default to `false`.
    * This is because your environment variables might not be stored in dotenv files in all environments.
    * Setting `required.file` to `true` will continue to cause the plugin to halt if no dotenv files are found.

## 6.0.x

* chore: update deps (2023-03-18) ([#237](https://github.com/neverendingqs/serverless-dotenv-plugin/pull/237))
  * The following packages were upgraded to the latest major version, which introduced changes to how `dotenv` files are parsed:
    * `dotenv-expand` (`^8.0.3` to `^10.0.0`)

## 5.0.x

* chore: remove support for Node.js 10 and Node.js 12 ([#236](https://github.com/neverendingqs/serverless-dotenv-plugin/pull/236))
  * Also added support for Node.js 18 and 19 (LTS and Current)

## 4.0.x

* chore: update deps (2022-04-17) ([#195](https://github.com/neverendingqs/serverless-dotenv-plugin/pull/195))
  * The following packages were upgraded to the latest major version, which introduced changes to how `dotenv` files are parsed:
    * `dotenv` (`^10.0.0` to `^16.0.0`)
    * `dotenv-expand` (`^5.1.0` to `^8.0.3`)

## 3.12.x

* feat: Adapt to `serverless@3` logging interface ([#174](https://github.com/neverendingqs/serverless-dotenv-plugin/pull/174))

## 3.11.x

* feat: add support for `serverless@3`. ([#178](https://github.com/neverendingqs/serverless-dotenv-plugin/pull/178))
* docs(README): add details around UNSUPPORTED_CLI_OPTIONS. ([#177](https://github.com/neverendingqs/serverless-dotenv-plugin/pull/177))
* fix: support for `serverless@pre-3`. ([#180](https://github.com/neverendingqs/serverless-dotenv-plugin/pull/180))

## 3.10.x

* chore(package.json): register `serverless` as peer dependency. ([#159](https://github.com/neverendingqs/serverless-dotenv-plugin/pull/159))

## 3.9.x

* feat: support "*" for include config. ([#146](https://github.com/neverendingqs/serverless-dotenv-plugin/pull/146))

## 3.8.1

* fix: undo behaviour around include = []. ([#145](https://github.com/neverendingqs/serverless-dotenv-plugin/pull/145))

## 3.8.0

* feat: adding an option to toggle breaking changes. ([#138](https://github.com/neverendingqs/serverless-dotenv-plugin/pull/138))

## 3.7.x

* fix: only package required files. ([#134](https://github.com/neverendingqs/serverless-dotenv-plugin/pull/134))

## 3.6.x

* feat: adding support for custom dotenv parser. ([#127](https://github.com/neverendingqs/serverless-dotenv-plugin/pull/127))

## 3.5.x

* feat: now logs when incompatible configs are set. ([#124](https://github.com/neverendingqs/serverless-dotenv-plugin/pull/124))

## 3.4.x

* feat: new option to expect specific env vars to be set. ([#118](https://github.com/neverendingqs/serverless-dotenv-plugin/pull/118))

## 3.3.x

* feat: adding variableExpansion option to turn off variable expansion. ([#116](https://github.com/neverendingqs/serverless-dotenv-plugin/pull/116))

## 3.2.x

* refactor: use helper functions to help with readabilty and future changes. ([#112](https://github.com/neverendingqs/serverless-dotenv-plugin/pull/112))
* Significant changes to documentation structure ([#85..#108 labelled `documentation`](https://github.com/neverendingqs/serverless-dotenv-plugin/pulls?q=is%3Apr+label%3Adocumentation+closed%3A2021-02-06..2021-02-07+))

## 3.0.x

* feat: Load `.env.*.local` envs ([#55](https://github.com/neverendingqs/serverless-dotenv-plugin/pull/55)) ([@danilofuchs](https://github.com/danilofuchs))

## Previous Versions

This list is not exhaustive:

* https://colyn.dev/serverless-dotenv-plugin-changelog/
* Added exclude option to custom config; updated documentation ([#36](https://github.com/neverendingqs/serverless-dotenv-plugin/pull/36)) ([@smcelhinney](https://github.com/smcelhinney))
* Added custom dotenv logging config option to disable serverless cli log output ([#37](https://github.com/neverendingqs/serverless-dotenv-plugin/pull/37)) ([@kristopherchun](https://github.com/kristopherchun))


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2017 InFront Labs

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# serverless-dotenv-plugin

[![CI](https://github.com/neverendingqs/serverless-dotenv-plugin/workflows/CI/badge.svg)](https://github.com/neverendingqs/serverless-dotenv-plugin/actions?query=workflow%3ACI+branch%3Amaster)
[![Coverage Status](https://coveralls.io/repos/github/neverendingqs/serverless-dotenv-plugin/badge.svg?branch=master)](https://coveralls.io/github/neverendingqs/serverless-dotenv-plugin?branch=master)
[![npm version](https://img.shields.io/npm/v/serverless-dotenv-plugin.svg?style=flat)](https://www.npmjs.com/package/serverless-dotenv-plugin)

Preload function environment variables into Serverless. Use this plugin if you have variables stored in a `.env` file that you want loaded into your functions.

This used to also preload environment variables into your `serverless.yml` config, but no longer does with `serverless>=2.26.0`.
See [this discussion thread](https://github.com/neverendingqs/serverless-dotenv-plugin/discussions/155) or the FAQ below for details on the impact of how environment variables are loaded with `serverless>=2.26.0` and `serverless>=3.0.0`.**

## Do you need this plugin?

Serverless Framework can now natively resolve `${env:xxx}` variables from `.env` files by setting [`useDotenv: true` in the configuration](https://www.serverless.com/framework/docs/environment-variables):

```yaml
useDotenv: true

provider:
  environment:
    FOO: ${env:FOO}
```

For more complex situations, you will need to [wire up `dotenv` yourself](https://github.com/neverendingqs/serverless-dotenv-example).

This plugin is only useful if you want to automatically import **all** variables from `.env` into functions:

```yaml
plugins:
  - serverless-dotenv-plugin

provider:
  environment:
    # With the plugin enabled, all variables in .env are automatically imported
```

## Install and Setup

First, install the plugin:

```bash
> npm i -D serverless-dotenv-plugin
```

Next, add the plugin to your serverless config file:

```yaml
service: myService
plugins:
  - serverless-dotenv-plugin
...
```

Now, just like you would using [dotenv](https://www.npmjs.com/package/dotenv) in any other JS application, create your `.env` file in the root of your app:

```bash
DYNAMODB_TABLE=myTable
AWS_REGION=us-west-1
AUTH0_CLIENT_ID=abc12345
AUTH0_CLIENT_SECRET=12345xyz
```

When deploying, all the variables listed in `.env` will automatically be available in the deployed functions.

## Automatic ENV File Resolution

By default, the plugin looks for the file: `.env`. In most use cases this is all that is needed. However, there are times where you want different env files based on environment. For instance:

```bash
.env.development
.env.production
```

When you deploy with `NODE_ENV` set: `NODE_ENV=production sls deploy` the plugin will look for files named `.env`, `.env.production`, `.env.production.local`. If for some reason you can't set NODE_ENV, you could always just pass it in as an option: `sls deploy --env production` or `sls deploy --stage production`. If `NODE_ENV`, `--env` or `--stage` is not set, it will default to `development`.

**DEPRECATION WARNING**: as of `serverless>=3.0.0`, `--env` will not be supported due to changes to the Serverless Framework.
See FAQ for details.

The precedence between the options is the following:
`NODE_ENV` **>** `--env` **>** `--stage`

The env resolution pattern follows the one used by [Rail's dotenv](https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use) and [create-react-app](https://create-react-app.dev/docs/adding-custom-environment-variables/#what-other-env-files-can-be-used)

| Valid .env file names | Description                                                                          |
| --------------------- | ------------------------------------------------------------------------------------ |
| .env                  | Default file, always included                                                        |
| .env.local            | Included in all environments except test                                             |
| .env.development      | If NODE_ENV or --env or --stage **is not set**, will try to load `.env.development`. |
| .env.{ENV}            | If NODE_ENV or --env or --stage **is set**, will try to load `.env.{env}`.           |
| .env.{ENV}.local      | Every env set up in `.env.{ENV}.local` **will override** other envs                  |

> Note: .env, .env.development, and .env.production files should be included in your repository as they define defaults. .env\*.local should be added to .gitignore, as those files are intended to be ignored. .env.local is where secrets can be stored.


## Lambda Environment Variables

Again, remember that when you deploy your service, the plugin will inject these environment vars into every lambda functions you have and will therefore allow you to reference them as `process.env.AUTH0_CLIENT_ID` (Nodejs example). If this behaviour is not desireable, set `include` to `[]`.


## Plugin options

All options are optional.

```yaml
custom:
  dotenv:
    # default: project root
    path: path/to/my/dotenvfiles

    # if set, ignores `path` option, and only uses the dotenv file at this location
    # basePath: path/to/my/.env

    # if set, uses provided dotenv parser function instead of built-in function
    dotenvParser: dotenv.config.js

    # default: adds all env variables found in your dotenv file(s)
    # this option must be set to `[]` if `provider.environment` is not a literal string
    include:
      - DDB_TABLE
      - S3_BUCKET

    # default: does not exclude any env variables found in your dotenv file(s)
    # does nothing if `include` is set
    exclude:
      - AWS_ACCESS_KEY_ID
      - AWS_SECRET_ACCESS_KEY
      - AWS_SESSION_TOKEN
      - NODE_ENV              # Can not be declared for Google Cloud Functions

    # defaults to `true`
    logging: false

    # default: plugin does not cause an error if any file or env variable is missing
    required:
      # default: []
      env:
        - API_KEY

      # default: false
      file: true

    # default: true
    variableExpansion: false
```

* path (string)
  * The plugin will look for your .env file in the same folder where you run the command using the file resolution rules as described above, but these rules can be overridden by setting the `path` option.
  * This will **disable** automatic env file resolution.

* basePath (string)
  * The problem with setting the `path` option is that you lose environment resolution on the file names.
  * If you don't need environment resolution, the `path` option is just fine.

* dotenvParser (string)
  * Path to a custom dotenv parser, relative to the project root (same level as `serverless.yml`).
  * Parameters passed into the function: `{ dotenv, paths }`.
    * `dotenv`: dotenv library provided for you or you can bring your own
    * `paths`: all the dotenv files discovered by the plugin, ordered by precedence (see `Automatic ENV File Resolution` above for details)
  * This function must return a single object, where each key/value pair represents the env var name and value.
  * By default, this uses the built-in parser, which calls `dotenv` followed by `dotenv-expand` for each file.

* include (list or `'*'`) (default: `'*'`)
  * All env vars found in your file will be injected into your lambda functions.
  * If you do not want all of them to be injected into your lambda functions, you can specify the ones you want with the `include` option.
  * If set to `'*'`, all env vars in all dotenv files will be injected.
  * If set to an empty list (`[]`), no env vars will be injected.
  * This option must be set to `[]` if `provider.environment` is not a literal string (see FAQ for details).

* exclude (list)
  * If you do not want all of them to be injected into your lambda functions, you can specify the ones you do not want with the `exclude` option.
  * Note, this is only available if the `include` option has not been set.

* logging: true|false
  * Supresses all logging done by this plugin if no errors are encountered.

* required
  * env: (list)
    * A set of env var that must be set either in the Serverless environment or via a `dotenv` file.
    * Throws an error if a required env var is not found.
    * By default, no env vars are required.
  * file: true|false (default false)
    * By default, this plugin will exit gracefully and allow Serverless to continue even if it couldn't find a .env file to use.
    * Set this to `true` to cause Serverless to halt if it could not find a .env file to use.

* v4BreakingChanges: true|false (default false)
  * Set this to `true` to introduce v3.x.x => v4.x.x breaking changes now

* variableExpansion: true|false (default true)
  * By default, variables can reference other variables
    * E.g. `INNER_ENV=innerenv, OUTER_ENV=hi-$INNER_ENV`, would resolve to `INNER_ENV=innerenv, OUTER_ENV=hi-innerenv`
  * Setting this to `false` will disable this feature
    * E.g. `INNER_ENV=innerenv, OUTER_ENV=hi-$INNER_ENV`, would resolve to `INNER_ENV=innerenv, OUTER_ENV=hi-$INNER_ENV`


Example `dotenvParser` file:

```js
// You can bring your own or use the one provided by the plugin
const dotenv = require('dotenv')
const dotenvExpand = require('dotenv-expand')

module.exports = function({ dotenv, paths }) {
  const envVarsArray = [...paths]
    .reverse()
    .map(path => {
      const parsed = dotenv.config({ path })
      return dotenvExpand(parsed).parsed
    })

  return envVarsArray.reduce((acc, curr) => ({ ...acc, ...curr }), {})
}
```

## Examples

You can find example usage in the `examples` folder.

## Changelog

The changelog is available in the `CHANGELOG.md` file in the package or [on GitHub](https://github.com/neverendingqs/serverless-dotenv-plugin/blob/master/CHANGELOG.md).

## FAQ

This plugin loads the dotenv environment variables inside the plugin constructor. Aside from legacy reasons, this also means all your dotenv environment variables are available to the other plugins being loaded.

However, Serverless variables are **not** resolved in the constructor:

> Variable references in the serverless instance are not resolved before a Plugin's constructor is called, so if you need these, make sure to wait to access those from your hooks.
> ~https://www.serverless.com/framework/docs/providers/aws/guide/plugins/#plugins/

This is important for several FAQ items below.

### How has changes to the Serverless Framework affected when environment variables are loaded?

#### `serverless>=2.26.0`

[serverless/serverless#8987](https://github.com/serverless/serverless/pull/8987) changed the order of when plugins are initialized in relationship to variable resolution as part of a larger initiative outlined in [serverless/serverless#8364](https://github.com/serverless/serverless/issues/8364). Because of this, any env var references inside JavaScript files will now get evaluated too early in the process.

#### `serverless>=3.0.0`

`env` variables will get resolved before this plugin is initialized. This means `env` variables inside `serverless.yml` can **no longer** rely on this plugin to load them from `dotenv` files. See [serverless/serverless#8364](https://github.com/serverless/serverless/issues/8364) for more details on the changes made to the Serverless Framework variables engine.

The [Serverless Framework has basic `dotenv` support built-in](https://www.serverless.com/framework/docs/environment-variables/). For support with more complicated workflows with `dotenv`, see [`serverless-dotenv-example`](https://github.com/neverendingqs/serverless-dotenv-example) for details.

You can continue to use this plugin to automatically load environment variables into all your functions using `dotenv`.

### How has changes to the Serverless Framework affected configuration options?

See [deprecation code `UNSUPPORTED_CLI_OPTIONS` for more details](https://www.serverless.com/framework/docs/deprecations/#UNSUPPORTED_CLI_OPTIONS).
This was introduced in [serverless/serverless#9171](https://github.com/serverless/serverless/pull/9171).

#### `serverless>=2.32.0`

Using the `--env` CLI option will now result in the following warning:

```
Detected unrecognized CLI options: "--env".
Starting with the next major, Serverless Framework will report them with a thrown error
More Info: https://www.serverless.com/framework/docs/deprecations/#UNSUPPORTED_CLI_OPTIONS
```

#### `serverless>=3.0.0`

Using the `--env` CLI option will now result in the following error:

```
Error:
Detected unrecognized CLI options: "--env".
```

### Why do env vars already defined by the system take higher precedence?

The [Serverless Framework has basic `dotenv` support built-in](https://www.serverless.com/framework/docs/environment-variables/). If you are loading variables from `.env` at the project root, it is possible the Serverless Framework preloads that env var before this plugin does.

As well, because of the variables engine changed in `serverless>=2.26.0` (see above), `env` variables can also be resolved before this plugin runs, which means Serverless could take the values already defined in the system before the plugin loads env vars via `dotenv`.


### Why doesn't the `basePath` or `path` options support Serverless variables?

Because Serverless variables have not been interpolated when this plugin runs, `basePath` and `path` will always be treated like literal strings (e.g. `${opt:stage}` would be presented to the plugin, not the passed in via `--stage`). The suggested pattern is to store all your dotenv files in one folder, and rely on `NODE_ENV`, `--env`, or `--stage` to resolve to the right file.

There are no plans to support anything other than literal strings at this time, although you are free to discuss this in [#52](https://github.com/neverendingqs/serverless-dotenv-plugin/issues/52).

### Why doesn't this plugin work when `provider.environment` references another file?

Upgrade to `serverless>=2.26.0`. The new variables engine introduced in the Serverless Framework in v2.26.0 now resolves `file` variables first before loading initializing any plugins.

Before v2.26.0, Serverless variables do not get interpolated before this plugin gets initialized, causing `provider.environment` to be presented to this plugin uninterpolated (e.g. `${file(./serverless-env.yml):environment}`). Because of this, the plugin tries to append items to a string instead of a list.

To work around this, you can set the `include` option to `[]` to avoid adding any environment variables to `provider.environment`. However, this means you will have to wire up the environment variables yourself by referencing every single one you need. E.g.

```yaml
provider:
  environment:
    - DDB_TABLE: ${env:DDB_TABLE}
```

More details are available at [#38](https://github.com/neverendingqs/serverless-dotenv-plugin/issues/38).

## Contributing

Because of the highly dependent nature of this plugin (i.e. thousands of developers depend on this to deploy their apps to production) I cannot introduce changes that are backwards incompatible. Any feature requests must first consider this as a blocker. If submitting a PR ensure that the change is developer opt-in only meaning it must guarantee that it will not affect existing workflows, it's only available with an opt-in setting. I appreciate your patience on this. Thanks.


================================================
FILE: examples/simple-express-app/.gitignore
================================================
node_modules
.env*.local

================================================
FILE: examples/simple-express-app/README.md
================================================
### Example Express App

This is a quick example setting up serverless-dotenv-plugin with an express app. It includes an env file for development and one for production:

Development
```
> sls deploy
```

Production
```
> NODE_ENV=production sls deploy
```
or
```
> sls deploy --env production
```
or
```
> sls deploy --stage production
```


================================================
FILE: examples/simple-express-app/index.js
================================================
const serverless = require('serverless-http');
const express = require('express');
const app = express();

app.get('/', function (req, res) {
  res.send(`Hello ${process.env.APP_MESSAGE}!`);
});

module.exports.handler = serverless(app);


================================================
FILE: examples/simple-express-app/package.json
================================================
{
  "name": "my-express-application",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.18.2",
    "serverless-http": "^3.2.0"
  },
  "devDependencies": {
    "serverless-dotenv-plugin": "^6.0.0"
  },
  "peerDependencies": {
    "serverless": "1 || 2 || pre-3 || 3"
  }
}


================================================
FILE: examples/simple-express-app/serverless.yml
================================================
# serverless.yml

service: my-express-application

plugins:
  - ../../

provider:
  name: aws
  runtime: nodejs14.x

functions:
  app:
    handler: index.handler
    events:
      - http:
          path: / # this matches the base path
          method: ANY
      - http:
          path: /{any+} # this matches any path, the token 'any' doesn't mean anything special
          method: ANY


================================================
FILE: package.json
================================================
{
  "name": "serverless-dotenv-plugin",
  "version": "6.0.0",
  "description": "Preload environment variables with dotenv into serverless.",
  "main": "src/index.js",
  "files": [
    "src/**/*"
  ],
  "scripts": {
    "deploy": "publish",
    "prettier": "prettier --ignore-path .gitignore --write \"**/*.js\"",
    "test": "nyc --reporter=lcov --reporter=text mocha",
    "posttest": "prettier --ignore-path .gitignore --check \"**/*.js\""
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/neverendingqs/serverless-dotenv-plugin.git"
  },
  "keywords": [
    "serverless",
    "dotenv",
    "serverless-plugin"
  ],
  "author": "Colyn Brown <colyn.brown@gmail.com>",
  "contributors": [
    "Mark Tse <mark.tse@neverendingqs.com> (https://neverendingqs.com/)"
  ],
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/neverendingqs/serverless-dotenv-plugin/issues"
  },
  "homepage": "https://github.com/neverendingqs/serverless-dotenv-plugin#readme",
  "dependencies": {
    "chalk": "^4.1.2",
    "dotenv": "^16.0.3",
    "dotenv-expand": "^10.0.0"
  },
  "devDependencies": {
    "chai": "^4.3.7",
    "mocha": "^9.2.2",
    "nyc": "^15.1.0",
    "prettier": "^2.8.4",
    "proxyquire": "^2.1.3",
    "publish-me-maybe": "^1.0.12",
    "sinon": "^15.0.2",
    "sinon-chai": "^3.7.0"
  },
  "peerDependencies": {
    "serverless": "1 || 2 || pre-3 || 3"
  }
}


================================================
FILE: src/index.js
================================================
'use strict';

const dotenv = require('dotenv');
const dotenvExpand = require('dotenv-expand');
const chalk = require('chalk');
const fs = require('fs');
const path = require('path');

const errorTypes = {
  HALT: 'HALT',
};

const logLevels = {
  NOTICE: 'NOTICE',
  WARNING: 'WARNING',
};

class ServerlessPlugin {
  constructor(serverless, options, v3Utils) {
    this.serverless = serverless;
    this.serverless.service.provider.environment =
      this.serverless.service.provider.environment || {};

    this.config = Object.assign(
      {
        exclude: [],
        include: '*',
        logging: true,
        required: {},
        variableExpansion: true,
        v4BreakingChanges: false,
      },
      (this.serverless.service.custom &&
        this.serverless.service.custom['dotenv']) ||
        {},
    );

    if (this.config.dotenvParser) {
      this.config.dotenvParserPath = path.join(
        serverless.config.servicePath,
        this.config.dotenvParser,
      );
    }

    this.v3Utils = v3Utils;
    this.loadEnv(this.getEnvironment(options));
  }

  log(msg, level = logLevels.NOTICE) {
    if (!this.config.logging) {
      return;
    }

    if (!this.v3Utils) {
      this.serverless.cli.log(msg);
      return;
    }

    // Add logging methods as needed
    // https://www.serverless.com/framework/docs/guides/plugins/cli-output#writing-to-the-output
    switch (level) {
      case logLevels.NOTICE:
        this.v3Utils.log.notice(msg);
        break;

      case logLevels.WARNING:
        this.v3Utils.log.warning(msg);
        break;

      default:
        throw new Error(`Unsupported log level '${level}'. Message: '${msg}'`);
    }
  }

  /**
   * @param {Object} options
   * @returns {string}
   */
  getEnvironment(options) {
    return (
      process.env.NODE_ENV || options.env || options.stage || 'development'
    );
  }

  /**
   * @param {string} env
   * @returns {string[]}
   */
  resolveEnvFileNames(env) {
    const basePath = (this.config && this.config.basePath) || '';

    if (this.config && this.config.path) {
      if (basePath) {
        this.log(
          'DOTENV (WARNING): if "path" is set, "basePath" is ignored.',
          logLevels.WARNING,
        );
      }

      if (Array.isArray(this.config.path)) {
        return this.config.path;
      }
      return [this.config.path];
    }

    // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
    const dotenvFiles = [
      `.env.${env}.local`,
      `.env.${env}`,
      // Don't include `.env.local` for `test` environment
      // since normally you expect tests to produce the same
      // results for everyone
      env !== 'test' && `.env.local`,
      `.env`,
    ];

    const filesNames = dotenvFiles.map((file) => basePath + file);

    return filesNames.filter((fileName) => fs.existsSync(fileName));
  }

  /**
   * @param {string[]} envFileNames
   * @returns {Object}
   */
  callDotenvParser(envFileNames) {
    try {
      return require(this.config.dotenvParserPath)({
        dotenv,
        paths: envFileNames,
      });
    } catch (err) {
      throw Object.assign(err, { type: errorTypes.HALT });
    }
  }

  /**
   * @param {string[]} envFileNames
   * @returns {Object}
   */
  parseEnvFiles(envFileNames) {
    const envVarsArray = envFileNames.map((fileName) => {
      const parsed = dotenv.config({ path: fileName });
      return this.config.variableExpansion
        ? dotenvExpand.expand(parsed).parsed
        : parsed.parsed;
    });

    return envVarsArray.reduce((acc, curr) => ({ ...acc, ...curr }), {});
  }

  /**
   * @param {Object} envVars
   */
  setProviderEnv(envVars) {
    const include = this.config && this.config.include;
    const exclude = (this.config && this.config.exclude) || [];

    if (include !== '*') {
      if (exclude.length > 0) {
        this.log(
          'DOTENV (WARNING): if "include" is set, "exclude" is ignored.',
          logLevels.WARNING,
        );
      }

      Object.keys(envVars)
        .filter((key) => !include.includes(key))
        .forEach((key) => {
          delete envVars[key];
        });
    } else if (exclude.length > 0) {
      Object.keys(envVars)
        .filter((key) => exclude.includes(key))
        .forEach((key) => {
          delete envVars[key];
        });
    }

    Object.keys(envVars).forEach((key) => {
      this.log('\t - ' + key);
      this.serverless.service.provider.environment[key] = envVars[key];
    });
  }

  /**
   * @param {string[]} envFileNames
   */
  validateEnvFileNames(envFileNames) {
    if (envFileNames.length > 0) {
      this.log(
        'DOTENV: Loading environment variables from ' +
          [...envFileNames].reverse().join(', ') +
          ':',
      );
    } else {
      const errorMsg = 'DOTENV: Could not find .env file.';
      this.log(errorMsg);

      if (this.config.required.file === true) {
        throw Object.assign(new Error(errorMsg), { type: errorTypes.HALT });
      }
    }
  }

  /**
   * @param {string[]} envFileNames
   */
  validateEnvVars(envVars) {
    if (!envVars) {
      throw Object.assign(
        new Error(
          'Unexpected env var object (expected an object but is falsy). Did you forget to return an object in your dotenv parser?',
        ),
        { type: errorTypes.HALT },
      );
    }

    const missingRequiredEnvVars = (this.config.required.env || []).filter(
      (envVarName) => !envVars[envVarName] && !process.env[envVarName],
    );

    if (missingRequiredEnvVars.length > 0) {
      throw Object.assign(
        new Error(
          `Missing the following required environment variables: ${missingRequiredEnvVars.join(
            ',',
          )}`,
        ),
        { type: errorTypes.HALT },
      );
    }
  }

  /**
   * @param {string} env
   */
  loadEnv(env) {
    const envFileNames = this.resolveEnvFileNames(env);
    try {
      this.validateEnvFileNames(envFileNames);

      const envVars = this.config.dotenvParserPath
        ? this.callDotenvParser(envFileNames)
        : this.parseEnvFiles(envFileNames);

      this.validateEnvVars(envVars);
      this.setProviderEnv(envVars);
    } catch (e) {
      if (
        e.type === errorTypes.HALT ||
        this.config.v4BreakingChanges === true
      ) {
        throw e;
      }

      console.error(
        chalk.red(
          '\n Serverless Plugin Error --------------------------------------\n',
        ),
      );
      console.error(chalk.red('  ' + e.message));
    }
  }
}

module.exports = ServerlessPlugin;


================================================
FILE: test/index.js
================================================
process.env.TEST_SLS_DOTENV_PLUGIN_ENV1 = 'env1';

const chai = require('chai');
const path = require('path');
const proxyquire = require('proxyquire').noCallThru();
const should = chai.should();
const sinon = require('sinon');

chai.use(require('sinon-chai'));

const logLevels = {
  NOTICE: 'NOTICE',
  WARNING: 'WARNING',
};

describe('ServerlessPlugin', function () {
  beforeEach(function () {
    this.sandbox = sinon.createSandbox();

    this.dotenvParser = {
      path: 'dotenvParser.js',
      prefix: '/tmp',
    };

    this.dotenvParser.fullPath = path.join(
      this.dotenvParser.prefix,
      this.dotenvParser.path,
    );

    this.requireStubs = {
      chalk: {
        red: this.sandbox.stub(),
      },
      dotenv: {
        config: this.sandbox.stub(),
      },
      'dotenv-expand': {
        expand: this.sandbox.stub(),
      },
      fs: {
        existsSync: this.sandbox.stub(),
      },
      [this.dotenvParser.fullPath]: this.sandbox.stub(),
    };

    this.ServerlessPlugin = proxyquire('../src', this.requireStubs);

    this.serverless = {
      cli: {
        log: this.sandbox.stub(),
      },
      service: {
        provider: {},
      },
    };
    this.options = {};
    this.v3Utils = {
      log: {
        notice: this.sandbox.stub(),
        warning: this.sandbox.stub(),
      },
    };

    this.createPlugin = ({ withV3Utils } = {}) => {
      if (withV3Utils) {
        return new this.ServerlessPlugin(
          this.serverless,
          this.options,
          this.v3Utils,
        );
      } else {
        return new this.ServerlessPlugin(this.serverless, this.options);
      }
    };
  });

  afterEach(function () {
    this.sandbox.verifyAndRestore();
  });

  describe('constructor()', function () {
    it('does not err out on minimal configuration', function () {
      should.exist(this.createPlugin());
    });

    it('loads environment variables as expected', function () {
      const env = 'unittests';

      const getEnvironment = this.sandbox.stub(
        this.ServerlessPlugin.prototype,
        'getEnvironment',
      );
      getEnvironment.withArgs(this.options).returns(env);

      const loadEnv = this.sandbox.stub(
        this.ServerlessPlugin.prototype,
        'loadEnv',
      );

      this.createPlugin();

      loadEnv.should.have.been.calledWith(env);
    });
  });

  describe('log()', function () {
    [true, false].forEach((withV3Utils) => {
      describe(JSON.stringify({ withV3Utils }), function () {
        it('logs by default', function () {
          const msg = 'msg';
          this.createPlugin({ withV3Utils }).log(msg);
          if (withV3Utils) {
            this.serverless.cli.log.should.not.be.called;
            this.v3Utils.log.notice.should.be.calledWith(msg);
          } else {
            this.serverless.cli.log.should.be.calledWith(msg);
            this.v3Utils.log.notice.should.not.be.called;
          }
        });

        it('logs when log level is set to WARNING', function () {
          const msg = 'WARNING: msg';
          this.createPlugin({ withV3Utils }).log(msg, logLevels.WARNING);

          if (withV3Utils) {
            this.serverless.cli.log.should.not.be.called;
            this.v3Utils.log.warning.should.be.calledWith(msg);
          } else {
            this.serverless.cli.log.should.be.calledWith(msg);
            this.v3Utils.log.notice.should.not.be.called;
          }
        });

        it('does nothing if logging is disabled', function () {
          this.serverless.service.custom = {
            dotenv: {
              logging: false,
            },
          };

          this.createPlugin({ withV3Utils }).log('msg');
          this.serverless.cli.log.should.not.be.called;
          this.v3Utils.log.notice.should.not.be.called;
        });
      });
    });
  });

  describe('getEnvironment()', function () {
    it("set to 'development' when no other options are available", function () {
      this.createPlugin().getEnvironment({}).should.equal('development');
    });

    it('uses option.stage if it is set', function () {
      this.createPlugin()
        .getEnvironment({ stage: 'teststage' })
        .should.equal('teststage');
    });

    it('prefers option.env if it is set', function () {
      this.createPlugin()
        .getEnvironment({ env: 'testenv', stage: 'teststage' })
        .should.equal('testenv');
    });

    it('prefers NODE_ENV if it is set', function () {
      this.sandbox.stub(process, 'env').value({ NODE_ENV: 'TEST_NODE_ENV' });
      this.createPlugin()
        .getEnvironment({ env: 'theenv', stage: 'thestage' })
        .should.equal('TEST_NODE_ENV');
    });
  });

  describe('resolveEnvFileNames()', function () {
    describe('with config.path configured', function () {
      it('returns singleton array if set to a string value', function () {
        const path = '.env.unittest';
        this.serverless.service.custom = {
          dotenv: { path },
        };

        this.createPlugin()
          .resolveEnvFileNames('env')
          .should.deep.equal([path]);
      });

      it('returns config.path as-is if set to an array value', function () {
        const path = ['.env.unittest0', '.env.unittest1'];
        this.serverless.service.custom = {
          dotenv: { path },
        };

        this.createPlugin().resolveEnvFileNames('env').should.deep.equal(path);
      });

      [true, false].forEach((withV3Utils) => {
        describe(JSON.stringify({ withV3Utils }), function () {
          it('logs an error if basePath is also set', function () {
            const path = '.env.unittest';
            this.serverless.service.custom = {
              dotenv: {
                basePath: 'base/path/',
                path,
              },
            };

            this.createPlugin({ withV3Utils })
              .resolveEnvFileNames('env')
              .should.deep.equal([path]);

            if (withV3Utils) {
              this.v3Utils.log.warning.should.have.been.calledWith(
                sinon.match(/basePath/),
              );
            } else {
              this.serverless.cli.log.should.have.been.calledWith(
                sinon.match(/basePath/),
              );
            }
          });
        });
      });
    });

    describe('with default dotenv paths', function () {
      ['staging', 'production', 'dmz'].forEach((env) => {
        it(`returns all path with any "env" other than "test" (${env})`, function () {
          const expectedDotenvFiles = [
            `.env.${env}.local`,
            `.env.${env}`,
            '.env.local',
            '.env',
          ];

          expectedDotenvFiles.forEach((file) =>
            this.requireStubs.fs.existsSync.withArgs(file).returns(true),
          );

          this.createPlugin()
            .resolveEnvFileNames(env)
            .should.deep.equal(expectedDotenvFiles);
        });

        it('filters out files that do not exist', function () {
          const missingDotEnvFiles = [`.env.${env}`, '.env.local'];

          const expectedDotenvFiles = [`.env.${env}.local`, '.env'];

          missingDotEnvFiles.forEach((file) =>
            this.requireStubs.fs.existsSync.withArgs(file).returns(false),
          );

          expectedDotenvFiles.forEach((file) =>
            this.requireStubs.fs.existsSync.withArgs(file).returns(true),
          );

          this.createPlugin()
            .resolveEnvFileNames(env)
            .should.deep.equal(expectedDotenvFiles);
        });
      });

      it('excludes local env file if "env" is set to "test"', function () {
        const env = 'test';
        const expectedDotenvFiles = [
          `.env.${env}.local`,
          `.env.${env}`,
          '.env',
        ];

        expectedDotenvFiles.forEach((file) =>
          this.requireStubs.fs.existsSync.withArgs(file).returns(true),
        );

        this.createPlugin()
          .resolveEnvFileNames(env)
          .should.deep.equal(expectedDotenvFiles);
      });

      it('uses "basePath" config if set', function () {
        const basePath = 'unittest/';
        this.serverless.service.custom = {
          dotenv: { basePath },
        };

        const env = 'unittest';
        const expectedDotenvFiles = [
          `${basePath}.env.${env}.local`,
          `${basePath}.env.${env}`,
          `${basePath}.env.local`,
          `${basePath}.env`,
        ];

        expectedDotenvFiles.forEach((file) =>
          this.requireStubs.fs.existsSync.withArgs(file).returns(true),
        );

        this.createPlugin()
          .resolveEnvFileNames(env)
          .should.deep.equal(expectedDotenvFiles);
      });
    });
  });

  describe('loadEnv()', function () {
    beforeEach(function () {
      this.env = 'unittests';

      this.setupResolveEnvFileNames = () => {
        const getEnvironment = this.sandbox.stub(
          this.ServerlessPlugin.prototype,
          'getEnvironment',
        );
        getEnvironment.withArgs(this.options).returns(this.env);

        const resolveEnvFileNames = this.sandbox.stub(
          this.ServerlessPlugin.prototype,
          'resolveEnvFileNames',
        );

        return resolveEnvFileNames;
      };
    });

    it('throws an error if resolveEnvFileNames() throws an error', function () {
      const error = new Error('Error in resolveEnvFileNames()');
      const resolveEnvFileNames = this.setupResolveEnvFileNames();
      resolveEnvFileNames.withArgs(this.env).throws(error);

      should.Throw(() => this.createPlugin(), error);
    });
    [true, false].forEach((v4BreakingChanges) => {
      describe(`${JSON.stringify({ v4BreakingChanges })}`, function () {
        const action = v4BreakingChanges ? 'throws' : 'logs';

        beforeEach(function () {
          this.serverless.service.custom = {
            dotenv: { v4BreakingChanges },
          };
        });

        it(`${action} an error if dotenv.config() throws an error`, function () {
          const fileName = '.env';

          const resolveEnvFileNames = this.setupResolveEnvFileNames();
          resolveEnvFileNames.withArgs(this.env).returns([fileName]);

          const error = new Error('Error while calling dotenv.config()');
          this.requireStubs.dotenv.config
            .withArgs({ path: fileName })
            .throws(error);

          if (v4BreakingChanges) {
            should.Throw(() => this.createPlugin(), error);
          } else {
            this.createPlugin();

            this.requireStubs.chalk.red.should.have.been.calledWith(
              '  ' + error.message,
            );
          }
        });

        it(`${action} an error if dotenvExpand() throws an error`, function () {
          const fileName = '.env';

          const resolveEnvFileNames = this.setupResolveEnvFileNames();
          resolveEnvFileNames.withArgs(this.env).returns([fileName]);

          const dotenvConfigResponse = {};
          this.requireStubs.dotenv.config
            .withArgs({ path: fileName })
            .returns(dotenvConfigResponse);

          const error = new Error('Error while calling dotenvExpand()');
          this.requireStubs['dotenv-expand'].expand
            .withArgs(dotenvConfigResponse)
            .throws(error);

          if (v4BreakingChanges) {
            should.Throw(() => this.createPlugin(), error);
          } else {
            this.createPlugin();

            this.requireStubs.chalk.red.should.have.been.calledWith(
              '  ' + error.message,
            );
          }
        });
      });
    });

    [true, false].forEach((withV3Utils) => {
      describe(JSON.stringify({ withV3Utils }), function () {
        it('logs an error if no .env files are required and none are found', function () {
          const resolveEnvFileNames = this.setupResolveEnvFileNames();
          resolveEnvFileNames.withArgs(this.env).returns([]);

          this.createPlugin({ withV3Utils });

          const expectedMsg = 'DOTENV: Could not find .env file.';
          if (withV3Utils) {
            this.v3Utils.log.notice.should.have.been.calledWith(expectedMsg);
          } else {
            this.serverless.cli.log.should.have.been.calledWith(expectedMsg);
          }
        });
      });
    });
    it('logs an error with V3Utils if no .env files are required and none are found', function () {
      const resolveEnvFileNames = this.setupResolveEnvFileNames();
      resolveEnvFileNames.withArgs(this.env).returns([]);

      this.createPlugin({ withV3Utils: true });
      this.v3Utils.log.notice.should.have.been.calledWith(
        'DOTENV: Could not find .env file.',
      );
    });

    it('throws an error if no .env files are found but at least one is required', function () {
      const resolveEnvFileNames = this.setupResolveEnvFileNames();
      resolveEnvFileNames.withArgs(this.env).returns([]);

      this.serverless.service.custom = {
        dotenv: {
          required: {
            file: true,
          },
        },
      };

      should.Throw(() => this.createPlugin());
    });

    it('throws an error if a missing env is not set', function () {
      const filesAndEnvVars = {
        file1: {
          ENV1: 'env1value',
          ENV2: 'env2overwrittenvalue',
        },
        file2: {
          ENV2: 'env2value',
          ENV3: 'env3value',
        },
      };

      const files = Object.keys(filesAndEnvVars);

      const resolveEnvFileNames = this.setupResolveEnvFileNames();
      resolveEnvFileNames.withArgs(this.env).returns(files);

      files.forEach((fileName) => {
        this.requireStubs.dotenv.config
          .withArgs({ path: fileName })
          .returns({ parsed: filesAndEnvVars[fileName] });

        this.requireStubs['dotenv-expand'].expand
          .withArgs({ parsed: filesAndEnvVars[fileName] })
          .returns({ parsed: filesAndEnvVars[fileName] });
      });

      this.serverless.service.custom = {
        dotenv: {
          required: {
            env: ['NOT_IN_ANY_FILE', 'NOT_IN_ANY_FILE2'],
          },
        },
      };

      should.Throw(() => this.createPlugin());
    });

    it('loads variables from all files when config.include is "*"', function () {
      const filesAndEnvVars = {
        file1: {
          env1: 'env1value',
          env2: 'env2overwrittenvalue',
        },
        file2: {
          env2: 'env2value',
          env3: 'env3value',
        },
      };

      this.serverless.service.custom = {
        dotenv: {
          include: '*',
          required: {
            // TODO: testing that `required.env` works as expected should be its own test
            env: ['env3', 'TEST_SLS_DOTENV_PLUGIN_ENV1'],
          },
        },
      };

      const files = Object.keys(filesAndEnvVars);

      const resolveEnvFileNames = this.setupResolveEnvFileNames();
      resolveEnvFileNames.withArgs(this.env).returns(files);

      files.forEach((fileName) => {
        this.requireStubs.dotenv.config
          .withArgs({ path: fileName })
          .returns({ parsed: filesAndEnvVars[fileName] });

        this.requireStubs['dotenv-expand'].expand
          .withArgs({ parsed: filesAndEnvVars[fileName] })
          .returns({ parsed: filesAndEnvVars[fileName] });
      });

      this.createPlugin();

      const expectedEnvVars = Object.values(filesAndEnvVars).reduce(
        (acc, envVars) => Object.assign(acc, envVars),
        {},
      );

      this.serverless.service.provider.environment.should.deep.equal(
        expectedEnvVars,
      );
    });

    it('removes all keys if config.include is set to "[]"', function () {
      const fileName = '.env';
      const envVars = {
        env1: 'env1value',
        env2: 'env2value',
        env3: 'env3value',
      };

      this.serverless.service.custom = {
        dotenv: {
          include: [],
        },
      };

      const resolveEnvFileNames = this.setupResolveEnvFileNames();
      resolveEnvFileNames.withArgs(this.env).returns([fileName]);

      this.requireStubs.dotenv.config
        .withArgs({ path: fileName })
        .returns({ parsed: envVars });

      this.requireStubs['dotenv-expand'].expand
        .withArgs({ parsed: envVars })
        .returns({ parsed: envVars });

      this.createPlugin();

      this.serverless.service.provider.environment.should.deep.equal({});

      this.serverless.cli.log.should.have.not.been.calledWith(
        sinon.match(/exclude/),
      );
    });

    it('removes keys not in config.include', function () {
      const fileName = '.env';
      const envVars = {
        env1: 'env1value',
        env2: 'env2value',
        env3: 'env3value',
      };

      this.serverless.service.custom = {
        dotenv: {
          include: ['env2'],
        },
      };

      const resolveEnvFileNames = this.setupResolveEnvFileNames();
      resolveEnvFileNames.withArgs(this.env).returns([fileName]);

      this.requireStubs.dotenv.config
        .withArgs({ path: fileName })
        .returns({ parsed: envVars });

      this.requireStubs['dotenv-expand'].expand
        .withArgs({ parsed: envVars })
        .returns({ parsed: envVars });

      this.createPlugin();

      this.serverless.service.provider.environment.should.deep.equal({
        env2: envVars.env2,
      });

      this.serverless.cli.log.should.have.not.been.calledWith(
        sinon.match(/exclude/),
      );
    });

    it('removes keys in config.exclude', function () {
      const fileName = '.env';
      const envVars = {
        env1: 'env1value',
        env2: 'env2value',
        env3: 'env3value',
      };
      this.serverless.service.custom = {
        dotenv: {
          exclude: ['env2'],
        },
      };

      const resolveEnvFileNames = this.setupResolveEnvFileNames();
      resolveEnvFileNames.withArgs(this.env).returns([fileName]);

      this.requireStubs.dotenv.config
        .withArgs({ path: fileName })
        .returns({ parsed: envVars });

      this.requireStubs['dotenv-expand'].expand
        .withArgs({ parsed: envVars })
        .returns({ parsed: envVars });

      this.createPlugin();

      this.serverless.service.provider.environment.should.deep.equal({
        env1: envVars.env1,
        env3: envVars.env3,
      });
    });

    it('ignores config.exclude if config.include is set', function () {
      const fileName = '.env';
      const envVars = {
        env1: 'env1value',
        env2: 'env2value',
        env3: 'env3value',
      };

      this.serverless.service.custom = {
        dotenv: {
          include: ['env1', 'env2'],
          exclude: ['env2'],
        },
      };

      const resolveEnvFileNames = this.setupResolveEnvFileNames();
      resolveEnvFileNames.withArgs(this.env).returns([fileName]);

      this.requireStubs.dotenv.config
        .withArgs({ path: fileName })
        .returns({ parsed: envVars });

      this.requireStubs['dotenv-expand'].expand
        .withArgs({ parsed: envVars })
        .returns({ parsed: envVars });

      this.createPlugin();

      this.serverless.service.provider.environment.should.deep.equal({
        env1: envVars.env1,
        env2: envVars.env2,
      });

      this.serverless.cli.log.should.have.been.calledWith(
        sinon.match(/exclude/),
      );
    });

    it('does not use `dotenv-expand` when `variableExpansion` is set to `false`', function () {
      const fileName = '.env';
      const envVars = {
        env1: 'env1value',
        env2: 'env2value',
        env3: 'env3value',
      };

      this.serverless.service.custom = {
        dotenv: {
          variableExpansion: false,
        },
      };

      const resolveEnvFileNames = this.setupResolveEnvFileNames();
      resolveEnvFileNames.withArgs(this.env).returns([fileName]);

      this.requireStubs.dotenv.config
        .withArgs({ path: fileName })
        .returns({ parsed: envVars });

      this.createPlugin();

      this.serverless.service.provider.environment.should.deep.equal({
        env1: envVars.env1,
        env2: envVars.env2,
        env3: envVars.env3,
      });

      this.requireStubs['dotenv-expand'].expand.should.not.have.been.called;
    });

    describe('dotenvParser', function () {
      beforeEach(function () {
        this.serverless.config = {
          servicePath: this.dotenvParser.prefix,
        };

        this.serverless.service.custom = {
          dotenv: {
            dotenvParser: this.dotenvParser.path,
          },
        };
      });

      it('throws if importing custom parser causes an error', function () {
        const error = new Error();
        this.requireStubs[this.dotenvParser.fullPath].throws(error);

        should.Throw(() => this.createPlugin(), error);
      });

      it('throws if custom parser returns undefined', function () {
        should.Throw(() => this.createPlugin());
      });

      it('uses output of custom parser', function () {
        const fileName = '.env';
        const envVars = {
          env1: 'env1value',
          env2: 'env2value',
          env3: 'env3value',
        };

        const resolveEnvFileNames = this.setupResolveEnvFileNames();
        resolveEnvFileNames.withArgs(this.env).returns([fileName]);

        this.requireStubs[this.dotenvParser.fullPath]
          .withArgs({
            dotenv: this.requireStubs.dotenv,
            paths: [fileName],
          })
          .returns(envVars);

        this.createPlugin();

        this.serverless.service.provider.environment.should.deep.equal({
          env1: envVars.env1,
          env2: envVars.env2,
          env3: envVars.env3,
        });
      });
    });

    it('runs with defaults when there are no configs', function () {
      const fileName = '.env';
      const envVars = {
        env1: 'env1value',
        env2: 'env2value',
        env3: 'env3value',
      };

      const resolveEnvFileNames = this.setupResolveEnvFileNames();
      resolveEnvFileNames.withArgs(this.env).returns([fileName]);

      this.requireStubs.dotenv.config
        .withArgs({ path: fileName })
        .returns({ parsed: envVars });

      this.requireStubs['dotenv-expand'].expand
        .withArgs({ parsed: envVars })
        .returns({ parsed: envVars });

      const plugin = this.createPlugin();

      plugin.config.logging.should.be.true;
      plugin.config.required.should.deep.equal({});
      plugin.config.v4BreakingChanges.should.be.false;
      plugin.config.variableExpansion.should.be.true;

      this.serverless.service.provider.environment.should.deep.equal({
        env1: envVars.env1,
        env2: envVars.env2,
        env3: envVars.env3,
      });
    });
  });
});
Download .txt
gitextract_cnyp1whv/

├── .editorconfig
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── breaking-change.md
│   │   ├── bug_report.md
│   │   └── feature-request.md
│   ├── labeler.yml
│   ├── pull_request_template.md
│   └── workflows/
│       ├── ci.yaml
│       ├── issue_comment-created.yml
│       ├── issues-cron.yml
│       ├── labeler.yml
│       ├── release.yml
│       ├── scheduled-node-dependency-updates.yml
│       └── tag-new-npm-package-releases-change.yml
├── .gitignore
├── .ncurc.yml
├── .prettierrc
├── CHANGELOG.md
├── LICENSE
├── README.md
├── examples/
│   └── simple-express-app/
│       ├── .gitignore
│       ├── README.md
│       ├── index.js
│       ├── package.json
│       └── serverless.yml
├── package.json
├── src/
│   └── index.js
└── test/
    └── index.js
Download .txt
SYMBOL INDEX (11 symbols across 1 files)

FILE: src/index.js
  class ServerlessPlugin (line 18) | class ServerlessPlugin {
    method constructor (line 19) | constructor(serverless, options, v3Utils) {
    method log (line 49) | log(msg, level = logLevels.NOTICE) {
    method getEnvironment (line 79) | getEnvironment(options) {
    method resolveEnvFileNames (line 89) | resolveEnvFileNames(env) {
    method callDotenvParser (line 126) | callDotenvParser(envFileNames) {
    method parseEnvFiles (line 141) | parseEnvFiles(envFileNames) {
    method setProviderEnv (line 155) | setProviderEnv(envVars) {
    method validateEnvFileNames (line 189) | validateEnvFileNames(envFileNames) {
    method validateEnvVars (line 209) | validateEnvVars(envVars) {
    method loadEnv (line 238) | loadEnv(env) {
Condensed preview — 28 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (65K chars).
[
  {
    "path": ".editorconfig",
    "chars": 133,
    "preview": "root = true\n\n[*.*]\nend_of_line = lf\nindent_style = space\nindent_size = 2\ninsert_final_newline = true\ntrim_trailing_white"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 600,
    "preview": "# These are supported funding model platforms\n\ngithub: neverendingqs\npatreon: # Replace with a single Patreon username\no"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/breaking-change.md",
    "chars": 274,
    "preview": "---\nname: Breaking Change\nabout: Breaking Change Definition of Done\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n* [ ] Updat"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 234,
    "preview": "---\nname: Bug report\nabout: Use this if you think there is a bug with the plugin\ntitle: ''\nlabels: bug\nassignees: ''\n\n--"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature-request.md",
    "chars": 666,
    "preview": "---\nname: Feature Request\nabout: A new feature you would like to see\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n*"
  },
  {
    "path": ".github/labeler.yml",
    "chars": 125,
    "preview": "chore:\n  - .*\n  - .github/**/*\n  - LICENSE\n  - package.json\n\ndocumentation:\n  - CHANGELOG.md\n  - examples/**/*\n  - READM"
  },
  {
    "path": ".github/pull_request_template.md",
    "chars": 325,
    "preview": "## Description\n\n<!-- Describe the feature here with examples if possible -->\n\n## Checklist\n\n<!-- Note: not all checklist"
  },
  {
    "path": ".github/workflows/ci.yaml",
    "chars": 1650,
    "preview": "name: CI\n\non:\n  pull_request:\n  push:\n    branches:\n      - master\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    timeou"
  },
  {
    "path": ".github/workflows/issue_comment-created.yml",
    "chars": 1280,
    "preview": "name: Issue Comments (Created).\n\non:\n  issue_comment:\n    types: [created]\n\njobs:\n  remove-label:\n    name: Remove label"
  },
  {
    "path": ".github/workflows/issues-cron.yml",
    "chars": 730,
    "preview": "name: Issues (Cron)\non:\n  schedule:\n    - cron: '0 14 * * *'\n  workflow_dispatch:\n\njobs:\n  stale:\n    runs-on: ubuntu-la"
  },
  {
    "path": ".github/workflows/labeler.yml",
    "chars": 296,
    "preview": "name: Pull Request Labeler\non: pull_request_target\n\njobs:\n  triage:\n    permissions:\n      contents: read\n      pull-req"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 779,
    "preview": "name: Release new version\non:\n  workflow_dispatch:\n    inputs:\n      release-type:\n        description: Type of release "
  },
  {
    "path": ".github/workflows/scheduled-node-dependency-updates.yml",
    "chars": 485,
    "preview": "name: Scheduled Node Dependencies Update (npm)\non:\n  schedule:\n    - cron:  '0 15 1 * *'\n  workflow_dispatch:\njobs:\n  up"
  },
  {
    "path": ".github/workflows/tag-new-npm-package-releases-change.yml",
    "chars": 309,
    "preview": "name: Tag new npm package releases\non:\n  push:\n    branches:\n      - master\n    paths:\n      - package.json\njobs:\n  tag-"
  },
  {
    "path": ".gitignore",
    "chars": 71,
    "preview": ".nyc_output/\n.serverless/\n.vscode/\n\ncoverage/\nnode_modules/\n\nyarn.lock\n"
  },
  {
    "path": ".ncurc.yml",
    "chars": 207,
    "preview": "reject:\n  # `serverless@2` still supports Node 10, so we can't switch to ESM\n  # https://github.com/chalk/chalk/releases"
  },
  {
    "path": ".prettierrc",
    "chars": 68,
    "preview": "{\n  \"semi\": true,\n  \"singleQuote\": true,\n  \"trailingComma\": \"all\"\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 4701,
    "preview": "# Changelog\n\nOnly major and minor version changes are included in this file. Changes not\nincluded in this log but can be"
  },
  {
    "path": "LICENSE",
    "chars": 1069,
    "preview": "MIT License\n\nCopyright (c) 2017 InFront Labs\n\nPermission is hereby granted, free of charge, to any person obtaining a co"
  },
  {
    "path": "README.md",
    "chars": 15380,
    "preview": "# serverless-dotenv-plugin\n\n[![CI](https://github.com/neverendingqs/serverless-dotenv-plugin/workflows/CI/badge.svg)](ht"
  },
  {
    "path": "examples/simple-express-app/.gitignore",
    "chars": 24,
    "preview": "node_modules\n.env*.local"
  },
  {
    "path": "examples/simple-express-app/README.md",
    "chars": 341,
    "preview": "### Example Express App\n\nThis is a quick example setting up serverless-dotenv-plugin with an express app. It includes an"
  },
  {
    "path": "examples/simple-express-app/index.js",
    "chars": 238,
    "preview": "const serverless = require('serverless-http');\nconst express = require('express');\nconst app = express();\n\napp.get('/', "
  },
  {
    "path": "examples/simple-express-app/package.json",
    "chars": 458,
    "preview": "{\n  \"name\": \"my-express-application\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n  "
  },
  {
    "path": "examples/simple-express-app/serverless.yml",
    "chars": 388,
    "preview": "# serverless.yml\n\nservice: my-express-application\n\nplugins:\n  - ../../\n\nprovider:\n  name: aws\n  runtime: nodejs14.x\n\nfun"
  },
  {
    "path": "package.json",
    "chars": 1401,
    "preview": "{\n  \"name\": \"serverless-dotenv-plugin\",\n  \"version\": \"6.0.0\",\n  \"description\": \"Preload environment variables with doten"
  },
  {
    "path": "src/index.js",
    "chars": 6545,
    "preview": "'use strict';\n\nconst dotenv = require('dotenv');\nconst dotenvExpand = require('dotenv-expand');\nconst chalk = require('c"
  },
  {
    "path": "test/index.js",
    "chars": 22684,
    "preview": "process.env.TEST_SLS_DOTENV_PLUGIN_ENV1 = 'env1';\n\nconst chai = require('chai');\nconst path = require('path');\nconst pro"
  }
]

About this extraction

This page contains the full source code of the colynb/serverless-dotenv-plugin GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 28 files (60.0 KB), approximately 15.4k tokens, and a symbol index with 11 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!