Angular Feature Flags: Feature toggle applications by using command line environment variables

Angular Feature Flags: Feature toggle applications by using command line environment variables
Photo by Lucas Benjamin / Unsplash

Angular comes with great built-in support for configuring multiple environments. But sometimes we want to simply pass in environment variables through the command line to toggle certain runtime features. This can be a pain using environment.ts files as we need to create a new environment.ts file for every single feature flag.

Wouldn’t it be great to be able to pass in arguments to angular over command line and have access to these arguments at runtime like so?

yarn serve --logErrors --no-runtimeChecks

This can be very useful to feature toggle and custom ci/cd pipeline environments.

There is one simple way we can achieve this. Read on to know more!


Install the tools

Angular uses webpack to bundle the application under the hood. We are going to use this to our advantage and create a custom webpack config to define a global environment object on our application bundle.

To achieve this we would require three tools:

  • ts-node is a handy tool that lets you execute typescript files in NodeJs runtime straight from the command line. We will use this to execute our custom node script written in TypeScript
  • yargs is a CLI arguments parser which we will use to parse the CLI arguments passed into our script as a key-value pair in a simple js object. We will also install @types/yargs to add typescript typings for yargs
  • @angular-builders/custom-webpack to let us extend angular’s webpack config. We will use this to define the environment variables as a global constant available in our application bundle at runtime
npm install -D -E @angular-builders/custom-webpack ts-node yargs @types/yargs

or with yarn

yarn add --dev @angular-builders/custom-webpack ts-node yargs @types/yargs

Define Schema for Environment Variables

I like to keep custom build scripts in thetools/build directory but you can use any folder structure you deem appropriate.

  • Create tools/build/app-env.ts file and define your environment variable schema like so:
declare global {
    /**
     * @description
     * Global commandline environment variables available to
     * devserver build at runtime
     *
     */
    export const APP_ENV: {
      /**
       * @description
       * Flag toggle ngrx runtime checks
       *
       * @usage
       * `$ yarn serve --no-runtimeChecks`
       *
       */
      runtimeChecks: boolean;

      /**
       * @description
       * Log exceptions to third party exception tracker (e.g. Sentry, Log Rocket)
       */
      logErrors: boolean;

      /**
       * @description
       * Url to post exception logs
       * 
       */
      loggerUrl: string;
    };
  }

export const DEFAULT_APP_ENV: typeof APP_ENV = {
    runtimeChecks: true,
    logErrors: false,
    loggerUrl: 'http://localhost:6000'
  };

Defining a strongly typed schema ensures that we have a central place of reference for our CLI argument definition. We can also reap benefits from IntelliSense provided by our code editor by adding JsDoc annotations.

  • import this file in your application’s main.ts file to import APP_ENV constant and type definitions in the project:
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

import '../tools/build/app-env'; // <- Import the app environment schema into our application bundle

if (environment.production) {
  enableProdMode();
}

platformBrowserDynamic().bootstrapModule(AppModule)
  .catch(err => console.error(err));

Child Process Script

We are now going to create a TypeScript script to invoke angular dev-server as a child process.

  • Create tools/build/serve.ts
  • Add the following code to it
import * as yargs from 'yargs';
import { spawn } from 'child_process';
import { DEFAULT_APP_ENV } from './app-env';

const parseNgArgs = () => process.argv.filter((arg, index) => index > 1 && Object.keys(DEFAULT_APP_ENV).every(key => !arg.includes(key)));

const ngArgs = parseNgArgs();

const argv = { ...DEFAULT_APP_ENV, ...yargs.argv};

const childProcess = spawn('yarn', ['start', ...ngArgs], {
  stdio: 'inherit',
  env: { ...process.env, APP_ENV: JSON.stringify(argv) },
  shell: true,
});

Explanation:

  • Here we are filtering out arguments intended for ng-cli by checking they don’t match the schema we’ve defined in APP_ENV and storing it as ngArgs
  • We then parse CLI args and merge with default DEFAULT_APP_ENV
  • We spawn ng serve as a child process using `spawn` method provided by the child_process module which ships with node
  • We configure the spawned child process to inherit it’s parent shell to write stdio
  • We pass the parsed arguments as the environment variable to the child process (this is the real secret of this solution!!!)

Add an npm script to package.json file

//package.json
{   ...
    script: {
    "serve": "ts-node --project tools/tsconfig.tools.json tools/build/serve.ts",
    }
    ...
}

Custom Webpack Config

We’re nearly there!

  • Create webpack.config.ts file in your application’s root directory and add the following code to it. Here we are defining a global variable APP_ENV on our bundle and passing it process.env.APP_ENV this works only because we are running the ng serve command as a child process and passing in APP_ENV from the parent process (serve.ts) down as an environment variable for the child process. (Told you its the secret sauce!)
import { CustomWebpackBrowserSchema, TargetOptions } from '@angular-builders/custom-webpack';
import * as webpack from 'webpack';
 
export default (
  config: webpack.Configuration,
  options: CustomWebpackBrowserSchema,
  targetOptions: TargetOptions
) => {
  config.plugins.push(
    new webpack.DefinePlugin({
      APP_ENV: process.env.APP_ENV,
    })
  );
  return config;
};
  • Then add custom webpack builder to angular.json file
{
    "architect": {
    "build": {
      "builder": "@angular-builders/custom-webpack:browser", // <- Change builder to Custom Webpack
      "options": {
        "customWebpackConfig": {
          "path": "webpack.config.ts"
        },
        //...
      },
      //...
    },
    "serve": {
      "builder": "@angular-builders/custom-webpack:dev-server", // <- Change builder to Custom Webpack
      // ...
    },
      //...
    "test": {
      "builder": "@angular-builders/custom-webpack:karma", // <- Change builder to Custom Webpack
      // ...
    },
    //...
  },
  // ...
}

Time for Action

That’s it! Now we’re ready to test this out.

npm run serve --no-runtimeChecks --logErrors --loggerHost="http://localhost:6000"

or

yarn serve --no-runtimeChecks --logErrors --loggerHost="http://localhost:6000"

Explanation:

  • We can now pass command-line flags to our runtime bundle as shown
  • boolean flags can be set to false by pre-pending them with --no- as shown
  • string type flags must be passed in after an = sign and optionally wrapped in quotes
  • These flags populate the APP_ENV global object which is made available throughout the app using webpack.Define plugin
// app.component.ts
//...
ngOnInit() {
    console.log(APP_ENV)    
}
//...

You can use the APP_ENV global constant anywhere in the Angular application for feature toggling. You can use this for conditional imports as well.

Summary

Feature Flags can be very useful for toggling features at runtime based on command-line flags. They can also be a powerful means to ship new features to a small segment of your user-base to test it out. You can read more about feature flags in this great article by Martin Fowler. We used a custom webpack builder to define a global environment variable and invoked ng serve as a child process.

You can use the same solution for ng build as well to feature toggle prod builds! But that’s your homework 😉

Thank you for reading, hope this helps you as it helped me. Let me know in the comments below :)

You can follow me on https://github.com/nivrith

Happy Engineering!

All code is available in this GitHub repo:
https://github.com/nivrith/angular-commandline-env