Malo
Malo

Reputation: 21

Webpack Module Federation and shared Angular Library issue with custom MatFormFieldControl

I recently migrated and split my Angular 17 frontend application into several micro-frontends using Webpack Module Federation. My goal was to enable independent deployment of each micro-frontend (each representing a distinct business area) to gain better control over production deployments.

Each micro-frontend is entirely separate, with no communication or shared dependencies between them, except for a shared Angular components library called @common-frontend.

Current Setup

The @common-frontend library is the only shared dependency between the micro-frontends. To allow independent deployment of each micro-frontend, I configured @common-frontend as follows in the shared section:

shared: {
    "@common-frontend": { singleton: false, strictVersion: false },
}

This setup ensures that each micro-frontend can use its own version of @common-frontend without conflicts. So far, everything worked fine!

The Issue

Recently, I added custom Angular Material form controls to the @common-frontend library. These controls are used like this:

<mat-form-field appearance="outline">
  <mat-label>Name</mat-label>
  <lib-custom-input placeholder="name" formControlName="name" />
</mat-form-field>

Here’s where the problem arises. To make it easier to explain, let’s assume three Angular 17 applications:

Scenario 1: Different Versions of @common-frontend

If B and C have different versions of @common-frontend, everything works perfectly. When navigating between B and C, the appropriate chunks for @common-frontend are fetched and loaded dynamically based on the respective app's version.

Scenario 2: Same Version of @common-frontend

If B and C share the same version of @common-frontend, the first app I navigate to (e.g., B) fetches the chunk for @common-frontend. However, when I then navigate to C, Webpack does not fetch the chunk again since it is already cached. This results in the following error when trying to use the custom Angular Material form controls:

Error: mat-form-field must contain a MatFormFieldControl.

This issue currently occurs only with mat-form-field. All other components from @common-frontend work correctly, even when the same version is used across multiple micro-frontends.

I believe this issue stems from Angular’s injection context not being properly shared or initialized between micro-frontends. Refreshing the page resolves the issue, but this is not an acceptable solution for production.

Goals

Here are my goals:

  1. Maintain one Angular application per "business" domain.
  2. Enable independent deployment of each micro-frontend without causing side effects on other applications.
  3. Use a shared components library (@common-frontend) across all micro-frontends.

Questions and Concerns

  1. Could this issue be related to how I’m using Webpack Module Federation?
  2. Is there a better or smarter way to achieve my goals while avoiding such injection issues?
  3. Are there specific considerations or patterns I should follow when sharing Angular libraries between micro-frontends?

If anyone has faced similar issues or has insights into resolving this problem, I would greatly appreciate your help. 🙏

Thank you for your time and support! ❤

Project dependencies

All the dependencies version are the same between all the app, excepts @common-frontend

  "dependencies": {
    "@common-frontend": "<CAN CHANGE>",
    "@airbus/elements": "1.9.0",
    "@airbus/icons": "1.8.1",
    "@angular-architects/module-federation": "^17.0.8",
    "@angular/animations": "^17.3.0",
    "@angular/cdk": "17.3.1",
    "@angular/common": "^17.3.0",
    "@angular/compiler": "^17.3.0",
    "@angular/core": "^17.3.0",
    "@angular/forms": "^17.3.0",
    "@angular/material": "17.3.1",
    "@angular/platform-browser": "^17.3.0",
    "@angular/platform-browser-dynamic": "^17.3.0",
    "@angular/router": "^17.3.0",
    "ngx-skeleton-loader": "7.0.0",
    "ngx-toastr": "^16.2.0",
    "rxjs": "~7.8.1",
    "tslib": "^2.3.0",
    "zone.js": "~0.14.0"
  },

webpack.config.js (remote)

const { withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');
const deps = require('./package.json').dependencies;

module.exports = withModuleFederationPlugin({

  name: 'micro-frontend-a',

  exposes: {
    './ARootModule': './src/app/root/root.module.ts',
  },

  shared: {
    "@common-frontend": { singleton: false, strictVersion: false },

    "@angular-architects/module-federation": { singleton: true, strictVersion: true, requiredVersion: deps['@angular-architects/module-federation'] },
    "@angular/animations": { singleton: true, strictVersion: true, requiredVersion: deps['@angular/animations'] },
    "@angular/common": { singleton: true, strictVersion: true, requiredVersion: deps['@angular/common'] },
    "@angular/common/http": { strictVersion: true },
    "@angular/compiler": { singleton: true, strictVersion: true, requiredVersion: deps['@angular/compiler'] },
    "@angular/core": { singleton: true, strictVersion: true, requiredVersion: deps['@angular/core'] },
    "@angular/forms": { singleton: true, strictVersion: true, requiredVersion: deps['@angular/forms'] },
    "@angular/platform-browser": { singleton: true, strictVersion: true, requiredVersion: deps['@angular/platform-browser'] },
    "@angular/platform-browser-dynamic": { singleton: true, strictVersion: true, requiredVersion: deps['@angular/platform-browser-dynamic'] },
    "@angular/router": { singleton: true, strictVersion: true, requiredVersion: deps['@angular/router'] },
    "@angular/material": { singleton: true, strictVersion: true, requiredVersion: deps['@angular/material'] },
    "@angular/cdk": { singleton: true, strictVersion: true, requiredVersion: deps['@angular/cdk'] },

    (...)
  }
});

webpack.config.js (host) => same without exposes: {} for the host i use loadRemoteModule inside app.routes.ts

  {
    loadChildren: () =>
      loadRemoteModule({
        exposedModule: './ARootModule',
        remoteEntry: environment.remote.a.url,
        type: 'module'
      }),
    path: 'a'
  },

Upvotes: 1

Views: 65

Answers (0)

Related Questions