Manan Vaghasiya

Setup run-time environment variables for Vite

24 Aug 2023

Why?

Vite already supports environment variables. These environment variables are statically replaced during build time. This works well for simple use cases, but what if we have a complicated setup where some functionality needs to be configured at the time of deployment(long after we build the app for production)?

I recently came across one such use case where I had to figure out a way to provide a configuration variable to an SPA built with Vite deployed via Docker/nginx when the container starts.

How?

Vite can serve plain assets from public directory and everything in this directory is served at / both during the build and run time, Vite does not transform them in any way. This is perfect for us, we can create a JavaScript file that adds environment variables in the global scope, like the following.

// /public/scripts/env.js
(function (window) {
  window.env = window.env || {};

  window['env']['MY_VARIABLE'] = 'my-value';
})(this);

Now in our Vite index.html file, we need to load this script.

<!-- index.html -->
<!doctype html>
<html lang="en">
  <head>
    <!-- other stuff -->

    <!-- we load the script we created earlier in the following line -->
    <script src="/scripts/env.js"></script>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

Now we can access the value of MY_VARIABLE from anywhere in our app by referencing it from the global scope like window.env.MY_VARIABLE.

All we need to do now is remove the hard coding of the variable from env.js and replace it during run time. And here is how we are going to do it.

We rename the env.js file to env.template.js and modify contents like the following.

// /public/scripts/env.template.js
(function (window) {
  window.env = window.env || {};

  window['env']['MY_VARIABLE'] = '${MY_VARIABLE}';
})(this);

We will use this file as a template and create an env.js file after replacing the placeholder with actual values. Remember we have to do this both at development time and run-time.

During development

We can utilise Vite's dev server to intercept any request to our environment file and serve custom response. We can create a custom Vite plugin to do this.

// vite.config.ts

/// <reference types="vitest" />
/// <reference types="vite/client" />
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

import { defineConfig, PluginOption } from 'vite';

const current = fileURLToPath(import.meta.url);
const root = path.dirname(current);

// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
  return {
    plugins: [
      // ...other plugins
      RuntimeEnvPlugin,
    ],
    // ...other config stuff
  };
});

// figure out variables to replace in the template file
function getEnvVariableNamesFromString(str: string): string[] {
  return str
    .split('${')
    .map((s1) => {
      return s1.split('}')[0];
    })
    .slice(1);
}

// a custom vite plugin for our use case
const RuntimeEnvPlugin: PluginOption = {
  name: 'runtime-env-plugin',
  apply: 'serve',
  configureServer(server) {
    return () => {
      server.middlewares.use('/scripts/env.js', (_, res) => {
        const configContent = fs.readFileSync(
          path.resolve(root, './public/scripts/env.template.js'),
          'utf-8',
        );
        let content = configContent;
        getEnvVariableNamesFromString(configContent).forEach((v) => {
          if (process.env?.[v] && process.env?.[v]?.length) {
            content = content.replace('${' + v + '}', process.env[v]);
          } else {
            content = content.replace('${' + v + '}', '');
          }
        });
        res.setHeader('content-type', 'application/javascript');
        res.end(content);
      });
    };
  },
};

Now we can supply some environment variables during development. Just do MY_VARIABLE=my-value npm run dev

During run-time

We can use envsubst program available in Linux-like OSes to replace the placeholder file just before starting our static server at run-time. Here is an example Dockerfile

FROM nginx:1.23-alpine

RUN apk upgrade --no-cache -U \
    && rm /usr/share/nginx/html/*
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY ./dist/ /usr/share/nginx/html

EXPOSE 8081
CMD ["/bin/sh", "-c", "envsubst < /usr/share/nginx/html/scripts/env.template.js > /usr/share/nginx/html/scripts/env.js && exec nginx -g 'daemon off;'"]

Now we can start our container with MY_VARIABLE=my-value docker run ...

CAUTION: Do not load sensitive variables this way. It can be read by anyone who can access our app and can be a security risk. This is meant to load configuration variables only.