Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Accessing environment variables in client-side code and creating builds for multiple environments #1250

Closed
karloespiritu opened this issue Nov 18, 2016 · 26 comments
Labels

Comments

@karloespiritu
Copy link

I'm having issues accessing environment variables (i.e API_URL) after I deploy and build my app to multiple environments (staging, production, etc).

I'm using the boilerplate as a client application that consumes data from a REST API. My application is deployed in Heroku and I use Heroku Pipelines for my deployment and build process.

What's the best way to access environment variables after I build the boilerplate (npm run build)? I deploy the boilerplate to multiple environments (staging, production, etc.) with different environment variables, and I can't seem to find a preferred of doing this.

Thank you very much.

@samit4me
Copy link
Contributor

I'm not sure I fully understand your requirements, but if you are wanting a GLOBAL variable available in all environments, maybe the webpack DefinePlugin could help!

If you add your own variable to the the webpack.base.babel.js it will be available on all platforms.

In the past I've added the API URL to package.json and then accessed it in the webpack config, for example:

var package = require('<pathto>/package.json');
...
new webpack.DefinePlugin({
  'process.env': {
    NODE_ENV: JSON.stringify(process.env.NODE_ENV),
  },
  API_URL: JSON.stringify(package.apiUrl)
}),
...

If you want to keep the linter happy you can add a globals sections to the eslint config, which is in package.json for this repo:

"eslintConfig": {
  ...
  "globals": {
    "API_URL": false
  }
  ...
},
...

Please let us know if that helps or if I've completely missed your question :)

@karloespiritu
Copy link
Author

Thanks, but it's not exactly what I'm looking for. I've actually tried that approach, but it requires defining the API_URL as a fixed string constant, and not as an environment variable. Let me illustrate what I'm trying to achieve:

  1. In my development environment, when I run npm run build, the build script should read my .env file contains the ff key-value pair:

    # .env
    API_URL=http://localhost:8000
    
  2. When I deploy it to my staging server using git push staging, the npm run build script should use the the environment variable defined in my staging server which contains the ff key-value pair:

    # .env
    API_URL=http://myapi-staging.herokuapp.com
    
  3. When I deploy it to my production server using git push production, the npm run build script should use the the environment variable defined in my production server which contains the ff key-value pair:

    # .env
    API_URL=http://myapi-production.herokuapp.com
    

I've read through the documentation and previously closed issues, but I can't seem to find any similar way of implementing this approach when deploying the boilerplate to multiple server environments.

@karloespiritu karloespiritu changed the title Accessing environment variables in client-side code Accessing environment variables in client-side code and deployment to multiple environments Nov 19, 2016
@karloespiritu karloespiritu changed the title Accessing environment variables in client-side code and deployment to multiple environments Accessing environment variables in client-side code and creating builds for multiple environments Nov 19, 2016
@samit4me
Copy link
Contributor

samit4me commented Nov 19, 2016

Ah okay, thanks for clarifying. I've just done a little experimenting with dotenv-webpack, webpack-dotenv-plugin, env-cmd and I think env-cmd may work!

Steps I followed:

  • npm i -D env-cmd
  • Added a .env file similar to the one you specified in the root dir of the boilerplate
  • In webpack.base.babel.js modified the DefinePlugin to look something like this:
new webpack.DefinePlugin({
  'process.env': {
    NODE_ENV: JSON.stringify(process.env.NODE_ENV),
    API_URL: JSON.stringify(process.env.API_URL),
  },
}),
  • In package.json, find all occurrences where cross-env NODE_ENV=<something> is specified and added env-cmd .env after it but before the actual command. So for example:
# BEFORE
"start": "cross-env NODE_ENV=development node server",

# AFTER
"start": "cross-env NODE_ENV=development env-cmd .env node server",
  • Added a console.log(process.env) to app.js (to TEST that it works)

Then I tested with both npm start and npm run start:production on a Windows box and in the console I see Object {NODE_ENV: "production", API_URL: "http://custom-api.com"}

Give that a try and let us know if it works? Likewise, if you do find a better solution or ways to improve this one, please keep us informed.

@karloespiritu
Copy link
Author

Thanks, this method works on my local machine. My only issue now is that when I deploy this update to Heroku, the env-cmd .env does not work as it cannot find the .env file. I think this is because Heroku does not store the environment variables in a .env file.

@samit4me
Copy link
Contributor

Firstly I do not know anything about Heroku so please take it easy on me if I am wrong.

The .env seems to be for local dev and not production https://devcenter.heroku.com/articles/heroku-local#set-up-your-local-environment-variables.

Heroku does seem to have environment variables tho, which should be available to process.env so it should be a matter of adding a different DefinePlugin for dev and prod builds, but similar to previous examples. https://devcenter.heroku.com/articles/config-vars#example

@karloespiritu
Copy link
Author

Thanks @samit4me, no problem, I understand. Your suggestion was very helpful and I was able to use your suggested implementation to make loading of environment variables work in my staging and production environments in Heroku.

So here's what I did to make it work:

  1. I created a sentenv.js file in the boilerplate root folder file that generates the .env file for different environments:
# setenv.js
const env = process.env.NODE_ENV;
if (env === 'production') {
  console.log(`API_URL=${process.env.API_URL}`);
} else {
  console.log(`API_URL=http://localhost:8000`);
}
  1. I updated the scripts section ofpackage.json and added a new npm task. This generates the .env file that contains the API_URL environment variable.
# package.json

"scripts": {
    "set:env": "node setenv.js > .env",
  1. I updated all npm tasks in package.json containing cross-env NODE_ENV=production env-cmd to:
# Before
"cross-env NODE_ENV=production env-cmd .env webpack --config internals/webpack/webpack.prod.babel.js --color -p

# After
"npm run set:env && cross-env NODE_ENV=production env-cmd .env webpack --config internals/webpack/webpack.prod.babel.js --color -p

This might not be be the best implementation for creating builds for multiple environments in Heroku, but this one will do for now.

I'm pretty sure a lot of users of this boilerplate also use 'Heroku Pipelines', and I hope there's a cleaner way of using npm run build for multiple environments in the near future.

Thank you very much @samit4me for the help 👍

@samit4me
Copy link
Contributor

samit4me commented Nov 22, 2016

That's great @karloespiritu, I'm really glad you got something that works for you. I agree, this certainly is a common use case and maybe something could be added to the docs in the future, but for now I think this issue is a fantastic reference for anyone trying to do something similar. Thank you for sharing your solution, I am certain it will help someone out in the future 👌

@dcarneiro
Copy link

I was looking for a solution for multiple environment with this boilerplate and this looks just right! Sadly I got an error right at the start :'(

So, what I've did was:

$ git clone https://github.com/mxstbr/react-boilerplate.git
$ cd react-boilerplate
$ npm install
$ npm i -D env-cmd
react-boilerplate@3.3.3 /Users/daniel/tmp/react-boilerplate
├─┬ env-cmd@4.0.0
│ └─┬ cross-spawn@5.0.1
│   └─┬ shebang-command@1.2.0
│     └── shebang-regex@1.0.0
└── UNMET PEER DEPENDENCY eslint-plugin-import@2.0.1

any suggestions to solve this dependency issue?

@gihrig
Copy link
Contributor

gihrig commented Dec 30, 2016

@dcarneiro
"eslint-plugin-import": "2.2.0", is already installed (in dev branch). The "UNMET PEER DEPENDENCY" you see is likely not a problem. Does everything work from there?

@dcarneiro
Copy link

@gihrig, indeed it does. I've panicked way to soon. Thanks for the help

@xavierfuentes
Copy link

Hi @samit4me sorry for replying to an old and closed issue, but would you mind to share the content of that .env example file you talk about? Why do you get two different values depending on the environment? do you use different .env files? Or you were creating dynamic .env files as @karloespiritu was suggesting? Sorry, It just seems something sooooo simple but I just don't get it..

@samit4me
Copy link
Contributor

To be honest @xavifuefer I cannot remember but I can only guess that it was something like this API_URL=http://localhost:8000. I was prototyping with 2 separate files but as they do not work on Heroku, it looks like @karloespiritu generated them.

What are you trying to acheive and what is your environment?

@janet
Copy link

janet commented Feb 28, 2017

i've been wrestling with this as well!

i got it to work by adding cross-env NODE_ENV=production before the npm run set:env command in package.json from @karloespiritu 's answer.

#package.json
{...
  "scripts":{
    "set:env": "node setenv.js > .env",
    "build": "cross-env NODE_ENV=production npm run set:env && cross-env NODE_ENV=production env-cmd .env webpack --config internals/webpack/webpack.prod.babel.js --color -p --progress",
    "start": "cross-env NODE_ENV=development env-cmd .env node server",
    "start:prod": "cross-env NODE_ENV=production npm run set:env && cross-env NODE_ENV=production env-cmd .env node server",
  ...
  },
...
}
#.env
API_URL=http://localhost:5000
# heroku env
$ heroku config:set API_URL=http://mycustomapi.herokuapp.com

@rsteckler
Copy link

The above works great, but assumes you're deploying your entire codebase to the production server and running "npm run build" from the prod server.

Has anyone found a way to only deploy the /build folder and package.json to the server and still have environment variables "passed" to the client-side?

@tplummerfni
Copy link

Anyone figured out how to bypass a lint error for undefined variables?

@bradwestfall
Copy link

@tplummerfni In eslint, if you leave a comment such as /* global varName */ at the top of the code, it will consider varName a global and therefore wont apply no-undef just for that file

@ashgansh
Copy link

ashgansh commented Jun 24, 2017

@rsteckler are you talking about some runtime variables?

If you are I have been able to do so by including a script tag inside app/index.html and then copy the file referenced in the script tag after the build is done

@rsteckler
Copy link

@benjaminshafii
That's an awesome idea. I hadn't thought of that. I'm going to implement it.

For those who might have less context:

  1. Include a <script> tag in your index.html to a file that doesn't exist in your repo. For example: /vars.js.
  2. Let your build happen on the build machine.
  3. When you deploy, run npm run start:prod
  4. Modify start:prod to have an additional step: It will call makeVars.sh.
  5. Create a shell script call makeVars.sh that reads from your environment and creates teh vars.js file with the variables you want.

Love it - Thanks, Ben!

@notflip
Copy link

notflip commented Aug 2, 2017

Do all these methods require you to build on the server? Is that good practice?

@zhenyulin
Copy link

I'm currently having problems with setting environment variables for the client side code after the build stage as well. Typically, I'm deploying multiple versions of the app with one image and was trying to avoid building multiple docker images. So the only available option here would be passing the parameters via docker run command -e environment flag. While the client script bundle was served as a static asset, it doesn't seem to pickup environment vars from node starting command. Is there anyone who has solved this before?

@rsteckler
Copy link

@zhenyulin
The answer is two posts above yours.

@zhenyulin
Copy link

@rsteckler I endup passing the env-var to the client side script tag with the initial page rendering on the server, it might be a good idea to use tools like convict to validate the env-vars on server start time; But overall, it exposes an extra browser window global scope variable, not sure if it is a good practice.

@foaly-nr1
Copy link

For future reference, this is now a feature of react-scripts making it available to all React apps created by create-react-app.

Your project can consume variables declared in your environment as if they were declared locally in your JS files. By default you will have NODE_ENV defined for you, and any other environment variables starting with REACT_APP_
https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#adding-custom-environment-variables

@redpanda-bit
Copy link

redpanda-bit commented Nov 13, 2017

I had the same exact issue. Using React + Webpack on a 100% client based application (frontend). I was using dotenv-webpack to manage environment variables. These were working totally fine on development but not on production.

In my case, I made the mistake of copying and pasting the code from the dotenv-webpack library. All I had to do was include systemvars: true

BEFORE:

const Dotenv = require('dotenv-webpack');
 
module.exports = {
  ...
  plugins: [
    new Dotenv({
      path: './.env', // Path to .env file (this is the default) 
      safe: true // load .env.example (defaults to "false" which does not use dotenv-safe) 
    })
  ]
  ...
};

AFTER:

const Dotenv = require('dotenv-webpack');
 
module.exports = {
  ...
  plugins: [
    new Dotenv({
      path: './.env', // Path to .env file (this is the default) 
      systemvars: true
    })
  ]
  ...
};

-Notice that I deleted safe: true. I do not know what this does, feel free to enlighten me.

However, it works like a charm now!

@davidruisinger
Copy link

davidruisinger commented Nov 22, 2017

Since I needed the env variables to be fetched during runtime and the solutions here all seemed to set the env variables during build I came up with the following "hack" in my ´_document.js´:

<html>
    <script
      dangerouslySetInnerHTML={{
        __html: `
          if (window.process && window.process.env && typeof window.process.env  === 'object') {
            window.process.env = Object.assign({}, window.process.env, ${JSON.stringify(
              process.env
            )});
          } else if (window.process && typeof window.process  === 'object') {
            window.process.env = ${JSON.stringify(process.env)}
          } else {
            window.process = {
              env: ${JSON.stringify(process.env)}
            }
          }
        `,
      }}
    />
...

For me window.process.env already exists when logging it with console.log so I'm not sure wether the if-else stuff is needed at all. I just wanted to ensure that it doesn't break if there is no window.process.env or window.process available.

So far this does the job for me.

UPDATE
Since there might be some private-/secret-key stuff in the env variables I'd recommend to only add those varibales you need:

const allEnvVariables = {
  ...process.env,
}
const whitelist = [
  'NODE_ENV',
  'SOME_OTHER_VAR',
  ...
]
const whiteListedVariables = Object.keys(allEnvVariables)
  .filter(key => whitelist(key))
  .reduce((obj, key) => {
    obj[key] = allEnvVariables[key]
    return obj
  }, {})

And then instead of ${JSON.stringify(process.env)} I use ${JSON.stringify(whiteListedVariables)}

Also I noticed that the JS modules bundeled by Next/Webpack seem to have a local variable called process with an empty object in process.env. So in order to access the passed in env variables I need to access them with window.process.env instead of just process.env.

@lock
Copy link

lock bot commented May 29, 2018

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@lock lock bot locked as resolved and limited conversation to collaborators May 29, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests