angulartypescriptwebpack-module-federationangular-module-federation

How can I create a single instance of my service in Angular, in Module Federation?


I have recreated the issue with the repo available here: https://github.com/module-federation/module-federation-examples/tree/master/angular13-microfrontends-lazy-components.

The mdmf-shared-service isn't being used by default, so I attempted to use it but notice two seperate instances of the service getting created for mdmf-shell, and mdmf-profile. I've confirmed this by logging the constructor call and some local class values. Constructor is called twice and neither of the values passed to the instance is known to the other app. Here is the code I added - what am I doing wrong?

mdmf-shared.service.ts

import { Injectable } from '@angular/core';
import { MdmfSharedModule } from '../modules/mdmf-shared.module';

@Injectable({
  providedIn: MdmfSharedModule, // I've also tried providedIn: 'root' and 'platform' same result
})
export class MdmfSharedService {
  private count: number = 0;
  private word: string = '';
  constructor() {
    console.log(this.count++)
  }

  ping(word?: string) {
    console.log('pinging mdmf shared service')
    this.word = word;
    console.log('this is the word: ', word)
  }
}

mdmf-shared.module.ts

import { NgModule } from '@angular/core';
import { MdmfSharedComponent } from '../components/mdmf-shared.component';
import { CommonModule } from '@angular/common';

@NgModule({
  // declarations: [MdmfSharedComponent, ListUserComponent],
  declarations: [MdmfSharedComponent],
  imports: [CommonModule],
  // exports: [MdmfSharedComponent, ListUserComponent]
  exports: [MdmfSharedComponent],
})
export class MdmfSharedModule {}

app.module(mdmf-shell)

import { APP_INITIALIZER, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';
import { ShellModule } from './shell/shell.module';
import { AppComponent } from './app.component';
import { APP_ROUTES } from './app.routes';
import { MicrofrontendService } from './microfrontends/microfrontend.service';
import { MdmfSharedModule } from 'projects/mdmf-shared/src/lib/modules/mdmf-shared.module';
import { NgxsModule } from '@ngxs/store';
import { UserState } from 'projects/mdmf-shared/src/lib/app-state/state/user.state';

export function initializeApp(mfService: MicrofrontendService): () => Promise<void> {
  return () => mfService.initialise();
}

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    RouterModule.forRoot(APP_ROUTES),
    NgxsModule.forRoot([UserState]),
    MdmfSharedModule,
  ],
  providers: [
    MicrofrontendService,
    {
      provide: APP_INITIALIZER,
      useFactory: initializeApp,
      multi: true,
      deps: [MicrofrontendService],
    }
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

app.component.ts(mdmf-shell)

import { Component } from '@angular/core';
import { MdmfSharedService } from 'projects/mdmf-shared/src/public-api';
import { MicrofrontendService } from './microfrontends/microfrontend.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent {
  title = 'mdmf-shell';
  constructor(public mfService: MicrofrontendService, public mdmfSharedService: MdmfSharedService) {
    mdmfSharedService.ping() // first instance
  }
}

app.module.ts(mdmf-profile) no imports of mdmf-shared.module or service

profile.component.ts(mdmf-profile)

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { Store } from '@ngxs/store';
import { AddUser } from 'projects/mdmf-shared/src/lib/app-state/actions/user.action';
import { User } from 'projects/mdmf-shared/src/lib/app-state/models/User';
import { MdmfSharedService } from 'projects/mdmf-shared/src/lib/services/mdmf-shared.service';

@Component({
  selector: 'app-profile',
  templateUrl: './profile.component.html',
  styleUrls: ['./profile.component.css'],
})
export class ProfileComponent implements OnInit {
  angForm: FormGroup;

  ngOnInit(): void {}

  constructor(private fb: FormBuilder, private store: Store, public mdmfSharedService: MdmfSharedService) {
    mdmfSharedService.ping() // second instance
    this.angForm = this.createForm();
  }

  /**
   * Initialize the form
   */
  createForm(): FormGroup {
    return this.fb.group({
      name: ['', Validators.required],
      email: ['', Validators.required],
    });
  }

  /**
   * Handle the add user when the 'Create User' button is clicked
   * @param name: user's name
   * @param email: user's email
   */
  addUser(name: string, email: string): void {
    this.store.dispatch(new AddUser({ name, email } as User));
  }

  /**
   * Get the users for unit testing purposes
   */
  getUsers(): User[] {
    return this.store.selectSnapshot<User[]>(state => state.users.users);
  }
}

webpack.config.js(mdmf-shell)

const webpack = require('webpack');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  output: {
    publicPath: 'http://localhost:4200/',
    uniqueName: 'shell',
    scriptType: 'text/javascript',
  },
  optimization: {
    runtimeChunk: false,
  },
  plugins: [
    new ModuleFederationPlugin({
      remotes: {
        profile: 'profile@http://localhost:4201/remoteEntry.js}',
      },
      shared: {
        '@angular/core': { eager: true, singleton: true },
        '@angular/common': { eager: true, singleton: true },
        '@angular/router': { eager: true, singleton: true },
        '@ngxs/store': { singleton: true, eager: true },
        'mdmf-shared': { singleton: true, eager: true },
      },
    }),
  ],
};

webpack.config.js(mdmf-profile)

const webpack = require('webpack');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  output: {
    publicPath: 'http://localhost:4201/',
    uniqueName: 'mdmfprofile',
    scriptType: 'text/javascript',
  },
  optimization: {
    runtimeChunk: false,
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'profile',
      library: { type: 'var', name: 'profile' },
      filename: 'remoteEntry.js',
      exposes: {
        ProfileModule: './projects/mdmf-profile/src/app/profile/profile.module.ts',
      },
      shared: {
        '@angular/core': { singleton: true, eager: true },
        '@angular/common': { singleton: true, eager: true },
        '@angular/router': { singleton: true, eager: true },
        '@ngxs/store': { singleton: true, eager: true },
        'mdmf-shared': { singleton: true, eager: true },
      },
    }),
  ],
};

Solution

  • I solved my own problem, the issue I was facing - was using yarn link instead of adding the lib to the compiler options in tsconfig.json

    My theory is that yarn link removes visibility of the lib (instead simulating involvement with symlinks) and therefore removes it from compilation and the configuration of webpack's shareAll functionality.