Making angular app CI/CD proof
Tags: angular, javascript, devops, architecture
Published: 2020-04-26 04:47:39

I get a chance to work on making an angular app _CI/CD_ proof. I've been thinking of writing an article on it for quite some time but yeah now finally I got the time.  I'll share my experience here so that if anyone in future looking for a solution they could look at it to get an idea about it.

## Problem
In *Angular* you can only set the environment of applications before creating build but when dealing with CI/CD you sometimes have to set the environment after the build creation. Because the idea is to use one build for all.

> #### Build once, deploy everywhere 

Let's divide the problem and conquer 
_Issue #1: Injecting/Set the environment into the application._
_Issue #2: Retrieve the environment and hold it before running the app._
_Issue #2: Which environment to run the application on._

## Solution
The problem we have here's that using the current environment system we can't set and update the environment after the build has been created because the angular team didn't designed it that way.
Let's make our application to work our way. We will start at the bottom first.
Imagine what your scripts must look like if you want to create a build and set the environment. 
Your `package.json` should have scripts to build an application and to set the environment in the application. So that makes 2 scripts 1 for the build and 1 for setting the environment. For multiple environments, you will need multiple scripts. Your `package.json` should look something like this.

```javascript
{
  "name":"ssr-angular-app",
  "version": "...",
  ...
  "scripts": {
    ....
    "build:ssr": "npm run build:client-and-server-bundles && npm run webpack:server",
    "build:client-and-server-bundles": "ng build --prod --env=prod --aot --vendor-chunk --common-chunk --delete-output-path --buildOptimizer && ng build --prod --env=prod --app 1 --output-hashing=false",
    "webpack:server": "webpack --config webpack.server.config.js --progress --colors",
    "production": "set NODE_ENV=production && node dist/server.js",
    "development": "set NODE_ENV=development && node dist/server.js"
  }
  ...
}
```

`build:ssr` and `build:client-and-server-bundles` are ssr build commands which will make the `production` build every time and scripts like `development` and `production` will insert the environment after the build.
After updating the scripts we will move forward and make our application to behave what we tell it to do not what angular tells it to do.

So we came up with this solution to create and read a `json` file. `json` has to be in the assets because assets don't get minified/uglified and bundler doesn't have any effect on the assets folder so we can play with it as much as we like. In that file we put the information about the `which` environment and using the second script we update the `json`.
Create a `appConfig.json` file inside `src/app/assets/config/` directory with the environment.

```javascript
{
  "env": "local"
}
```

Now as we have a config file we need to read it and find the environment according to that. 
Angular comes with a solution to the problem to wait before the application loads. It allows us to call functions during app initialization. Add the following function in you `app.module.ts`

```javascript
const appInitializerFn = (appConfig: AppConfigService) => {
  return () => {
    return appConfig.loadAppConfig();
  };
};
```

Also, add this in your `providers` array

```javascript
providers: [
  AppConfigService,
  {
    provide: APP_INITIALIZER,
    useFactory: appInitializerFn,
    multi: true,
    deps: [AppConfigService]
  },
]
```

We provide the `APP_INITIALIZER` token in combination with a factory method. The factory function that is called during app initialization must return a function which returns a promise.
Now create a service called `app-config`. Which will fetch the `json` file from assets directory.

```javascript
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { setEnv } from '../../config';

@Injectable()
export class AppConfigService {
    private appConfig;
    private readonly CONFIG_URL = '/assets/config/appConfig.json';
    constructor(private http: HttpClient) { }

    loadAppConfig() {
        return this.http.get(this.CONFIG_URL)
            .toPromise()
            .then(data => {
                this.appConfig = data;
                setEnv(data);
            });
    }
}

```

Now we are all set for a local environment everything will work if we do `npm start` but that's not what we want we want the application to work on build too. Let's work on that too.
To set the environment after build we will use `fs` to update the `appConfig.json`. In the second script, we are `set`ting the environment using `NODE_ENV` which is accessible in `server. (ts|js)`. We will fetch the env from `process.env` and update the `appConfig.json`.
In your `server.ts` add the following code 

```javascript
...
addEnv(process.env.NODE_ENV);
const environment = setEnv(process.env.NODE_ENV);
...
```

Now create `index.ts` and environment files like `local.ts`, `production.ts` inside `app/config` directory it should look something like this.
![Config directory](https://drive.google.com/uc?export=download&id=19MUT3kxXaW14VCVrKUy82JA2orXnf6yd)

In `index.ts` add the following code to set env locally

```javascript
import LocalEnvironment from './local';
import DevEnvironment from './development';
import ProdEnvironment from './production';

const AppConfigFilePath = 'dist/browser/assets/data/appConfig.json';

export let environment = LocalEnvironment;

export function setEnv(appEnv) {
    appEnv = appEnv.trim();
    switch (appEnv) {
        case 'production':
            environment = ProdEnvironment;
            return ProdEnvironment;
        case 'development':
            environment = DevEnvironment;
            return DevEnvironment;
        default:
            environment = LocalEnvironment;
            return LocalEnvironment;
    }
}

export const addEnv = (appEnv = 'development') => {
    const output = {
        env: appEnv.trim(),
    };
    writeFileSync(AppConfigFilePath, JSON.stringify(output));
};
```

In `local.ts` and other environments add your variables.

```javascript
const LocalEnvironment = {
    production: false,
    googleAnalytics: "UA-XXXXXXXXX-1",
    fbId: 'XXXXXXXXXXXXXXXX'
};
export default LocalEnvironment;
```

Create other environment files likewise and Voila! 😃 you're done.

## Fin
Let's recap what we did 
1. We created an `appConfig.json` file in our assets because bundler doesn't have any effect on assets.
2. After that we make our application to wait and load the environment first.
3. After build when using a command to set the environment we update the `appConfig.json`