Is there any way to externalize completely environment variables (secret keys included) in a React app in a DevOps context?
We want to automate our builds for different environments (each environment has its own environment variables).
We have looked into environment variables in React using Webpack, but it doesn't quite satisfy our need and we can't externalize environment variables with this approach. Besides, we would have to explicitly declare all these variables inside webpack config file, which would be bloated if we did this.
We think that the best way to go is to put api secrets in Firebase and runtime specifics inside a jenkins script.
We are using Jenkins as our CI/CD server and the app runs is deployed with nginx in a Docker container.
You may create two .env files, .env and .env.prod at the root level and can define your particular 'variables' related to specific environment into these env files. Then in your code you should be able to access them through process.env.NODE_ENV
if(process.env.NODE_ENV === "production"){
//Execute this in production environment
var url= `${process.env.REACT_APP_AC_ORIGIN}/`;
url = url + ...,
}else{
//Execute this in dev environment
var url= `${process.env.REACT_APP_AC_ORIGIN}/`;
url = url + ...,
}
.env
REACT_APP_AC_ORIGIN=https://example1.com
.env.prod
REACT_APP_AC_ORIGIN=https://example2.com
If you have multiple environments, say, production, staging, QA, and so on, you may even consider generating dynamic URLS assuming that different environments will be accessed by different hostnames in the browser, Something like this:
api-config.js
let backendHost;
const apiVersion = 'v1';
const hostname = window && window.location && window.location.hostname;
if(hostname === 'realsite.com') {
backendHost = 'https://api.realsite.com';
} else if(hostname === 'staging.realsite.com') {
backendHost = 'https://staging.api.realsite.com';
} else if(/^qa/.test(hostname)) {
backendHost = `https://api.${hostname}`;
} else {
backendHost = process.env.REACT_APP_BACKEND_HOST || 'http://localhost:8080';
}
export const API_ROOT = `${backendHost}/api/${apiVersion}`;
And then you can import it like this:
import { API_ROOT } from './api-config';
function getUsers() {
return fetch(`${API_ROOT}/users`)
.then(res => res.json)
.then(json => json.data.users);
}
OR, you can define your variables at run time on different environments.
$ REACT_APP_API_HOST=example.com yarn run build
# The resulting app would have
# process.env.REACT_APP_API_HOST === "example.com"
# process.env.NODE_ENV === "production"
Ref : https://daveceddia.com/multiple-environments-with-react/
Related
I'm building and trying do deploying a packaged electron app. FOr the packaging i used
electron-packager
electron-installer-debian
electron-installer-dmg
electron-winstaller
and I'm facing a little issue where I have to store tha appa datas somewhere in my user computer.
I saw that the good practice is to use the the folder in the path that is returned by the electron method app.getPath('userData').
from the docs
It is The directory for storing the app's configuration files, which by default it is the appData directory appended with the app name.
%APPDATA% on Windows
$XDG_CONFIG_HOME or ~/.config on Linux
~/Library/Application Support on macOS
By my tests sometimes this folder is not created automatically when the app is installed and other times yes and I'm wondering if i should create it or not.
Right now i'm quitting the app if this folder isn't present in the pc with the following code
var DatasPath = app.getPath('userData')
if (!fs.existsSync(DatasPath)){
process.exit()
}
So the question is
should i create the DatasPath folder with fs.mkdirSync(DatasPath); when it is not present or it is 'bad practice to do so', and if I can create the folder i have to warning the user the i have just added that folder?
(Expanding my reply from a "comment" to an "answer")
i don't know if i'm supposed to create it or not so i automatically
make the app quit if there is not that folder
It seems you are taking "userData" too literally? It is not an actual "folder" named "userData – it is a path to where the operating system stores data for that application. Electron currently runs on 3 operating systems and each one does things differently. For our convenience, Electron hides those differences by creating the wrapper method app.getPath(name) so the same code will work on each OS.
Try this: put the line below in your main.js script:
console.log(app.getPath('userData'));
/Users/*********/Library/Application Support/MyCoolApp
(the "*********" will be your user account name.)
UPDATED:
Run the code below in main.js and then look in the folder specified by the "userData" path
const fs = require("fs");
const path = require('path');
var datasPath = app.getPath('userData')
var data = "I am the cheese"
var filePath = path.join(datasPath, "savedData.txt")
fs.writeFileSync(filePath, data)
At pathConfig.js
function getAppDataPath() {
switch (process.platform) {
case "darwin": {
return path.join(process.env.HOME, "Library", "Application Support", "myApp");
}
case "win32": {
return path.join(process.env.APPDATA, "myApp");
}
case "linux": {
return path.join(process.env.HOME, ".myApp");
}
default: {
console.log("Unsupported platform!");
process.exit(1);
}
}
}
const appPath = __dirname;
const appDataPath =
!process.env.NODE_ENV || process.env.NODE_ENV === "production"
? getAppDataPath() // Live Mode
: path.join(appPath, "AppData"); // Dev Mode
if (!fs.existsSync(appDataPath)) {
// If the AppData dir doesn't exist at expected Path. Then Create
// Maybe the case when the user runs the app first.
fs.mkdirSync(appDataPath);
}
In each operating system the appData folder has a different path and the perfect way of getting this path is by calling app.getPath('userData') in the main process.
But there is a package that can handle this for you, it stores data in a JSON file and update it in every change.
In my opinion this package is much better than handling everything by your self.
Read more :
https://www.npmjs.com/package/electron-data-holder
in server code I have this:
import express from "express";
const server = express();
import path from "path";
// const expressStaticGzip = require("express-static-gzip");
import expressStaticGzip from "express-static-gzip";
import webpack from "webpack";
import webpackHotServerMiddleware from "webpack-hot-server-middleware";
import configDevClient from "../../config/webpack.dev-client";
import configDevServer from "../../config/webpack.dev-server.js";
import configProdClient from "../../config/webpack.prod-client.js";
import configProdServer from "../../config/webpack.prod-server.js";
const isProd = process.env.NODE_ENV === "production";
const isDev = !isProd;
const PORT = process.env.PORT || 8000;
let isBuilt = false;
const done = () => {
!isBuilt &&
server.listen(PORT, () => {
isBuilt = true;
console.log(
`Server listening on http://localhost:${PORT} in ${process.env.NODE_ENV}`
);
});
};
if (isDev) {
const compiler = webpack([configDevClient, configDevServer]);
const clientCompiler = compiler.compilers[0];
const serverCompiler = compiler.compilers[1];
const webpackDevMiddleware = require("webpack-dev-middleware")(
compiler,
configDevClient.devServer
);
const webpackHotMiddlware = require("webpack-hot-middleware")(
clientCompiler,
configDevClient.devServer
);
server.use(webpackDevMiddleware);
server.use(webpackHotMiddlware);
console.log("process.env.NODE_ENV",process.env.NODE_ENV);//RETURNS UNDEFINED
server.use(webpackHotServerMiddleware(compiler));
console.log("Middleware enabled");
done();
} else {
webpack([configProdClient, configProdServer]).run((err, stats) => {
const clientStats = stats.toJson().children[0];
const render = require("../../build/prod-server-bundle.js").default;
server.use(
expressStaticGzip("dist", {
enableBrotli: true
})
);
server.use(render({ clientStats }));
done();
});
}
I client and server config files I have this plugin enabled:
new webpack.DefinePlugin({
"process.env": {
NODE_ENV: JSON.stringify("development"),
WEBPACK: true
}
but this is the output
process.env.NODE_ENV undefined
Server listening on http://localhost:8000 in undefined
in client side it is working BUT express side process.env.NODE_ENV returns undefined
Assuming you using Webpack-Dev-Server, you can use this call syntax witch is proper :
const dev = Boolean( process.env.WEBPACK_DEV_SERVER )
You will no longer need to pass environment type parameters, because I think you pass parameters in your script run in packages.json
I'm writing my experience to help anyone out there with this problem, though I did not actually solve it. I had the same problem with setting the environment variables in my server-side project.
apparently, after you set the environment variables they are completely accessible in the building process, which is in the build tools like webpack config or even .babelrc.js.
but after the build process, the environment variables get overwritten AND there is a time gap between build process and overwriting environment variables.
I used many webpack or babel plugins but neither of them could hold on to environment variables after the build process ON the server-side but they were immediately defined on the client-side.
since I'm using ReactJs, I tried adding REACT_APP_ to the beginning of variables, but still no luck.
some other plugins I used: webpack dotenv, webpack.DefinePlugin, webpack.EnvironmentPlugin, babel-plugin-transform-define, babel-plugin-transform-inline-environment-variables
so it made me use the good old-fashion way of setting environment.js on DEPLOY but not on the BUILD process.
in case someone is not familiar: you have one main environment.js and (in my case) 2 other files, one for staging environment.staging.js and one for production environment.prod.js. On each deploy, you copy the related js to environment.js, the main environment file, and in your code, you always read global CONSTs,baseUrl for APIs and ... from environment.js.
hope it helps someone out there.
Looking for elegant and simple solution to have "local configuration override" files.
The idea is to be able to have local configuration that will not ask to be added to git repository every time.
For that I need to include local.config.js if it exists.
I have global app configuration in config.js with configuration like
export const config = {
API_URL="https://some.host",
}
and config.local.js
export const config = {
API_URL="https://other.address",
}
there's .gitignore:
config.local.js
Difficulty:
I do not want to add a node module to project just for this one thing. I believe there should be an elegant way to do this in one or few lines, but have not found any so far.
Things that I tried:
1.
try {
const {
apiUrl: API_URL,
} = require('./config.local.js');
config. API_URL =apiUrl;
} catch (e) {
}
require does not work inside try{} block.
2.
const requireCustomFile = require.context('./', false, /config.local.js$/);
requireCustomFile.keys().forEach(fileName => {
requireCustomFile(fileName);
});
does not work.
3.
export const config = require('./config.local.js') || {default:'config = {...}'}
does not work.
4.
Using .env and settings environment variable: I need to override whole array of configuration values. Not one by one.
This solution uses process.argv. It is native to node as documented here and does not use .env
It inspects the command values used to start the app. Since these should be different between your local and production environments, it's an easy way to switch with no additional modules required.
command prompt to start your node app:
(this might also be in package.json and incurred via npm start if you're using that approach.)
$ node index.js local
index.js of your node app:
var express = require('express');
var config = require('./config');
if (process.argv[2] === 'local') {
// the 3rd argument provided at startup (2nd index) was 'local', so here we are!
config = require('./config_local');
}
var app = express();
// rest of owl…
In my node app, I want to set production and development in my config.js file.
For that I have all most set all thing but I'm still missing something.
I want to get config data like database credential from config file based on my development mode. If I upload on live then app will use live cred. On other hand if I used local then it should be use local cred.
module.exports = function () {
console.log("Process env is ::: ", process.env.NODE_ENV);
if (process.env.NODE_ENV == 'production') {
return {
db : {
host:'localhost',
batabase:'dbname',
username:'',
password:''
}
}
} else {
return {
db : {
host:'localhost',
batabase:'dbname',
username:'',
password:''
}
}
}
};
I have taken ref from this answer
Just try this way.
module.exports = (function () {
process.env.NODE_ENV='development';
if(process.env.NODE_ENV === 'production'){
// Config data of Live
}else{
//Config data of Local
}
})()
This works for me. :)
process.env refers to the Environment Variables exists at the time you start you nodejs app . (it's part of the os)
When you deploy to cloud , usually it's handled for you already (process.env.NODE_ENV = production) .
Some cloud providers even give you the option to control it via a GUI .
But for local environment , you can use .dotenv package . (https://github.com/motdotla/dotenv)
With this package you create a .env file at the top of your project ,
and just write down NODE_ENV = local/staging/production
Please note that you can always run in shell:
export NODE_ENV=production
(WATCH FOR WHITESPACES !)
before you start you nodejs app which will also give you the effect
of controlling process.env
Using the config files in other files, just by requiring it .
const config = require('path/to/config.js');
then config.data.host will be changed depend on the NODE_ENV
Any ideas on how to implement Mixpanel analytics through segment.io, that can track for all production and non-production environments.
Right now I have created 3 different projects (dev, staging and production) on both mixpanel & segment.io. And traking them. But when I'm changing dev code and pushing to staging and prouction, it overwrites analytics main code.
I am not using ruby....I'm using javascript. Any suggestions? Will a config file that substitutes token work?
Thanks. I did some research. I do have a simpler way of implementing this if someone is not familiar with config files or not having access to those files.
I can have a if condition that I can use when initializing segment i/o.
var apikey;
if (window.location.host === "dev.xyz.com") {
apikey = <api_key>;
} else if (window.location.host === "staging.xyz.com") {
apikey = <api_key>;
} else if (window.location.host === "prod.com") {
apikey = <api_key>;
}
analytics.load(apikey);
Replace with respective api_keys from segment I/o . This works well.
A config file that substitutes tokens is the perfect solution.
You'll want to do something like this in the javascript snippet:
analytics.load("<%= config.segmentio.apiKey %>");
Where config is your dev settings on your dev machine, and staging/prod settings on staging and prod.
I would suggest that as part of your build step, you bake in a configuration variable that identifies the environment which your code is running inside - e.g. ['Dev', 'Staging', 'Production'].
You would then do something similar to what #monical has suggested except without using URL's in the mix:
var token;
switch(environment) {
case 'Staging':
token = 'TOKEN_STAGE';
break;
case 'Production':
token = 'TOKEN_PROD';
break;
default:
token = 'TOKEN_DEV';
}
analytics.load(token);