1

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?

gigaDIE
  • 388
  • 4
  • 10
  • My guess is, this is the issue here: new URL('../path/to/workers/worker.ts',) - because you are actually on the Shell domain, not on the remote domain... Not sure how to fix though as I'm unsure if you can use an absolute URL for a worker.. – MikeOne Sep 28 '21 at 14:09
  • Thanks @MikeOne, but I don't think that's the issue. Cause that's the path to the ts file which Webpack uses to generate the bundle, which gets executed at runtime on a separate context by the worker. The problem is more related to the fact that ModuleFederation bundles modules/components into remoteEntry.js and exposes it publicly, so that the shell app can access to it. But, being the worker bundle separate, it doesn't get included in the public "route" and therefore it's not accessible to the shell.. still trying to understand how to solve this issue. – gigaDIE Sep 29 '21 at 12:33

2 Answers2

1

Thanks to alexander-akait, the answer is here

https://github.com/webpack/webpack/discussions/14380

Given that the error was a SecurityError

ERROR Error: Uncaught (in promise): SecurityError

The issue was related to needing to setup CORS in the local server.

gigaDIE
  • 388
  • 4
  • 10
  • Define "local server"?? I'm having the exact same issue for a while now, the cors are enabled as by default my env setup is to use a proxy configuration (the remote app config in angular.json serve section contains the proxyConfig pointing to the proxy.conf file) not sure why it doesn't work for me :/ when serving I get a proper indication: "Generating browser application bundles (phase: setup)...[HPM] Proxy created: /web/api/v2.1 -> https://foo" – liron_hazan Jan 14 '22 at 09:36
0

I have found a workaround.

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

export class CorsWorker {
 private readonly worker: Worker;

constructor(url: string | URL) {
    const objectURL = URL.createObjectURL(
        new Blob([`importScripts(${JSON.stringify(url.toString())});`], {
            type: 'application/javascript'
        })
    );
    this.worker = new Worker(objectURL);
    URL.revokeObjectURL(objectURL);
}

getWorker() {
    return this.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 = new Worker(new URL('../worker', import.meta.url));

const worker = corsWorker.getWorker();

This example works for me. We can get more info by following this GitHub discussion thread https://github.com/webpack/webpack/discussions/14648

Pravanjan
  • 698
  • 7
  • 16