angularangular-routingangular-libraryangular-route-guardsangular-lazyloading

Angular routing: Conditional route with library import


TL;DR

How can I have a conditional route with conditional import of the corresponding Angular library during build time, depending on a feature flag in the environment file?

Background

I'm having a route configuration that lazy loads an Angular library / module:

{
    path: 'wishlist-management',
    loadChildren: () => import('@mycompany/thelibrary').then(m => m.TheLibrary),
    canActivate: [AuthGuardService],
    canLoad: [AuthGuardService],
}

The requirement is, that it should only load the library, if it is enabled with a feature flag in the environments file. For that, I've implemented an AuthGuardService with the canLoad method:

  public canLoad(
    route: Route,
    segments: UrlSegment[],
  ): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    return this.isFeatureEnabled(this.router.getCurrentNavigation().extractedUrl.toString());
  }

This canLoad method is also provided in above mentioned route configuration.

However, supposing the feature flag is false and therefore canLoad returns false, and that therefore also @mycompany/thelibrary is not installed as npm dependency in the project, I'm getting an error:

Error: Module not found: Error: Can't resolve '@mycompany/thelibrary' in '/home/user/projects/myproject/frontend/app/node_modules/@mycompany/someotherlibrary/fesm2020'

which is as expected, since canLoad will only load the chunk file at runtime, but is already building it during build time.

However, how can I achieve a fully conditional route and import of the corresponding library / module already in build time, only if the corresponding feature flag is true? It should only try to import the library if the feature flag is true.


I have found a blog post solving this problem by passing null in the route configuration and then manipulating the loadChildren dynamically on router event hooks. But this feels hacky and not the way to go for me.


Solution

  • You have to completely remove any reference to your library from code before build. Use fileReplacements configuration in angular.json file. We will replace this file with a dummy based on the build environment.

    1. Create a separate file for loading the route.

      src/environments/thelibrary-routes.ts.

      export const thelibraryRoutes = [{
        path: 'wishlist-management',
        loadChildren: () => import('@mycompany/thelibrary').then(m => m.TheLibrary),
        canActivate: [AuthGuardService],
        canLoad: [AuthGuardService],
      }]
      
    2. Then in your routes load it as

      import { thelibraryRoutes } from 'src/environments/thelibrary-routes.ts'
      // Other imports
      
      const routes = [
        // Other routes
        ...thelibraryRoutes
      ]
      
    3. Then create a second dummy file.

      src/environments/thelibrary-routes.empty.ts

      export const thelibraryRoutes = [];
      
    4. Now this is the cool part. Edit your angular.json file and under build.configurations add a new one for when your module should be excluded (or if it fits your case, do it in the production configuration, if you want to exclude your module in production for example). Under your desired configuration, add a new entry to fileReplacements list like this:

      angular.json:

      // Other JSON
             "configurations": {
               "production": {
                 "fileReplacements": [
                   { 
                     "replace": "src/environments/environment.ts",
                     "with": "src/environments/environment.prod.ts"
                   },
                   {
                     "replace": "src/environments/thelibrary-routes.ts",
                     "with": "src/environments/thelibrary-routes.empty.ts"
                   },
      
                 ],
      // Other JSON
      

      Note that the first entry in fileReplacements is already there. Only add the second one.

    You can use this approach to include/exclude routes depending on your build configuration. Optionally you can set the routes inside the environment.ts file itself if you don't want to create a new file for the routes, in which case you wouldn't need to edit angular.json file.