feat(env loader): add basic support for autoloading env variables by pgrzesik ยท Pull Request #8413 ยท serverless/serverless

@pgrzesik

Add support for autoloading env variables from .env and .env.${stage} files.

The support is pretty basic and is meant to be similar to support implemented in serverless/components - https://github.com/serverless/components/blob/master/src/cli/index.js#L64-L76

There are still a few small questions that I think are better to discuss directly on a PR:

  1. Where should the documentation of that feature live? I know that there's an open issue to change the current documentation structure to have a provider-agnostic section(s), but at the moment I don't see a good place to document that feature. Should it live as a top-level piece of documentation, similar to e.g. configuration-validation.md?
  2. How should the path for .env files be resolved? Should it take into account servicePath if one is set/provider?
  3. By following the similar approach as serverless/components, only one .env file can be loaded - would it make sense to load both .env and .env.stage if there're present in a way that .env.stage will be loaded later to override overlapping variables from .env?

Closes: #7907

medikoo

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @pgrzesik that looks really good! Please see my comments

Comment on lines +30 to +35

if (doesStageEnvFileExists) {
dotenv.config({ path: stageEnvFilePath });
} else if (doesDefaultEnvFileExists) {
dotenv.config({ path: defaultEnvFilePath });
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't tested but I assume that dotenv.config() is safe to call when env file at path doesn't exist.

If it's the case I would just run it unconditionally

Technically we need a file exists check only to eventually display deprecation message so with v3 it'll be nice to remove this checks.

I take it can be constructed as follows

if (useDotEnv) {
  dotenv.config(...)
} else {
  cost hasDotEnvFile = ...
  if (hasDotEnvFile) showDeprecation()
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that approach, however, that suggests that you'd like the behavior to be a bit different than the one in serverless/components - please see my question from the above

By following the similar approach as serverless/components, only one .env file can be loaded - would it make sense to load both .env and .env.stage if there're present in a way that .env.stage will be loaded later to override overlapping variables from .env?

So, do you think that we should load both env files (default one first, then stage) if they exist?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I read correctly from dotenv implementation, they return object with error property if file was not found: motdotla/dotenv@7301ac9/lib/main.js#L108

Do you suggest using that to avoid explicit checks if the file exists? If yes, then, unfortunately, we still have to check for the existence of files in case the useDotenv is not set to true.

This also reminds me, that it'll be good to signal any parsing errors to the users. I think it'll be good to crash then with meaningful error.

That's a good idea, however, I couldn't really come up with any actual possible parsing errors from dotenv.config that are not related to nonexistent file under provided path. I still think it's a good idea to check for any error. How would you like the framework to handle it? Crash or rather emit a warning to the user and continue processing as "normal"?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If yes, then, unfortunately, we still have to check for the existence of files in case the useDotenv is not set to true

Yes, but it's temporary, it's only for deprecation reporting and will be removed with next major

How would you like the framework to handle it? Crash or rather emit a warning to the user and continue processing as "normal"?

I think we should crash. If env setup is broken it's unlikely things will go well

@pgrzesik

Thank you @medikoo for a swift review ๐Ÿ™‡ There are still a few questions that I'm not sure about and I was hoping if you would be able to help make a proper decision:

  1. Where should the documentation of that feature live? I know that there's an open issue to change the current documentation structure to have a provider-agnostic section(s), but at the moment I don't see a good place to document that feature. Should it live as a top-level piece of documentation, similar to e.g. configuration-validation.md?
  2. How should the path for .env files be resolved? Should it take into account servicePath and search for .env files in the same dir as servicePath if one is set/provider?

Thanks in advance ๐Ÿ™‡

@medikoo

@pgrzesik great questions, sorry I forgot to answer them in initial review:

Where should the documentation of that feature live?

Yes, let's put them top-level as environment-variables.md (I'll ensure site is configured to take that)

How should the path for .env files be resolved? Should it take into account servicePath if one is set/provider?

Simply against process.cwd(), `servicePath will in all cases i point it afaik

would it make sense to load both .env and .env.stage if there're present in a way that .env.stage will be loaded later to override overlapping variables from .env

I imagined supporting both (as you describe), but let me confirm with Components authors on why it works differently there.

@codecov-io

@pgrzesik

Hello @medikoo ๐Ÿ‘‹ Did you hear back from serverless/components team? I've made the suggested updates and if we have that clear I think we will be able to wrap up that PR soon ๐Ÿ‘

@medikoo

@pgrzesik

Hello @medikoo, thanks a lot for your response ๐Ÿ™‡ I'll apply the changes to once again use only one env file and push the changes, hopefully later today ๐Ÿ’ฏ

@pgrzesik

Updated, any feedback will be much appreciated ๐Ÿ™‡

medikoo

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @pgrzesik ! It looks very promising. Please see my suggestions and let me know what yo think

layout: Doc
-->

# Preloading environment variables

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's maybe name it as "Resolution of environment variables"

And add another section header as:

## Support for `.env` files

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed ๐Ÿ‘


# Preloading environment variables

The framework automatically loads environment variables from `.env` files with the help of [dotenv](https://www.npmjs.com/package/dotenv).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'll be good to inform upfront that it's an opt-in. So maybe let's put it as:

With useDotenv: true set in your serverless.yml file, framework automatically... (from next major version .env files will be loaded by default and useDotenv setting will be ignored)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed ๐Ÿ‘


The framework automatically loads environment variables from `.env` files with the help of [dotenv](https://www.npmjs.com/package/dotenv).

The framework looks for `.env` and `.env.{stage}` files in current directory and then tries to load them using `dotenv`. If `.env.{stage}` is found, `.env` will not be loaded. If stage is not explicitly defined, it defaults to `dev`.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's maybe change current directory to service directory, it'll be a bit more specific

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed ๐Ÿ‘


The framework looks for `.env` and `.env.{stage}` files in current directory and then tries to load them using `dotenv`. If `.env.{stage}` is found, `.env` will not be loaded. If stage is not explicitly defined, it defaults to `dev`.

**Note**: There are a few differences between above functionality and [serverless-dotenv-plugin](https://github.com/colynb/serverless-dotenv-plugin):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's maybe make it a subsection as:

###  Differences against  `serverless-dotenv-plugin`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed ๐Ÿ‘

const path = require('path');
const _ = require('lodash');

class EnvLoader {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if creating a class is justified in this case. It looks as relatively simple loadEnv function encapsulated into class, which doesn't feel right.

Let's host it as function, same as e.g. eventuallyUpdate.js logic

Still I notice that utils folder also doesn't seem right, to me it can also be placed directly in lib (we anyway need at some point to improve internal organization)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was also not sure but I decided to follow similar approach as with YamlParser class. I currently changed implementation from class-based to function-based, however, it still depends on serverless instance. I was thinking on how to separate it, but it actually requires a few dependencies such as access to serverlessConfigFile, options and logDeprecation. I'm on the fence here and I see a few options:

  • keep the dependency on serverless
  • keep the dependency on specific pieces - instead of passing serverless instance, just pass the needed pieces
  • resolve everything in Serverless and only pass stage, useDotenv to loadEnv function

Which one do you think makes the most sense here? I personally like the option #3, but that moves some of the responsibilities of loadEnv directly to Serverless class, which is not ideal

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was also not sure but I decided to follow similar approach as with YamlParser class

Totally understood, we have a lot of legacy conventions. Still it's good to abort some of them, especially when they resemble noisy not well justified practices.

it still depends on serverless instance

That's true. I think the cleanest approach for now, would be to follow with something as:

InServerless.js (inline in init or in new method):

if (serviceContext && serviceConfig.useDotEnv) {
  const stage = // resolve stage
  loadDotEnv(stage);
} else {
  isDotFileEnvExisting = // fs exists checks
  if (isDotFileEnvExisting) showDeprecation();
}

And in lib/resolveDotEnv.js just load eventually existing config for given stage

Note, that if we do it that way, then we would be able to use lib/resolveDotEnv.js in as is form, in refactor we plan to do here: #8364 (it is mostly about separating CLI and programmatic logic, so Framework core is purely programmatic (there will be then no dotenv resolution in its core), it'll also allow to integrate Components better)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So yes, option 3 as you pointed, seems best :)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But without passing useDotenv (let's call the util when we know we want to resolve env from .env)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for a comprehensive explanation, I wasn't sure if moving some of the env-related logic makes sense in this case, but thanks to your explanation I see the reasoning and I also think that this is the most appropriate solution moving forward ๐Ÿ™‡

Comment on lines +30 to +35

if (doesStageEnvFileExists) {
dotenv.config({ path: stageEnvFilePath });
} else if (doesDefaultEnvFileExists) {
dotenv.config({ path: defaultEnvFilePath });
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

describe('EnvLoader', () => {
describe('with useDotenv', () => {
it('should load matching stage env file if present', async () => {
const result = await runServerless({

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having it as util (which just takes servicePath as an input) we may write simpler test not involving runServerless.

Then we can write temporary files to home dir (it's a temporary folder in test run, not real user home dir), and handle process.env with process-uils/override-env as:

overrideEnv(() => {
  // Load env from .env
  // Confirm on `process.env` it's effective
});

It'll be also helpful to have it detached from Serverless instance, as it's likely we will refactor whole process follow up to this proposal: #8364

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depending on the decision on dependencies of loadEnv func, I will either refactor it to not depend on serverless or keep it in current form

@pgrzesik

Hello @medikoo - I'm really sorry about radio silence - I was not available for pretty much whole last week and I'm only now catching up. I hope to address all the comments at the beginning of the week.

@medikoo

No worries, and great thanks for heads up @pgrzesik !

medikoo

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great thanks @pgrzesik for valuable input I hope my answers helped to clarify my point.

Sorry, if it's too confusing. We plan a bigger refactor improvement to the Framework (so it's more composable and fits along Components better), and that roadmap also influences implementation choices for PR's like this.

@pgrzesik

Thanks a lot @medikoo for additional clarification - that was definitely very valuable to me and helped me understand a bit better the vision for upcoming changes/refactoring of the framework. My initial gut feeling was to encapsulate the whole logic related to loading env (including all needed checks, etc) in separate util to keep Serverless class "cleaner", but I definitely see that it wouldn't make sense here, given the upcoming changes. I'll follow up with a PR, according to your suggestions ๐Ÿ‘

pgrzesik

let restoreEnv;

beforeEach(() => {
const tmpDir = createTmpDir();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a place that I would love to get your feedback @medikoo - I was trying to just setup .env files once in before, however, after first test, the files were no longer accessible, so I switched to beforeEach/afterEach combo that I'm not super happy about due to unnecessary creation of .env files before each test. Do you have any suggestions on how to properly set it up so these files setup in before will be visible to all tests?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pgrzesik As we're preparing it as standalone module (that takes stage), the unit test may simply put needed .env files to process.cwd(), and confirm that after running util expected env vars are seen on process.env

Important notes:

  • process.env is reset of each test file, therefore we don't have to worry about side effects (it's ensured by: restore-env Mocha extension we have loaded
  • process.cwd() is set to home dir, which is set to temporary dir (again ensured by Mocha extensions: mock-cwd and mock-homedir - note that homedir is cleanup up for other test files, so we also do not need to take care of cleaning it.

Additionally if you'd like to have process.env or process.cwd isolated for specific test runs (so there are no side effects for following tests in same test file). check: process-utils/override-env an process-utils/override-cwd

When having it isolated, and tested as above with unit test, not sure if it's worth testing that resolution works in context of Serverless instance. We might have some one sanity test, and such can be constructed as:

  1. Setup a chosen fixture (like here:
    const { servicePath, updateConfig } = await fixtures.setup('function');
    )
  2. Then in returned servicePath setup needed .env files
  3. Invoke runServerless run on fixture (as here:
    const { cfTemplate: originalTemplate } = await runServerless({
    cwd: servicePath,
    cliArgs: ['package'],
    });
    )
  4. Confirm on the outcome

If that doesn't answer your questions, let me know. I know I've provided an indirect answer, but I hope that above information clarifies how to setup test efficiently

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've posted above comment, without looking into test file, which looks very good imo.

I believe you don't need to create tmp dir, you may just put files in process.cwd() (once) as is, and all that you need to reset is process.env (which you already do with process-utils/override-env).

.env files should definitely not be removed between single tests, so you should be able to setup them once in before (they will however be cleaned after whole test file runs)

pgrzesik


const { error: stageEnvResultError } = dotenv.config({ path: stageEnvFilePath });

if (!stageEnvResultError) return;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, as suggested, I changed the approach to try to load files and based on error returned by dotenv.config decide how to proceed further. I'd love to hear your opinion on the chosen approach and I'm open to suggestions on how to improve it further

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks great!

medikoo

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pgrzesik that looks great! I've posted just few clarifications towards proposed tests

let restoreEnv;

beforeEach(() => {
const tmpDir = createTmpDir();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've posted above comment, without looking into test file, which looks very good imo.

I believe you don't need to create tmp dir, you may just put files in process.cwd() (once) as is, and all that you need to reset is process.env (which you already do with process-utils/override-env).

.env files should definitely not be removed between single tests, so you should be able to setup them once in before (they will however be cleaned after whole test file runs)

@pgrzesik

Thanks a lot for very valuable suggestions @medikoo.

Initially, I wanted to setup files in before hook, relying on the mock-cwd addition to mocha, but I was experiencing suspicious errors and failing tests - when I was running full suite, there were no errors, but when I was running only newly added tests, one of them was failing. So I started digging a bit. What I found out is that, most likely, there's a small race condition in the current mocha runner setup, which makes some of the tests and/or before/beforeEach hooks to run without mocked cwd.

loadEnv
  cwd in before: /home/pgrzesik/Resources/opensource/serverless
  cwd in first test: /home/pgrzesik/Resources/opensource/serverless
    โœ“ should load matching stage env file if present
  cwd in second test: /tmp/tmpdirs-serverless/17eb/f19d17
    1) should load from default env file if present and no matching stage file found
  cwd in third test: /tmp/tmpdirs-serverless/17eb/f19d17
    โœ“ should throw ServerlessError if dotenv returns error other than missing file

Here, I added simple logging statements to log process.cwd in before hook as well as in each of the tests. As can be seen, in before hook and first test, the cwd is not changed, which results in creation of the .env files in non-mocked directory. An additional side effect of that are leftover .env files in project dir. When running all tests with npm test, mentioned loadEnv tests didn't have that problem, because they weren't run at the beginning of the whole test suite. The problem surfaces again when all tests are run with mocha-isolated - please see the failed build: https://github.com/serverless/serverless/pull/8413/checks?check_run_id=1437876395

I'm not very familiar with mocha, but it seems like the problem (if that race condition is the actual problem with the setup) could be solved with the use of root hooks or even better, with global fixtures. That, however, would require an upgrade to at least v.8.2.0 of mocha. I would love to hear your thoughts on this one, maybe you already encountered that issue before?

As for the changes relevant to the main topic of this PR I did the following:

  • Changed to setup in before, but I reverted to previous approach due to issues described above
  • Added an "integration" sanity test(s) in lib/Serverless.test.js

@medikoo

@pgrzesik thanks for this insight. Still I'm not able to reproduce the race condition you're describing.

I've created following test file:

'use strict';

console.log('MODULE', process.cwd());

describe('test', () => {
  console.log('DESCRIBE', process.cwd());

  before(() => {
    console.log('BEFORE', process.cwd());
  });

  beforeEach(() => {
    console.log('BEFORE EACH', process.cwd());
  });
  it('test', () => {
    console.log('IT', process.cwd());
  });
});

I've put it in Framework folder into test.js

and then I've simply run npx mocha test.js, and outcome is as follows:

% npx mocha test.js  
MODULE /Users/medikoo/npm-packages/serverless
DESCRIBE /Users/medikoo/npm-packages/serverless


  test
BEFORE /private/var/folders/wx/nx84lt4j59q46pdq879v9j540000gn/T/tmpdirs-serverless/8a5a/68dcfd
BEFORE EACH /private/var/folders/wx/nx84lt4j59q46pdq879v9j540000gn/T/tmpdirs-serverless/8a5a/68dcfd
IT /private/var/folders/wx/nx84lt4j59q46pdq879v9j540000gn/T/tmpdirs-serverless/8a5a/68dcfd
    โœ“ test


  1 passing (5ms)

While in scope of module body cwd is not yet tweaked (and that's known limitation), it's in place in before call.

Are you sure you're relying on Mocha v6 as referenced in package.json ?

Concerning Mocha v8. Upgrade is pending, I've already started work on it (check: https://github.com/serverless/test/pull/59 - it'll be ready if not today, than in next days)

@pgrzesik

Thanks for checking that out @medikoo. I've confirmed on my side once again, and you're right that with npx mocha test.js it works, but when using npx mocha-isolated or running it via npm test -- -g "testname", it resurfaces again, which is very strange. Please see the following examples, I took the test you mentioned and added it to lib/something.test.js so it would be picked up by npm test. I can also confirm that I'm on mocha@6.2.3.

Running with npx mocha lib/something.test.js

โžœ  serverless git:(add-support-for-loading-env-files) โœ— npx mocha lib/something.test.js
MODULE /home/pgrzesik/Resources/opensource/serverless
DESCRIBE /home/pgrzesik/Resources/opensource/serverless


  RaceCondition
BEFORE /tmp/tmpdirs-serverless/85f6/f6ff40
BEFORE EACH /tmp/tmpdirs-serverless/85f6/f6ff40
IT /tmp/tmpdirs-serverless/85f6/f6ff40
    โœ“ test


  1 passing (5ms)

Running with npx mocha-isolated lib/something.test.js

โžœ  serverless git:(add-support-for-loading-env-files) โœ— npx mocha-isolated lib/something.test.js
MODULE /home/pgrzesik/Resources/opensource/serverless
DESCRIBE /home/pgrzesik/Resources/opensource/serverless


  RaceCondition
BEFORE /home/pgrzesik/Resources/opensource/serverless
BEFORE EACH /home/pgrzesik/Resources/opensource/serverless
IT /home/pgrzesik/Resources/opensource/serverless
    โœ“ test


  1 passing (3ms)

Running with npm test -- -g "RaceCondition"

โžœ  serverless git:(add-support-for-loading-env-files) โœ— npm test -- -g "RaceCondition"

> serverless@2.11.1 test /home/pgrzesik/Resources/opensource/serverless
> mocha "!(test|node_modules)/**/*.test.js" "-g" "RaceCondition"

DESCRIBE /home/pgrzesik/Resources/opensource/serverless
MODULE /home/pgrzesik/Resources/opensource/serverless
DESCRIBE /home/pgrzesik/Resources/opensource/serverless


  RaceCondition
BEFORE /home/pgrzesik/Resources/opensource/serverless
BEFORE EACH /home/pgrzesik/Resources/opensource/serverless
IT /home/pgrzesik/Resources/opensource/serverless
    โœ“ test


  1 passing (11ms)

As can be seen above, the test uses non-mocked cwd if ran with npm test and mocha-isolated. I also did another "experiment", where I modified the test to wait for a second in before hook.

'use strict';

const wait = require('timers-ext/promise/sleep');

console.log('MODULE', process.cwd());

describe('RaceCondition', () => {
  console.log('DESCRIBE', process.cwd());

  before(async () => {
    console.log('BEFORE', process.cwd());
    await wait(1000)
    console.log('BEFORE', process.cwd());
  });

  beforeEach(() => {
    console.log('BEFORE EACH', process.cwd());
  });

  it('test', () => {
    console.log('IT', process.cwd());
  });
});

Running it with npm test -- -g "RaceCondition"

โžœ  serverless git:(add-support-for-loading-env-files) โœ— npm test -- -g "RaceCondition"
> serverless@2.11.1 test /home/pgrzesik/Resources/opensource/serverless
> mocha "!(test|node_modules)/**/*.test.js" "-g" "RaceCondition"

DESCRIBE /home/pgrzesik/Resources/opensource/serverless
MODULE /home/pgrzesik/Resources/opensource/serverless
DESCRIBE /home/pgrzesik/Resources/opensource/serverless


  RaceCondition
BEFORE /tmp/tmpdirs-serverless/aa19/738736
BEFORE EACH /tmp/tmpdirs-serverless/aa19/738736
IT /tmp/tmpdirs-serverless/aa19/738736
    โœ“ test


  1 passing (1s)

After waiting for a second, cwd is properly mocked.

Another example of this behavior, not related to my machine surfaced in mocha-isolated tests on CI on the commit where I didn't create a temporary dir manually: https://github.com/serverless/serverless/pull/8413/checks?check_run_id=1437876395
It additionally includes information that lib/loadEnv.test.js didn't clean created temporary files which suggests that the .env files were created in "real" dir and not the mocked one.

Would you be able to verify it on your side as well?

@medikoo

@pgrzesik indeed, I was able to reproduce it locally, great thanks for thorough explanations.

Interestingly it can be reproduced already by node node_modules/.bin/mocha test.js . I've diagnosed and patched the issue here: https://github.com/serverless/test/pull/62. It's already released and I've bump @serverless/test version in package.json, to ensure patch is taken.

Therefore after you update against master issue should be gone.

@pgrzesik

Thanks a lot @medikoo ๐Ÿ™‡ Indeed, your changes resolved the issue. I've pushed version rebased on top of current master and changed the approach to setting up the files in before hook, without creating yet another temporary dir.

medikoo

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pgrzesik that looks great! I've proposed just few minor, mostly style improvements, and we're good to go!

).servicePath;

const defaultFileContent = 'DEFAULT_ENV_VARIABLE=valuefromdefault';
writeFileSync(path.join(servicePath, '.env'), defaultFileContent);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use async function (and rely on fs.promises directly).

(Unfortunately in Framework we have some not great old patterns that dates Node.js v4 and before, we will slowly recover from that, and ideally if new code is free from them)

before(() => {
const stage = 'testing';
const stageFileContent = 'FROM_STAGE=valuefromstage';
writeFileSync(path.join(process.cwd(), `.env.${stage}`), stageFileContent);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here let's rely on fs.promises.writeFile


it('should load matching stage env file if present', async () => {
await loadEnv('testing');
expect(process.env.FROM_DEFAULT).to.be.undefined;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More logical check is to.not.have.property() (theoretically undefined can be assigned as value, and that should be not expected)


There are a few differences between above functionality and [serverless-dotenv-plugin](https://github.com/colynb/serverless-dotenv-plugin):

- the framework only loads environments variables locally and does not pass them to your functions' environment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd probably start each sentence capitalized.

also let's fix functions' into function's

Comment on lines +11 to +12

`Encountered: "${error}" while trying to load environment variables`,
` from filepath: ${filePath}.`,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd probably reword it to:

Failed to load environment variables from "${filePath}": ${error}"
  • Original error message can be verbose, so it's better to put this at the end
  • It's also convention in few other places, to first provide short context info, and then trail it with original error message.

@pgrzesik

@pgrzesik

@pgrzesik

@medikoo thank you, all should be fixed now ๐Ÿ™‡

medikoo

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @pgrzesik ! It's great to have this long waited functionality finally in