Local development with non-local federated modules

Does complex usages require complex solutions? (spolier alert...)
In this case it doesn't.

We have multiple frontend applications, that are being deployed to three different environments (qa, staging and production) and each application is being served under a specific subpath (i.e: example.com/app1, example.com/app2) using CloudFront, Lambda@Edge and S3.

We are sick of full page loads when navigating between applications and I'm gonna cut to the chase here - we are trying to use module federation to have one host application which will act as a skeleton, and several remotes which acts as the content. In simple terms - microfrontends.

Ideally we want to be able to work with non-local remotes while in local development of the host application because we don't really want to run more than 15 applications everytime we work locally.

Problem is, until now each application had a fixed, relative publicPath, and now when we load the remote application in our local host application all the assets are relative to the wrong domain (the local one, that belongs to the host) causing all the assets to be fetched from the wrong place.

The problem in simple terms

Let's say we have an application, called app1 and it's deployed to QA environment.
As I previously mentioned, it's common sense that we want to be able to use the remote from QA locally when working on our host application but the relative publicPath that is set on the remote is causing trouble when it's being loaded on a local host - being relative to the wrong domain and failing to fetch the correct files.

We need to be able to modify the publicPath of a remote during runtime so that it includes the domain, and the subpath that the application is being served on.

Wait! There's more.

We want this to happen only when a local host is using a non-local remote, publicPath works just fine in all the other scenarios. (this is our insurance that we don't modify the publicPath when we don't need to)

The solution in simple terms

One would say you could solve it by creating a separate build for each environment, but that's not as futuristic as module federation, so no thank you.

Based on the circumstances, a custom webpack plugin that will accept the subpath of the current application, and will detect during runtime whether it's being rendered inside a local host and will modify (during runtime) the publicPath with the full domain and subpath, sounds better.

tl;dr

// DynamicPublicPathForRemoteInLocalHost.js

module.exports = class DynamicPublicPathForRemoteInLocalHost {
  constructor(props) {
    if (props.pathPrefix) {
      this.pathPrefix = props.pathPrefix;
    }
  }

  apply(compiler) {
    compiler.hooks.make.tap('MutateRuntime', (compilation) => {
      compilation.hooks.runtimeModule.tap('MutateRuntime', (module) => {
        const isPublicPathRuntimeModule =
          module.constructor.name === 'PublicPathRuntimeModule';

        if (!isPublicPathRuntimeModule) {
          return;
        }

        const [key] = module.getGeneratedCode().split('=');

        // This function mutates publicPath in runtime when remote is used in local env.
        // Without this plugin, when running host in local and importing remotes from QA, remote files are mistakenly being fetched from local instead of QA.
        module._cachedGeneratedCode = `${key}=(() => {
          if (window.location.host.startsWith('local') && !document.currentScript.src.includes('local')) {
            return new URL(document.currentScript.src).origin + '${this.pathPrefix}';
          } else {
            return '${module.publicPath}';
          }
        })();`;
        return module;
      });
    });
  }
};

And here's how you'd invoke it in your webpack config:

module.exports = {
  ...
  output: {
    publicPath:
      process.env.NODE_ENV === 'local'
        ? 'https://localhost:3001/'
        : '/app1/',
  },
  plugins: [
    ...
    new DynamicPublicPathForRemoteInLocalHost({
      pathPrefix: '/app1/',
    }),
  ],
};

Next time you'll try to use a non-local remote locally, the custom webpack plugin will detect that and will modify the publicPath to be absolute, therefore fixing the issue with relative publicPath.

What happens if you won't? absolutely nothing!
The custom plugin will fallback to whatever publicPath you set in your webpack config if it doesn't detect the local scenario, which is a local host loading a non-local remote.

Have a better solution? I will be more than happy to hear in the comments and hopefully make our design even better.

Resources:

This article is based on several github comments and some articles, all combined helped achieve this solution that suits our specific design.

28