Angular load external configuration before AppModule loads

Angular documentation has a great chapter called NgModule FAQs which contains the following section:

What if two modules provide the same service?

...

If NgModule A provides a service for token 'X' and imports an NgModule B that also provides a service for token 'X', then NgModule A's service definition "wins".

In other words, you can override OAuthModuleConfig for your library in AppModule:

main.ts

(async () => {
  const response = await fetch('https://api.myjson.com/bins/lf0ns');
  const config = await response.json();

  environment['allowedUrls'] = config.apiBaseURL;

  platformBrowserDynamic().bootstrapModule(AppModule)
    .catch(err => console.error(err));
})();

app.module.ts

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

import { AppComponent } from './app.component';
import { OAuthModule, OAuthModuleConfig } from 'angular-oauth2-oidc';
import { HttpClientModule } from '@angular/common/http';
import { environment } from '../environments/environment';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    OAuthModule.forRoot(),
  ],
  providers: [
    {
      provide: OAuthModuleConfig,
      useFactory: () => ({
        resourceServer: {
          allowedUrls: [environment['allowedUrls']],
          sendAccessToken: true
        }
      })
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Note that we should also use useFactory instead of useValue so we don't depend on when AppModule is imported.


Another option here. @yurzui answer works, but it requires the use of useFactory which make the code harder to understand.

useFactory is required because Angular @NgModule decorators will be executed as soon as the AppModule is imported in main.ts and so the configuration isn't loaded yet.

So I decided to load the configuration even before that by adding a script in the scripts section of angular.js. Here's how:

src/config/load.js:

// This file is added to the scripts section of 'angular.json' so it can run before Angular bootstrap process.
// It responsability is to load app configuration from JSON files.
(() => {
  const fetchSync = url => {
    // The code below will log the following warning: "[Deprecation] Synchronous XMLHttpRequest on the main thread is deprecated because of its detrimental effects to the end user's experience. For more help, check https://xhr.spec.whatwg.org/.",
    // but since we want the configuration to be set before Angular bootstrap process, we ignore this warning.
    const xhr = new XMLHttpRequest();
    xhr.open('GET', url, false);
    xhr.send(null);
    return JSON.parse(xhr.responseText);
  };

  // We attach the fetched configuration to the 'window' global variable to access it later from Angular.
  window.configuration = {
    ...fetchSync('config/config.base.json'),
    ...fetchSync('config/config.local.json'),
  };
})();

angular.json:

  // ...
  "architect": {
    "build": {
      "builder": "@angular-devkit/build-angular:browser",
      "options": {
        // ...
        "assets": [
          // ...
          "src/config/config.base.json",
          "src/config/config.local.json"
        ],
        "scripts": ["src/config/load.js"],
  // ...

src/config/configuration.ts:

import get from 'lodash/get';

export class Configuration {
  // We get the configuration from the 'window.configuration' property which as been set earlier by 'config/load.js'.
  private static value = (window as any).configuration;

  /**
   * Get configuration value.
   * @param path The path of the configuration value. Use '.' for nested values.
   * @param defaultValue The returned value if the given path doesn't exist.
   * @example
   * const baseUrl = Configuration.get<string>('apis.github.baseUrl');
   */
  static get<T>(path: string, defaultValue?: T): T {
    return get(Configuration.value, path, defaultValue);
  }
}

Then you can use:

OAuthModule.forRoot({
  resourceServer: {
    allowedUrls: Configuration.get('allowedUrls')
    sendAccessToken: true
  }
}),

See this if you have problem with lodash.