DEV Community

loading...
Cover image for Making angular app CI/CD proof

Making angular app CI/CD proof

mkamranhamid profile image Kamran Hamid Updated on ・4 min read

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.

{
  "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.

{
  "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

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

Also, add this in your providers array

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.

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 setting 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

...
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

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

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.

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

Discussion (0)

pic
Editor guide