Angular Feature Flags: Feature toggle applications by using command line environment variables
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 importAPP_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 thechild_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 variableAPP_ENV
on our bundle and passing itprocess.env.APP_ENV
this works only because we are running theng serve
command as a child process and passing inAPP_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