How to access environment variables in client side code during build in Meteor?

Viewed 547

Related:

I'm also aware of METEOR_SETTINGS.

I have a Meteor app that will be deployed for different clients with different theming/features. I want to use an env variable such as process.env.CLIENT and have code like if(process.env.CLIENT === 'foo'). During build the env variable should be replaced and dead code elimination will remove/strip the if-block. This already works with NODE_ENV.

I don't want to ship code that a certain client does not need and I also don't want to implicitly expose the list of other clients. Both these things would happen if I check Meteor.settings at runtime. I need this to happen at compile time. Each client gets a different build.

How do I get environment variables through to the client? How are others solving this? process.env on the client currently is:

{"NODE_ENV":"development","TEST_METADATA":"{}"}

In other build tools I can either access all env variables on the client or those with a special prefix (for security reasons) like FOO_.

3 Answers

Creating multiple, distinct builds is not something Meteor support natively. I think it's also fairly unusual.

However, since it is separate processes, you can just build support for it yourself using a bash script similar to this:

#!/bin/bash

FOO_VALUE=$1
echo "Using variable value $FOO_VALUE";

for file in $(find imports/ui/); do
  echo "Replacing special variables in $file"
  sed -i "s/FOO_CLIENT/$FOO_VALUE/g" $file;
done

DIR="build_$FOO_VALUE/"
mkdir -p $DIR
meteor build -d $DIR

As explained in @ChristianFritz's answer, your use case does not seem to be directly supported by Meteor build system.

As you found out, a current solution could be to rely on Meteor Settings, but you would still ship code for all cases (clients/customers) in your bundle.

If I were to use a workaround, I would gather all customer-specific code in separate folders, and swap with a symlink. That way, the rest of your application would import those bits with a constant path. Obviously, make sure to update the symlink target before every customer-specific build.

But such use case would also imply a customer-specific server bundle. This might be fine for you, if you plan to have 1 server per customer, as you seem to imply by "Each client gets a different build."

As usual a night of sleep brought the answer: https://babeljs.io/docs/en/babel-plugin-transform-inline-environment-variables/

meteor npm install babel-plugin-transform-inline-environment-variables --save-dev

Add .babelrc

{
  "plugins": [
    ["transform-inline-environment-variables", {
      "include": [
        "CLIENT"
      ]
    }]
  ]
}

Now both of these work as expected

CLIENT=foo meteor run
CLIENT=foo meteor build --directory $OUT

(sprinkle in some m -rf .meteor/local/plugin-cache/ before switching clients in development to properly rebuild)

Original code:

if (process.env.CLIENT === 'foo') {
  console.log('Client is foo');
} else if (process.env.CLIENT === 'bar') {
  console.log('Client is bar');
}

Output during development:

if ("foo" === 'foo') {
  console.log('Client is foo');
} else if ("foo" === 'bar') {
  console.log('Client is bar');
}

Output in production:

console.log("Client is foo")

During development this allows me to easily switch between clients. While working on the same client adding/removing process.env.CLIENT works as expected and with hmr etc.

For deployments I'm using dokku. That means once I've configured the environments (dokku config:set app CLIENT=foo) I can do:

git push client_foo main
git push client_bar main

to deploy both clients from the same branch without worrying that things get messed up.

Related