Search code examples
angularwebpackweb-workerwebpack-module-federationwebpack-config

In Angular, using WebPack 5 Module Federations, how can we expose Web Workers used by a remote?


in my configuration I am currently using a shell application and a remote, both written in angular 12.2.0, and I'm facing a problem I am not sure how to solve.

SHELL webpack.config.js

const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
const mf = require("@angular-architects/module-federation/webpack");
const path = require("path");
const share = mf.share;

const sharedMappings = new mf.SharedMappings();
sharedMappings.register(
  path.join(__dirname, '../../tsconfig.json'),
  ['auth-lib']
);

module.exports = {
  output: {
    uniqueName: "portal",
    publicPath: "auto"
  },
  optimization: {
    runtimeChunk: false
  },
  resolve: {
    alias: {
      ...sharedMappings.getAliases(),
    }
  },
  plugins: [
    new ModuleFederationPlugin({

        // For hosts (please adjust)
        remotes: {
          "myremote": "myremote@http://localhost:4250/remoteEntry.js"
        },

        shared: share({
          "@angular/core": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
          "@angular/common": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
          "@angular/common/http": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
          "@angular/router": { singleton: true, strictVersion: true, requiredVersion: 'auto' },

          ...sharedMappings.getDescriptors()
        })

    }),
    sharedMappings.getPlugin()
  ],
};

REMOTE webpack.config.js

const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
const mf = require("@angular-architects/module-federation/webpack");
const path = require("path");
const share = mf.share;

const sharedMappings = new mf.SharedMappings();
sharedMappings.register(
  path.join(__dirname, 'tsconfig.json'),
  [/* mapped paths to share */]
);

module.exports = {
  output: {
    uniqueName: "interviews",
    publicPath: "auto"
  },
  optimization: {
    runtimeChunk: false
  },
  resolve: {
    alias: {
      ...sharedMappings.getAliases(),
    }
  },
  plugins: [
    new ModuleFederationPlugin({

        // For remotes (please adjust)
        name: "myremote",
        filename: "remoteEntry.js",
        exposes: {
            './Module': './src/app/myremote/myremote.module.ts'
        },

        shared: share({
          "@angular/core": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
          "@angular/common": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
          "@angular/common/http": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
          "@angular/router": { singleton: true, strictVersion: true, requiredVersion: 'auto' },

          ...sharedMappings.getDescriptors()
        })

    }),
    sharedMappings.getPlugin()
  ],
};

Everything works smoothly when I use the reference to my remote module in the shell app like so:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './components/home/home.component';

const routes: Routes = [{
  path: '',
  component: HomeComponent,
  pathMatch: 'full'
}, {
  path: 'myremote',
  loadChildren: () => import('myremote/Module').then(m => m.MyRemoteModule)
}];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

The problem appears when the remote module's component within its code instantiate a new Web Worker like so:

const myWorker = new Worker(
    new URL('../path/to/workers/worker.ts',
    import.meta.url
  ),
  { type: 'module', name: 'my-worker' }
);
// since I'm also using comlink, I also do that, but that's not really relevant here
this.worker = this.createWrapper<WorkerService>(myWorker);

The code above, in Webpack 5, already generates a separate bundle in my app called my-worker.js, and it's not part nor of the component's neither the module's bundle. Therefore exposing the module to Module Federation, will not expose into the remoteEntry.js also the worker's code. Then trying with the shell app to navigate to the route which loads my remote, will error and notify that:

ERROR Error: Uncaught (in promise): SecurityError: Failed to construct 'Worker': Script at 'http://localhost:4250/my-worker.js' cannot be accessed from origin 'http://localhost:5000'. Error: Failed to construct 'Worker': Script at 'http://localhost:4250/my-worker.js' cannot be accessed from origin 'http://localhost:5000'.

So the question is, if I want to have a remote and expose its modules to be used in a shell app, and one of its modules uses a Web Worker, how can I make sure that the separate worker bundle created by Webpack is also exposed in order for the shell app to be able to instantiate it?


Solution

  • I modified slightly Pravanjan answer in order to have this to work and not to throw errors related to importScripts not available on WorkerGlobalScope when dealing with Modules.

    Step 1. Create a custom worker class in a separate file

    export class CorsWorker {
        private readonly url: string | URL;
        private readonly options?: WorkerOptions;
    
        // Have this only to trick Webpack into properly transpiling
        // the url address from the component which uses it
        constructor(url: string | URL, options?: WorkerOptions) {
            this.url = url;
            this.options = options;
        }
    
        async createWorker(): Promise<Worker> {
            const f = await fetch(this.url);
            const t = await f.text();
            const b = new Blob([t], {
                type: 'application/javascript',
            });
            const url = URL.createObjectURL(b);
            const worker = new Worker(url, this.options);
            return worker;
        }
    }
    

    Step 2 - do not use Webpack Worker instead use CorsWorker to import your worker file.

    import { CorsWorker as Worker } from './cors-worker';
    
    // aliasing CorsWorker to Worker makes it statically analyzable
    
    const corsWorker = await (new Worker(
        new URL('../workers/my-worker.worker', import.meta.url),
        { type: 'module', name: 'my-worker' },
    )).createWorker();