5

I'm trying to move all configuration values from environment.ts to config.json, so I can use the same build for multiple different environments (development, staging and production).

I have been following the suggestion written in here, VSTS build - replace Angular4 environment variables in Release stage but in my case, I'm using some of the configuration values in another module, namely core.module which I'm setting up Azure MSAL details like Client Id, redirect Uri, etc.

I'm hitting an error saying that the configuration values that I need for setting up Azure MSAL are not yet loaded up.

config.service.ts

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

import { Config } from 'src/app/model/config';

export let CONFIG: Config;

@Injectable()
export class ConfigService {

  constructor(private http: HttpClient) { }

  public load() {
    return new Promise((resolve, reject) => {
      this.http.get('/assets/config/config.json')
        .subscribe((envResponse: any) => {
          const t = new Config();
          CONFIG  = Object.assign(t, envResponse);
          resolve(true);
        });

    });
  }
}

export function configFactoryService(configService: ConfigService): Function {
  return () => configService.load();
}

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { LOCALE_ID, NgModule, APP_INITIALIZER } from '@angular/core';

// Load the required calendar data for the de locale
import '@progress/kendo-angular-intl/locales/en-DK/all';

import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { routingModule } from './app.routing';
import { CoreModule } from './core/core.module';
import { SharedModule } from './shared/shared.module';
import { HomeModule } from './modules/home/home.module';
import { ConfigService, configFactoryService } from './core/services/config.service';
import { HttpClient, HttpClientModule } from '@angular/common/http';

@NgModule({
  declarations: [
    AppComponent,
  ],
  imports: [
    BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }),
    BrowserAnimationsModule,
    HomeModule,
    SharedModule,
    CoreModule,
    routingModule,
    HttpClientModule
  ],
  providers: [
    ConfigService,
    {
      provide: APP_INITIALIZER,
      useFactory: configFactoryService,
      deps: [
        ConfigService,
        HttpClient
      ],
      multi: true
    },
    { provide: LOCALE_ID, useValue: 'en-DK' }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

core.module.ts

import { NgModule } from '@angular/core';
import { NavigationComponent } from './components/navigation/navigation.component';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { RouterModule } from '@angular/router';
import { HttpModule } from '@angular/http';
import { AuthMSALCustomInterceptor } from './interceptors/auth.msal.custom.interceptor';
import { MsalModule } from '@azure/msal-angular';
import { CONFIG as environment } from './services/config.service';
import { CommonModule } from '@angular/common';

@NgModule({
  imports: [
    CommonModule,
    HttpClientModule,
    FormsModule,
    ReactiveFormsModule,
    RouterModule,
    HttpModule,
    MsalModule.forRoot({
      clientID: environment.clientID,
      authority: environment.authority,
      validateAuthority: true,
      redirectUri: environment.redirectUri,
      popUp: false,
      consentScopes: [
        `api://${environment.sApplicationId}/access_as_user`,
        `api://${environment.wApplicationId}/access_as_user`,
        `api://${environment.oApplicationId}/access_as_user`,
      ],
      protectedResourceMap: [
        [
          environment.sUrl,
          [`api://${environment.sApplicationId}/access_as_user`]
        ],
        [
          environment.wUrl,
          [`api://${environment.wApplicationId}/access_as_user`]
        ],
        [
          environment.oUrl,
          [`api://${environment.oApplicationId}/access_as_user`]
        ]
      ]
    })
  ],
  exports: [
    HttpClientModule,
    FormsModule,
    ReactiveFormsModule,
    HttpModule,
    NavigationComponent
  ],
  declarations: [NavigationComponent],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthMSALCustomInterceptor,
      multi: true
    }
  ],
})
export class CoreModule { }

I need to be able to load the configuration values and use them in core.module. Is there a way to load config.json values even before the modules are initialized?

EDIT: A screenshot to show where the error occurs - core.module error

Helmi Khaled
  • 53
  • 2
  • 6
  • 1
    Use angular enviroment files for setting up configuration - https://medium.com/@balramchavan/configure-and-build-angular-application-for-different-environments-7e94a3c0af23 – eduPeeth Aug 20 '19 at 07:31
  • @eduPeeth I'm trying not to use angular environment files (environment.ts, environment.prod.ts, etc.) cause I don't want to run the build process again, and that's why I'm trying to move the config values to a config json file so I can just do JSON variable substitution. – Helmi Khaled Aug 20 '19 at 07:42
  • 1
    Environment.ts is indeed the wrong place to be loading that type of config from. There is a complete answer from Matt Tester at https://stackoverflow.com/questions/43193049/app-settings-the-angular-way/43316544#43316544 . His answer is not the officially accepted one, so you'll need to scroll down. I've implemented this solution and can confirm it works. – Bytech Aug 20 '19 at 07:49
  • Possible duplicate of [App.settings - the Angular way?](https://stackoverflow.com/questions/43193049/app-settings-the-angular-way) – Bytech Aug 20 '19 at 07:51
  • Agree with @Bytech Instead of compile-time we should have runtime configuration for service endpoint URLs – eduPeeth Aug 20 '19 at 08:25

2 Answers2

3

After a good amount of digging including the original blog post in this link, I landed on an implementation in the comments (https://link.medium.com/QleSbGcCb7) that I liked the most, it's relatively simple and easy to follow. Hope this helps. See the link for full implementation, but here's the gist. Note that you'll need a config file and a model for you config.

fetch('./config.json')
  .then((response) => response.json())
  .then((config: ShellModuleConfig) => {
     if (environment.production) {
        enableProdMode();
     }

    platformBrowserDynamic(
      [{ provide: SHELL_RUNTIME_CONFIG, useValue: config }]
    )
    .bootstrapModule(AppModule)
    .catch((err) => console.error(err));
  });
patrickbadley
  • 2,510
  • 2
  • 29
  • 30
0

The question is a little old but a similar issue may happen to others. The problem is that you are injecting HttpClient and as a result Angular needs to resolve all the HTTP_INTERCEPTORS first. This also means that MsalInerceptor is loaded and thus the MsalService is loaded before APP_INITIALIZER and therefore has no config loaded.

What you need to do is get rid off injection of HttpClient. Instead inject HttpBackend (make sure you use HttpBackend not HttpHandler) and create HttpClient locally. See the full example in this GitHub issue: https://github.com/AzureAD/microsoft-authentication-library-for-js/issues/1403

Tomasz Chudzik
  • 1,867
  • 1
  • 14
  • 21