manfredsteyer / module-federation-plugin-example

268 stars 189 forks source link

Shared monorepo lib duplicated and not shared on new mfe build #34

Closed pacoita closed 1 year ago

pacoita commented 1 year ago

Our Setup We have some Angular 14.x applications in a NX Monorepo using @angular-architects/module-federation": "^14.3.0". We have a shared library added to the sharedMappings array, as we want to share it across all mfes. This shared library exports some services, e.g. LoginService, where the shell app sets the user data and the different remote mfes read it.

Current Issue When we deploy all mfes together, everything works fine. But if we build one mfe individually and deploy only this one to leverage the mfes architecture, we notice that a new bundle is created for the shared library and added to the newly built mfe (this is not the case when we make a build all). This breaks our logic, as a new service instance is created without the user data set by the shell.

In the screenshot below, it is possible to see the duplicate bundle for the shared lib b2b-mfe-shared created for the dashboard mfe: MicrosoftTeams-image (1)

Expected behavior We would expect that even for partial builds/deployments the same shared library instance is used. We migrated from Angular 12 and the previous @angular-architects/module-federation version and individual builds/deployments worked fine with that version. We followed the provided tutorial and all seems correctly configured, therefore not sure why we get this behavior.

Build commands used:

# Individual builds
"build:shell": "ng build shell --configuration production && cp apps/shell/scripts/mime.types apps/shell/scripts/.profile apps/shell/scripts/nginx.conf apps/shell/scripts/resolvers.conf dist/apps/shell/",
"build:dashboard": "ng build dashboard --configuration production && cp apps/dashboard/scripts/mime.types apps/dashboard/scripts/nginx.conf dist/apps/dashboard/",

# builds for all mfes
"build:all": "nx run-many --target=build --configuration production --all --parallel --maxParallel=3 && npm run copy:all",
"copy:all": "cp apps/shell/scripts/mime.types apps/shell/scripts/.profile apps/shell/scripts/nginx.conf apps/shell/scripts/resolvers.conf dist/apps/shell/ && cp apps/dashboard/scripts/mime.types apps/dashboard/scripts/nginx.conf dist/apps/dashboard/",

Our shell web.config.js:

const webpackConfiguration = withModuleFederationPlugin({
  name: 'shell',
  filename: 'shell.js',

  sharedMappings: ['@b2b-frontend/b2b-mfe-shared'],  <-- This is our shared monorepo lib

  exposes: {
    './Module': 'apps/shell/src/app/app.module.ts',
  },
  remotes: {},

  shared: share({
    '@angular/core': { singleton: true, strictVersion: true, requiredVersion: '14.2.12' },
    '@angular/common': { singleton: true, strictVersion: true, requiredVersion: '14.2.12' }
    ...
  }),
});

One remote mfe (dashboard) web.config.js:

module.exports = withModuleFederationPlugin({
  name: 'dashboard',
  filename: 'dashboard.js',

  sharedMappings: ['@b2b-frontend/b2b-mfe-shared'],
  exposes: {
    './Module': 'apps/dashboard/src/app/app.module.ts',
  },

  shared: share({
    '@angular/core': { singleton: true, strictVersion: true, requiredVersion: '14.2.12' },
    '@angular/common': { singleton: true, strictVersion: true, requiredVersion: '14.2.12' },
    ...
  }),
});

One of the shared service from lib:

@Injectable({
  providedIn: 'root',   <-- We also tried with 'platform' but without any success
})
export class LoginService implements Resolve<LoginService> {
  private userInfo: UserInfo;

  // When a mfe is newly deployed, this method returns undefined, 
  // as a new instance of this service is created and not the shared one is used.
  getUserInfo(): UserInfo {
    return this.userInfo;
  }

  setUserInfo(userInfo: UserInfo): void {
    this.userInfo = userInfo;
  }

  resolve(): Observable<LoginService> {
    ...
  }
}

Shell routes for remote mfes:

const routes: Routes = [
  {
    path: 'dashboard',
    loadChildren: () => loadRemoteModule('./mfedashboard/dashboard.js').then((m) => m.AppModule),
  ...
];

@NgModule({
  imports: [
    RouterModule.forRoot(routes, { 
               onSameUrlNavigation: 'reload', 
               relativeLinkResolution: 'legacy', 
               paramsInheritanceStrategy: 'always' }),
  ],
  exports: [RouterModule],
})
export class AppRoutingModule {}

tsconfig.base.json

    "paths": {
      "@b2b-frontend/b2b-mfe-shared": ["libs/b2b-mfe-shared/src/index.ts"],
      ...
    }
pacoita commented 1 year ago

After further debugging, we found the root of the issue. In our pipeline, we set a new version in the package.json when a new build is done (either a build all or just a single mfe).

Below is the logic to create a new version through our pipeline:

string currentVersion = sh(script: "npm run version --silent", returnStdout: true).trim() 

packageJson = '{ 
     "name": "b2b-frontend-MFE-TOKEN", 
     "version": "VERSION-TOKEN", 
     "repository": { "type": "git", "url": "https://...git" }, 
     "publishConfig": { "registry": “…” }, 
     "main": "", 
     "files": [ "dist", "nginx.conf", "mime.types" ]}'.replace("MFE-TOKEN", mfe).replace("VERSION-TOKEN", currentVersion) 

writeFile file: "dist/apps/${mfe}/package.json", text: packageJson

Looking at the bundles, we saw that our monorepo shared library (b2b-mfe-shared) was taking the latest build version, even if the shared lib did not change:

MicrosoftTeams-image (2)

This was creating a new instance of the service if we were building just one mfe, since a new version was created and this was used as a reference for the shared library in the affected (newly built) mfe:

Version_Diff

As a temporary solution, we stopped updating the version in the package.json.

However, is this a known and wished behavior? This way, it seems we are binding the mfes to a specific version

manfredsteyer commented 1 year ago

As discussed, the version found in the monorepo's package.json is used.