/dev/random

Exposing ENV vars in a Vue-cli app

Vue.js applications created via vue-cli make it very hard to expose environment variables at runtime.

This is a hacky solution I found to make it possible.

The problem

By default vue-cli creates a purely browser-based application, completely served as static files. The built application can be exposed as a static website.

This is really good, unless you need to expose ENV variables defined at runtime.

If you use process.env anywhere in your application, it gets resolved at compile time, and the result is hard-coded into the generated js file. Moreover, only variables starting with VUE_APP_ are included. Variables are read from the envirnoment (in Unix-based systems) and from a .env file in the root of the project (on any OS).

In my case, I needed to generate a Docker image with an integrated webserver, but I needed to pass a variable based on where the Docker image is run, and I couldn't recompile the image on every run

Serving the static files

The first step is to create a server.js file in the root of the project. This is the default file served when using yarn start (or npm start). This is the base:

const express = require('express')
const serveStatic = require('serve-static')
const path = require('path')

const app = express()

app.use('/', serveStatic(path.join(__dirname, '/dist')))

const port = process.env.PORT || 5000
app.listen(port)

console.log('Server started on port ' + port)

In this file process.env is the real environment, but it's not really helpful, as everything Vue uses starts from src/main.js, which will become the base for the generated dest/main.js.*.

The hacky solution

First of all we must serve the real process.env content from a dedicated endpoint. Modify server.js:

const express = require('express')
const serveStatic = require('serve-static')
const path = require('path')

const app = express()

app.get('/config.js', (req, res) => {
    res.set('Content-type', 'text/javascript')
    res.send(`window.CONFIG = ` + JSON.stringify(process.env))
})

app.use('/', serveStatic(path.join(__dirname, '/dist')))

const port = process.env.PORT || 5000
app.listen(port)

console.log('Server started on port ' + port)

The only addition is the app.get('/config.js'... part, which will export a CONFIG variable as a global inside the window object.

Here, instead of exporting the whole process.env content, we can filter out just the variables we need exported.

The second step is to add it to public/index.html:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="shortcut icon" href="<%= webpackConfig.output.publicPath %>favicon.ico">
    <title>My site title</title>
    <script src="/config.js"></script>
</head>
<body>
<noscript>
    <strong>We're sorry but this site doesn't work properly without JavaScript enabled. Please enable it to
        continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

Most of the file is the original one created by vue-cli. We just added the <script src="/config.js"></script> part.

Now the browser will start complaining that the file /config.js is missing when running via yarn serve, so let's add a semi-empty file to make it happy. Create a new public/config.js with this content:

window.CONFIG = null

And finally put it to good use in src/main.js:

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

Vue.config.productionTip = false

// Start support for ENV vars
let CONFIG = !!window.CONFIG ? window.CONFIG : process.env
function getConfig(k, dfl) {
    if (CONFIG[k]) {
        return CONFIG[k]
    } else if (CONFIG['VUE_APP_'+k]) {
        return CONFIG['VUE_APP_'+k]
    } else {
        return dfl
    }
}
// End support for ENV vars

new Vue({
    router,
    store,
    render: h => h(App),
}).$mount('#app')

Most of the file is generated by vue-cli. We added the part marked between comments.

The let... line checks if window.CONFIG is defined. If it is, the file has been served by node.js, so it loaded the genrated config.js, and it uses it.

If it's not defined, it means the file has been served by yarn serve, so window.CONFIG is null, and it takes process.env as a fallback.

The getConfig()) function allows to use the same name when served via normal ENV (supporting any name) and via "simulated" ENV (where the variables must start with VUE_APP_).

How to use it in the code

The getConfig function in this form can be used in the main.js, but not in the components.

To make it globally accessible, let's create a plugin. In src/main.js, add this after the getConfig definition and before the new Vue line:

const envPlugin = {
    install(Vue, options) {
        Vue.prototype.$env = {
            get: function (k, dfl) {
                return getConfig(k, dfl)
            }
        }
    }
}

Vue.use(envPlugin)

This will create a global $env object in every component. For example, to use it in a computed property:

<template>
    <h1>Server URL: { { server_url } }</h1>
</template>

<script>
export default {
  name: 'my-component',
  computed: {
    server_url() { return this.$env.get('SERVER') },
  }
}
</script>

Note: there should be no space between the { and }. It is shown here to avoid Vue.js interpolation.

How to use it at runtime

During development we can use a .env file to define our variables, for example:

VUE_APP_REST_SERVER=http://localhost:8088
VUE_APP_TOKEN=abc123def456ghi789

and run yarn serve

or pass them on the command line:

VUE_APP_REST_SERVER=http://localhost:8088 VUE_APP_TOKEN=abc123def456ghi789 yarn serve

To prepare the bundle for production we use:

yarn build

At runtime we then define the variables on the command line (or via Kubernetes or whetever is used):

REST_SERVER=http://localhost:8088 TOKEN=abc123def456ghi789 yarn start

WARNING: All the data you pass in this way is visible from the browser. Do not send sensitive information like passwords or personal email addresses