Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular load external configuration before AppModule loads

Consider the following scenario (Angular v7):

  1. Load configuration parameters (API server URL and Auth server URL) from an external endpoint (JSON), before the AppModule is loaded
  2. Pass configuration to AppModule (OAuth2 module)
  3. Compile the app with AOT

Point 2 is key here, and looks like this:

@NgModule({
  imports: [
    ...
    OAuthModule.forRoot({
      resourceServer: {
        allowedUrls: [API_SERVER_URL], // <== we need to set the value that we loaded from the external endpoint (JSON) here
        sendAccessToken: true
      }
    }),
    ...
  ],
  declarations: [AppComponent],
  bootstrap: [AppComponent]
})
export class AppModule { }

What I've tried to far:

  • A solution with APP_INITIALIZER. This doesn't work, as the OAuthModule.forRoot() is triggered before the APP_INITIALIZER can download the external configuration JSON.
  • Load the config with an async function in the main.ts into the Angular environment variables, then bootstrap the AppModule. Also doesn't work due to the import { AppModule } from './app/app.module'; statement in main.ts, which causes the AppModule to load and fire OAuthModule.forRoot() before the external config is loaded (this comment confirms this behavior).
  • Load the AppModule dynamically in main.ts, so without the import statement on top. This is the StackBlitz example given in that comment. It works, but 1) breaks lazy loading WARNING in Lazy routes discovery is not enabled. and 2) doesn't work with AOT compiling. It does come very close to what I need.

Curious to hear if someone is aware of another method to get external configuration loaded before the AppModule loads.

StackBlitz for option 3 (Load the AppModule dynamically): https://stackblitz.com/edit/angular-n8hdty

like image 585
Dennis Ameling Avatar asked Jan 31 '19 21:01

Dennis Ameling


3 Answers

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.

like image 154
yurzui Avatar answered Oct 22 '22 19:10

yurzui


In addition to @yurzui's answer, if you try this in AOT (e.g. ng build --prod), you will get

ERROR in Error during template compile of 'AppModule' Function expressions are not supported in decorators in 'AuthModule' 'AuthModule' contains the error at src\app\core\auth.module.ts(29,23) Consider changing the function expression into an exported function.

so we create an exported function for the factory:

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';

export function oAuthConfigFactory() : OAuthModuleConfig {
  return {
    resourceServer: {
      allowedUrls: [environment.servers.apiServer],
      sendAccessToken: true
    }
  }
}

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    OAuthModule.forRoot(),
  ],
  providers: [
    {
      provide: OAuthModuleConfig,
      useFactory: oAuthConfigFactory
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}
like image 22
Dennis Ameling Avatar answered Oct 22 '22 17:10

Dennis Ameling


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.

like image 6
Maxime Gélinas Avatar answered Oct 22 '22 18:10

Maxime Gélinas