DEV Community

Angular dynamic modules at runtime with Module Federation

Sean Perkins on May 20, 2021

Angular 12 recently launched with the added enhancements of Webpack 5 and opening the door to using module federation. If you are looking for a gre...
Collapse
 
cholt0425 profile image
cholt0425

So with my project setup like the article describes I got an error when the application loaded about can't mix multi-providers and regular providers. So I ended up adding this to the route module code:

},
          multi: true,
          deps: [ROUTES, PLATFORM_ROUTES],
Enter fullscreen mode Exit fullscreen mode

Specifically I added multi:true but now I get the following error:

Error: NG0200: Circular dependency in DI detected for InjectionToken ROUTES

I just don't even know where to start to debug this. I don't see any circular dependencies visually inspecting the code. However, I'm concerned I had to add multi: true to even get this far and the example does not. I am using Angular 12.0.10.

Thanks for any help you might be able to provide!

Collapse
 
cholt0425 profile image
cholt0425

I found the problem. It does not like the fact that you are providing ROUTES and also importing routes as a dependency in the provider. Changed static routes defined to something else and this corrected the problem.

Collapse
 
jibinp profile image
Jibin Philipose

Thank you for the reply it saved me a lot of time, I will just add on to your answer for more clarity for those in the future who will face this problem.
In the app-routing.module.ts

{
      ngModule: RouterModule,
      providers: [
        {
          provide: ROUTES,
          useFactory: (dynamicRoutes: any = []) => {
            const rootRoutes: Routes = [];
            if (Array.isArray(dynamicRoutes)) {
              rootRoutes = [...rootRoutes, ...dynamicRoutes];
            }
            return rootRoutes;
          },
          deps: [PLATFORM_ROUTES],
          multi: true,
        },
      ],
    },
Enter fullscreen mode Exit fullscreen mode

So we just add multi: true and remove ROUTES from the deps array will stop giving this error.

Collapse
 
axell9641 profile image
axell9641

Most of the samples you can find online assume that you have a monerepo. Do you know of any example that deals with the shell and mfes being in separate repos? What would happen to the shared dependencies? Will I still be able to download them just once?

Collapse
 
seanperkins profile image
Sean Perkins

Excellent question, I am not currently aware of any examples of cross-repo MFE; although in concept they should work the same.

The usage of mono-repos, only simplifies set-up and maintenance; by making use of schematics to generate webpack configs and establish the remote entries.

You should be able to do this all manually, pointing your remote entry to whatever port your other repository/project is on. The handshake for dependencies should also be the same, since it's evaluated at runtime against the generated bundles (independent of anything mono-repo specific).

Collapse
 
jibinp profile image
Jibin Philipose • Edited

Awesome implementation of platform-config.json, read the article it's amazing, I will be implementing this for the dynamic mfe remoteEntry points. Also I had a question about the usage of cdn, if I have extra business logic like making calls to another server in my exposed remote module and if I hosted that particular mfe in a cdn will it work out of the box or will CORS give me trouble? Basically i know i will have to try it and seek the answers just wanted to know your experience using cdns and is there any references which helped you out, it would be great if you can point me towards it😁.

Collapse
 
balb profile image
Alvaro Enrich

Hi, Great post btw!

I am currently working into something similar, but I am facing some trouble. What happens if you want your MFE to have some routing inside? i.e.: your LoginModule has routing to go to a 2FA view or a ForgotMyPassword view? That routing should be inside the MFE, and I'm not able to navigate there correctly from the Shell?

In addition, could you please share a github URL with the source code of this example?

Many thanks!! :)

Collapse
 
uwer70 profile image
UweR70

Nice!
But how to test "loadRemoteModule" using jasmine?
Non of my tries to mock this function are working.

Please see
here (stackoverflow)
and
here (github / module federation)

What do I miss?

Collapse
 
gr4vitonn profile image
gr4vitonn • Edited

Hello Sean,

Great article. Thank you for sharing.

I'm trying to do what you advise against: exposing AppModule and AppComponent.

So what I'm trying to achieve is the following:

  • have a host which lazily loads the remote on specific route
  • have a remote which has its own routes which lazily load modules

With your advice the code structure at remote app level will look like:
app.module.ts
|- feature.module.ts
_
_|- sub-feature1.module.ts
_
_|- sub-feature2.module.ts

By exposing tha app.module.ts and app.component.ts I would only have 1 router-outlet and flat structure at remote app level:
app.module.ts
_ |- feature1.module.ts
_ |- feature2.module.ts

Unfortunately I hit the wall with this approach (exposing the app.module.ts and app.component.ts). When the host loads the remote, the remote's sub-route gets loaded in the host's router-outlet, without running the remote's app.component.ts (nothing runs from the remote's app.component.ts and the content of app.component.html is not rendered).

So I figured I can enforce it by using named outlets. However it just breaks the app completely.

I also tried to rename the remote's app.component and app.module to avoid possible name collision, but nothing changed.

Do you know what causes the problem?

Collapse
 
seanperkins profile image
Sean Perkins

In your example, does the feature.module.ts have a component with a router outlet and a router module declaration for forChild([...])? By exposing feature.module.ts in your remote's webpack config, you should be able to something similar to this in your shell/root's app routing module:

RouterModule.forRoot([
   {
       path: 'feature-path',
       loadChildren: () => loadRemoteModule({
            remoteEntry: 'http://localhost:xxxx/remoteEntry.js',
            remoteName: 'remoteExampleName',
            exposedModule: './FeatureModule'
       }).then(m => m['FeatureModuleName'])
   }
])
Enter fullscreen mode Exit fullscreen mode

This should lazy-load your remote app's feature module contents when navigating to /feature-path and then defer to that module's structure for further nested lazy loading. I've done something similar where my shell loads different authentication experiences under the /authentication namespace and certain auth experiences have further lazy loading for forgot password & user registration screens.

If you can statically declare the module federation information, it's much easier. Otherwise, you'll have to have that information in a config and pass static tokens into the app module of the shell and override the ROUTES token as shown above.

If you eco-system is small enough or not having live reloading against the remote isn't a deal breaker, you can federate the AppModule from the remote and not run into any issues.

Collapse
 
craig_payne_26cd679fd3b28 profile image
weirdfishes

Is it possible running two versions of Angular on the same page? One for Host and a different version for Remotes?

Collapse
 
seanperkins profile image
Sean Perkins

Yes, it should be possible. I'd be cautious with doing this over major versions; as you're pulling in the remote app into the context of the host. One of the many benefits of micro frontends and module federation, is that you can pull in different frameworks, versions, etc.; so that you can independently manage slices of your application and update and deploy those changes.

If your two Angular applications are using Angular Elements, you may need to share the PlatformRef between these (via the window global). There's a great walkthrough of this in the link at the top of this article.

Collapse
 
craig_payne_26cd679fd3b28 profile image
weirdfishes

Hi Sean

Thank you for the response!

I'm currently testing with:

  • Shell (Angular 12.1.0-next.6)
  • Remote (Angular 12.0.5)

Shell shared configured with @angular/* singleton: true
Remote shared configured with @angular/* singleton: false

When Shell injects Remote with loadRemoteModule lazy routing; node_modules_angular_core___ivy_ngcc___fesm2015_core_js (Angular 12.0.5 chunk from Remote) JS file is added from Remote. This immediately fires a run-time error: Error: inject() must be called from an injection context. From what I have found, the error is caused by two Angular instances running concurrently.

If I updated shared configured with
Shell shared configured with @angular/* singleton: true
Remote shared configured with @angular/* singleton: true

The Remote; is lazy loaded successfully, loaded on <router-outlet> but Remote module is rendered from the single @angular/core instance from Shell. Not ideal.

I have multiple micro-frontends running on a single Shell instance. I won't be able to necessarily keep all micro-frontends up to date with latest Angular, and Shell instance might move ahead in versions. Thought module federation would have some solutions.

Collapse
 
mbharanidharan88 profile image
mbharanidharan88

Nice Article! How to pass the injection token from the shell to the remote app?

stackoverflow.com/questions/691373...

Collapse
 
seanperkins profile image
Sean Perkins

You are unable to pass injection token instances between boundaries (shell to remote) at this time. It will always create a new instance, so you can use factories if you expect a default value. Instead of sharing tokens, you are likely better off using a service instance and sharing that between shell to remote. You can also use service workers, DOM events and other messaging-based solutions to pass the needed context you would otherwise be using a token for.

Collapse
 
thalesxavier profile image
thalesxavier

Congrats... could you please share a github URL with the source code of this example?