Skip to content

stopsopa/envprocessor

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TypeScript Tests npm version npm version NpmLicense jest coverage dependencies

The problem

Sometimes we would like to make some env vars available in the browser - on the client side.

Obviously we have to be very careful with that, because we don't want to expose any sensitive information.

Anyway, it is often beneficial to just somehow expose some env vars to the browser, so we can use them in our browser code.

After working for few years with different frameworks I've noticed that most of the frameworks have builtin support for that:

  • Create React App (tool deprecated now) link
  • Vite link
  • Next.js link

The problem with all solutions above is that each of them allows you to expose only during build time. So it means you can't expose anything specific to PROD, STAGING or DEV environment because during build these environments are not available/visible yet.

Once build is done exposed variables becomes part of the build itself - so in other words they are hardcoded in the bundle.

Another problem is that each of framwork seems to allow to expose only env vars prefixed with some specific prefix (e.g. VITE_ or REACT_APP_). It would be nice to have more flexibility here and be able to pick exactly what we want to expose.

So let's try to find a solution to that.

The solution

Solution is based on the fact that for each of these framework we can modify the main HTML file and inject our custom (generated) script exposing env vars to the browser like:

// dist/preprocessed.js file

window.process = {
  env: {
    "MY_ENV": "value",
    "ANOTHER_ENV_VAR": "1.99.2"
  }
};

This script have to be loaded before our bundled code assembled by given framework during build time, so like (just example):

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My app...</title>
  </head>
  <body>

    <script src="/dist/preprocessed.js"></script> 
        <!-- just make sure to load that before bundled code -->

    <script src="/dist/bundle.js"></script> 
        <!-- generated by our framework Vite or Next.js-->
    
  </body>
</html>

And in our code we can use library envprocessor which will pick up window.process.env and provide set of method to work with exported env vars in the browser:

import {   
  all,
  get,
  has,
  getDefault,
  getThrow,

  getIntegerThrowInvalid, // equivalent to get
  getIntegerDefault,
  getIntegerThrow,  
} from "envprocessor";

if (has('MY_ENV')) {
  console.log(getThrow('MY_ENV'));
} else {
  console.log('MY_ENV not found');
}

Above is safe to import into our bundled code.

Important thing to understand is that natural point where building/generating dist/preprocessed.js have to happen is just before launching the app in the final environment (e.g. in the container or in the K8S pod).

Normally our pod will be defined with entrypoint in Dockerfile something like:

FROM node:...

... other stuff in Dockefile

ENTRYPOINT ["npm", "start"]

In order to generate dist/preprocessed.js we would need to install npm install envprocessor (not as a devDependencies) and can create shell script in our project like:

# start.sh file

set -e 
# this is actually important to forward errors

set -x 
# we can also add debug mode (optional) 
# that will print every command before executing in the pod log

node node_modules/.bin/envprocessor --mask "^TERM_" dist/preprocessed.js
# add --debug and --verbose flags to command above if you need more details

# then we can just run our app as before
npm run start

Then we can swap our entrypoint in Dockerfile to:

ENTRYPOINT ["sh", "start.sh"]

Install

npm install envprocessor

yarn add envprocessor

Try from CLI

# to see help and version
npx envprocessor

npx envprocessor --mask "^(TERM_|USER|HOME)" --verbose --debug \
    --enrichModule node_modules/envprocessor/enrich.js var/preprocessed.js var/dist/prep.js
# mask above is just an example, probably you will not want to expose USER env var ...
# that will generate two files: var/preprocessed.js var/dist/prep.js
# and will add some extra env vars by executing custom script
# in this case it is node_modules/envprocessor/enrich.js 
# but you can use this one as an template and create your own

# One can name these "added" env vars a "synthetic env vars". 
# Because it can be anything you like but those
# will not exist in original backend environment as real pod env vars

# the same way as you don't expose all env vars from the backend, 
# here you will have env vars which will only exist in the browser

# you might also provide mask using env var injected into the pod during creation if you wish, 
# this way we have to just tell the tool which env var to use to get the mask like:

export MASK="^(TERM_|USER|HOME)"
npx envprocessor --maskEnv "MASK" --verbose --debug var/preprocessed.js

# once you install envprocessor:
npm install envprocessor

# you can use it directly from node_modules

# ESM
node node_modules/.bin/envprocessor
node node_modules/envprocessor/dist/esm/cli.js

# CJS
node node_modules/envprocessor/dist/cjs/cli.cjs

# or register in package.json
{
  "scripts": {
    "preprocessor": "envprocessor --mask '^(TERM_|USER|HOME)' --verbose --debug --enrichModule node_modules/envprocessor/enrich.js var/preprocessed.js var/dist/prep.js"
  }
}

# and then run it (also you can use this style in start.sh above):
npm run preprocessor

# you can also process env vars from .env file using native --env-file or --env-file-if-exists
# try to create .env file
cat <<EEE > .env
ENV_VAR_FROM_DOTENV="from .env file"
EEE
# and then run:
node --env-file=.env node_modules/.bin/envprocessor --mask "^ENV_" --verbose var/from_env.js

Script form - examples

There is also way to create custom script where you can extend behaviour of envprocessor the way you like, see examples directory.

Demo

web example /envprocessor/examples/index.html loading preprocessed.js into the browser context - just inspect it with chrome developer tools

Using from the project

// ESM
import {  
  all, 
  get,
  has,
  getDefault,
  getThrow,

  getIntegerThrowInvalid, // equivalent to get
  getIntegerDefault,
  getIntegerThrow, 
} from "envprocessor";

// CJS
const {   
  all,
  get,
  has,
  getDefault,
  getThrow,

  getIntegerThrowInvalid, // equivalent to get
  getIntegerDefault,
  getIntegerThrow,  
} = require("envprocessor");

console.log(`get('USER') >${get("USER")}`);