Making angular app CI/CD proof
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 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
...
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.
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
- We created an
appConfig.json
file in our assets because bundler doesn't have any effect on assets. - After that we make our application to wait and load the environment first.
- After build when using a command to set the environment we update the
appConfig.json