How to import module or provide service conditionally? (AOT)

Viewed 1809

My problem is that one of Angular modules, does not work correctly with the Safari browser.

I have following code :

@NgModule({
    bootstrap: [ AppComponent ],
    imports: [
        BrowserAnimationsModule,
        BrowserModule.withServerTransition({
            appId: 'app'
        }),
        AppModule,
        'navigator' in window && 'serviceWorker' in navigator ? ServiceWorkerModule.register('./ngsw-worker.js') : ServiceWorkerModuleMock
    ]
})

As you can see I want to conditionally import module. When I am using JIT compilation, this mechanism works well. Unfortunately, it is not working with AOT - ServiceWorkerModuleMock is imported regardless condition.

As a solution I was trying to play a little bit with ngDoBootstrap method:

@NgModule({
    entryComponents: [ AppComponent ],
    imports: [
        BrowserAnimationsModule,
        BrowserModule.withServerTransition({
            appId: 'app'
        }),
        AppModule,
        ServiceWorkerModule.register('./ngsw-worker.js')
    ]
})
export class BrowserAppModule {
    constructor(private injector: Injector) {}

    public ngDoBootstrap(appRef: ApplicationRef): void {
        // console.log('navigator' in window && 'serviceWorker' in navigator);

        appRef.bootstrap(AppComponent);
    }
}

But so far I don't know how can I change module imported in @NgModule annotation here. Which method of which class should I use?

Maybe it is possible to change an instance of given service in the Injector? That solution would also satisfy me.

2 Answers

Per my research, currently there seems to be no way to overcome AOT compiler limitations which can only use compile time constructs.

Trying to overcome it as suggested by another answer would not work, it just shifts the same static evaluation to another place and either fails compilation or silently compiles to a useless incorrect constant. The proper way is to initialize through providers and angular service worker indeed has fixed this in later versions. But there is no fix for Angular 7.

However, if you can run an additional post-build script, the following extremely dirty but working hack will allow to inject a runtime code into a decorator AOT compiled code:

1) instead of using constructs which fail compilation or are incorrectly evaluated by AOT, use a string with a "macro" to inject a script into AOT compiled decorator code, e.g.

enabled: '#INJECT_CODE("serviceWorker" in navigator)' as any

The compiler will retain this string in the generated JS code.

Note, the string will evaluate to true in the default build / ng serve. You can combine this with additional static logic e.g. environment.production && '#INJECT_CODE(... to negate on debug builds (service worker is useless on ng serve anyway).

2) post-process main.*.js after the build and replace '#INJECT_CODE(XX)' by   XX . If using source maps, keep the expression length the same using spaces to avoid breaking the mapping. To illustrate with Ant:

<replaceregexp match="&quot;#INJECT_CODE(.*?)&quot;"
               flags="gs"
               preserveLastModified="true"
               replace="             \1 "
>
    <fileset dir="${basedir}">
        <include name="main*.js"/>
    </fileset>
</replaceregexp>

The end result will be the actual code instead of the macro string. For more complex logic, to avoid polluting the code and possible corner cases, the injection may be just a window.XX where XX was evaluated and stored before module load, or it can be a function call. In the case of a function call, it should be referenced also by a normal code somewhere, to avoid tree shaking.

I wonder why AOT doesn't provide something like this built-in, or at least doesn't retain a runtime access to the window object.

You'll probably want to use another module or function to wrap this logic. This is not ready solution and requires some work, but it is still good enough to show you the idea:

@NgModule({
    ...
})
export class MyBootstrapModule {
    static getModule() {
        if ('navigator' in window && 'serviceWorker' in navigator) {
            return ServiceWorkerModule.register('./ngsw-worker.js') 
        } else {
            return ServiceWorkerModuleMock;
        }
    }
}

@NgModule({
    bootstrap: [ AppComponent ],
    imports: [
        BrowserAnimationsModule,
        BrowserModule.withServerTransition({
            appId: 'app'
        }),
        AppModule,
        MyBootstrapModule.getModule()
    ]
})

Also, you can take a look at some other examples of more sophisticated module setup, for example ngrx effects module. It is pretty simple (see below part of it). Please pay attention at how they use ngModule. You may also need to redefine and re-import/re-export some stuff from your modules depending on what you exactly need to do.

@NgModule({
  providers: [
    Actions,
    EffectsSubscription,
    {
      provide: APP_BOOTSTRAP_LISTENER,
      multi: true,
      deps: [ Injector, EffectsSubscription ],
      useFactory: runAfterBootstrapEffects
    }
  ]
})
export class EffectsModule {
  static run(type: Type<any>) {
    return {
      ngModule: EffectsModule,
      providers: [
        EffectsSubscription,
        type,
        { provide: effects, useExisting: type, multi: true }
      ]
    };
  }
....
Related