Angular CLI and OS Environment Variables

Trying to google for help about how to use environment variables with Angular CLI is kind of a nightmare. I found it to be so because the Angular CLI’s answer to this seems to be naive to the fact that one should not store sensitive credentials in a VCS for all devs to see. Now of course, there’s always ng eject, but the Angular CLI is actually really, really nice! With its simplicity and its conventions, it is, in this programmer’s opinion, a welcome respite from the chaos of modern front-end development. So what does one do?

Let me answer that in two parts: firstly by describing the convention, then the problem as I experienced it, along with a the solution that is straight forward, and doesn’t flaunt said conventions that much. But first, the TL;DR:

Overview

  1. Update .angular-cli.json such that environment.dev.ts, is the config for your dev environment.
  2. npm install -g yargs dotenv
  3. Add a .env (not committed to version control) with your sensitive environment variables as per the dotenv README
  4. Add a script (I called it set-env.ts) that to dynamically generate your environment-specific file using environment variables (now available via process.env thanks to dotenv)
  5. Update the scripts in your package.json to run set-env.ts first, then the respective ng commands

Here’s a working example (output of ng new + these changes): https://github.com/natchiketa/angular-cli-envvars

The Convention

The Angular CLI way of exposing global configuration properties on a per-environment basis revolves around environment-specific files, named something likeenvironment.__YOUR_ENVIRONMENT_NAME__.ts. These are basically just TypeScript files that export an object with whatever your environment-specific properties might be, like this:

environment.ts (Angular CLI v1.1.0)

So, to start out, you’ve got this one property, production, which is false by default. Then, as the comments explain, for your production environment you would start out with something like this:

environment.prod.ts (Angular CLI v1.1.0)

These files are, by convention, kept in src/environments/ and declared in .angular-cli.json, in apps[].environmentSource and apps[].environments like so:

"environmentSource": "environments/environment.ts",
"environments": {
"dev": "environments/environment.ts",
"prod": "environments/environment.prod.ts"
}

This is the convention in the Angular CLI, as described in the wiki.

And I’ll now say ‘Convention’ with a capital ‘C’ because it’s ‘convention’ in literally the same sense as in the Rails world, of which the Angular CLI is a spiritual successor, being based on Ember CLI—that is to say it inherits the Rails mantra “Convention Over Configuration”.

A process.env Problem

Of course, I wouldn’t want to put anything like say, an AWS access key/secret in these files, especially for production, if the file is to be committed to a repository shared by many developers.

You might think “Maybe you could just not commit those files? Keep them per environment and stage them in the build process.” In my experience, this tends to be a pain in the ass—especially if you have more environments (a shared, hosted dev, QA, ‘pre-prod’, and/or one or more spun-up test environments for CI, etc, ad infinitum, ad nauseum).

The more painless and, in my opinion, the more elegant solution, is to use environment variables. Essentially, let OSes do this thing at which they have always been quite good.

In this GIF we’re Elliot, and the OS is all like HELP ME HELP YOU HELP ME HELP YOU

But unfortunately, simply trying to run dotenv’s config function doesn’t seem to work with environment.*.ts files—this would certainly be the most pragmatic way to do it. But when I tried to do this, everything would just come up undefined.

So, accepting that the Angular CLI seems to just want these files to be static, and sort of bites it’s thumb at my attempts to do dynamic something-or-other, I figured OK, I’ll just dynamically generate these with a regular-ol’ script, before each ng command, and wrap it all in npm scripts.

Reglah-Ass Script to the Rescue

So, as per the TL;DR overview at the top—you’ll want yargs and dotenv. I’ve already pretty much described what dotenv brings to the table, but to be clear: dotenv essentially allows you do specify environment variables in a file. I know hearing me say this may now be causing you to make a certain face, popularized by a certain Jackie Chan meme, but bear with me:

The file dotenv reads, .env, won’t be committed. It’s just that, when you’re in your local dev environment, it’s kind of a pain to have to actually set environment variables in your OS, especially when you might have your hand in a bazillion different codebases at any given time. This use case, and this use case alone, is why dotenv is included. For all other environments—which will most likely be using a more automated deployment process, and (ideally) have much more guarded shell access, regular old environment variables will be the way to go.

yargs is the one that is used on all environments, purely so that the script we’re now creating—which will run first, and neither knows nor cares about Angular CLI—can know which environment we’re going to ng serve, ng build, etc.

The idea is to be able to do this:

# Dynamically generate a config and write it
# to src/environments/environment.dev.ts
ts-node set-env.ts --environment=dev
# Note: ts-node is the TypeScript Node.js REPL,
# and should already be installed by default
# by the Angular CLI.
# Now that that's generated, use any
# Angular CLI command, for example:
ng serve --environment=dev

And in order to avoid this verbosity, wrap it in npm scripts. For example:

"scripts": {
"ng": "ng",
"config": "ts-node ./scripts/set-env.ts",
"start": "npm run config -- --environment=dev && ng serve --environment=dev",
"build": "npm run config -- --environment=prod && ng build --environment=prod",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},

NOTE: The extra -- after npm run config is so that the argument gets passed to the script, not to npm run.

Oh, right—the script! It goes something like this:

Thoughts?

I would love any suggestions—please create an issue on the repo! This initial implementation isn’t 100% baked just yet. When it is though, I don’t see anything reason it can’t be added to the Angular CLI. Anything anyone can suggest/contribute to make this PR-ready would be most welcome!