2

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.

dude
  • 5,678
  • 11
  • 54
  • 81

2 Answers2

1

Lazy loaded modules, as name indicates, are LOADED at runtime, not conditionaly bundled (or not) into the package. You cannot (nor you should) exclude it from the build process by removing it from npm dependencies. (at least not without custom preprocessing)

Antoniossss
  • 31,590
  • 6
  • 57
  • 99
  • *Are also not directly loaded at runtime, only when absolutely needed, OR if the `preloadingstrategy` is set to `PreloadAllModules` – Pieterjan Aug 04 '22 at 17:25
  • @Antoniossss Thanks for your reply. My goal is to only ship that module as part of my JS bundle, if the corresponding route is enabled. This can already be determined during the build, it's not something dynamic - just a static configuration setting in the environment file. Therefore, having it conditionally included in the bundle is exactly what I want. Even if lazy loaded modules don't fit for my approach like you say, I'm still searching for a solution. Do you have an idea? – dude Aug 04 '22 at 19:02
  • I understand that what is your need and there is NO build in mechanism to do it as far as I am aware of. You would have to have 2 separate source codebases and switch between them. In one of them you would include routing to your module and in another you would not. It is doable using architect i think, but I am not sure if this is something you want to go after. – Antoniossss Aug 05 '22 at 06:35
  • What if you would actually build your bundle and just remove chunk with your module? Potentially it could work as every code required to lazy load your module is already bundled into the main, so as long as you will not try to load it up, you should be fine (404 in worst case scenario i think) – Antoniossss Aug 05 '22 at 06:36
1

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.

Abraham Toledo
  • 401
  • 4
  • 8