I'm working on a new project using Angular 11 and Webpack 5. I am basing my work on Manfred Steyer's Module Federation Plugin Example repo, which uses Angular CLI. I can't figure out how to share a singleton service from a shared local Angular library between my two apps.
I'll do my best to explain my setup.. Real sorry about how long this is going to be.
Simplified File Structure
root
package.json
projects/
shell/
src/app/
app.module.ts
app.component.ts
webpack.config.ts <-- partial config
mfe1/
src/app/
app.module.ts
app.component.ts
webpack.config.ts <-- partial config
shared/
src/lib/
global.service.ts
package.json <-- package.json for lib
Angular
both app.component.ts files are identical
import {GlobalService} from 'shared';
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {
constructor(shared: SharedService) {}
}
global.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class GlobalService {
constructor() {
console.log('constructed SharedService');
}
}
Webpack
shell/webpack.config.ts
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
output: {
publicPath: "http://localhost:5000/",
uniqueName: "shell"
},
optimization: {
// Only needed to bypass a temporary bug
runtimeChunk: false
},
plugins: [
new ModuleFederationPlugin({
remotes: {
'mfe1': "mfe1@http://localhost:3000/remoteEntry.js"
},
shared: ["@angular/core", "@angular/common", "@angular/router"]
})
],
};
mfe1/webpack.config.ts
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
output: {
publicPath: "http://localhost:3000/",
uniqueName: "mfe1"
},
optimization: {
// Only needed to bypass a temporary bug
runtimeChunk: false
},
plugins: [
new ModuleFederationPlugin({
// For remotes (please adjust)
name: "mfe1",
library: {type: "var", name: "mfe1"},
filename: "remoteEntry.js",
exposes: {
'./Module': './projects/mfe1/src/app/app.module.ts',
},
shared: ["@angular/core", "@angular/common", "@angular/router"]
})
],
};
shared library package.json
{
"name": "shared",
"version": "0.0.1",
"main": "src/public-api.ts",
"peerDependencies": {
"@angular/common": "^11.0.0-next.5",
"@angular/core": "^11.0.0-next.5"
},
"dependencies": {
"tslib": "^2.0.0"
}
}
This configuration compiles and runs, but mfe1
instantiates a new GlobalService
. I see "constructed SharedService" logged on app load, then again as soon as the remote module is loaded. I tried to follow another example by ScriptedAlchemy, but I can't figure out how to make it work. It either compiles and runs and creates two instances, or it fails to compile citing a missing module depending on how i mess up my configuration I'm sure.
The ScriptedAlchemy example makes it seem like I need to reference my shared library in my shared
library array in the webpack.config.ts
files. This makes complete sense but i can't get it to work
shell/webpack.config.ts and mfe1/webpack.config.ts
...
shared: ["@angular/core", "@angular/common", "@angular/router", "shared"]
If I reference the local library this way I inevitably end up with errors during builds
Error: Module not found: Error: Can't resolve 'shared' in '/Path/to/module-federation-plugin-example/projects/shell/src/app'
The examples I posted are simplified. Hopefully not overly so, but here is a link to a repo that shows the issue
TL;DR
webpack.config.ts
I'm going to note that my project is an NX Monorepo using Angular CLI. I'm using (at the moment) Angular 11.0.0-next and Webpack 5, which is only available as an opt-in with ng11 at the time of writing.
If you're using path aliases in your tsconfig, you are used to importing local libraries like "@common/my-lib", but you can't share modules by alias in your webpack config. Furthermore, if you're on NX, your linting will complain if you import from absolute or relative library paths, so there is a disconnect between what Webpack wants, and what nx/tslint wants.
In my project I have some library aliases like the following
To make this work in my webpack config. We have to use those aliases, but tell webpack where to find the libraries by using the
import
optionThat solved the issues I was having with Webpack being unable to find modules at compile time.
My second problem was that I was attempting to share common facades that depended on common data access services. I didn't think to share the data access services because they are stateless, but this caused my MFEs to instantiate new singletons. This must be because the shared services had 'unshared' dependencies. Sharing my data access layer along with my facade layer resulted in shared singletons throughout my app.
The issue of dependencies is the most important thing here, because it's harder to debug. There are no errors or anything -- things just don't work like you expect. You may not even realize that you have two instances of a shared service at first. So, if you run into this issue take a look at your dependencies and make sure you're sharing everything you need. This is probably an argument for keeping minimal shared state/dependencies across your apps.