Search code examples
javascriptwebpackwebpack-module-federation

Auto import modules with Module Federation


I have a simple application written in vanilla javascript and using Module Federation to wrap things up. So far, I've separated the javascript and the styling into two separate "apps":

├ src/
│ ├ lib/
│ │ └ myApp.js
│ ├ scss/
│ │ └ styles.scss
│ ├ index.js
│ └ styles.js
└ webpack.config.js

The index.js imports myApp.js that has all the logic and styles.js simply imports a SASS-file with all necessary styling like this:

import './scss/signing-widget.scss';

The ModuleFederationPlugin in webpack.config.js is setup as follows:

module.exports = {
  entry: {
    index: ['./src/index.js'],
    styles: ['./src/styles.js'],
  },
  ...
  plugins: [
    new ModuleFederationPlugin({
      name: 'myApp',
      filename: 'remoteEntry.js',
      exposes: [
        './myApp': './src/index.js'
        './myAppStyles': './src/styles.js'
      ],
      shared: [
        require('./package.json').dependencies
      ],
    })
  ],
  ...

And to implement and use myApp you need to do the following:

<html>
  <head>
    ...
    <script defer src="http://path-to-my-app/index.js"></script>
    <script defer src="http://path-to-my-app/styles.css"></script>
    <script defer src="http://path-to-my-app/remoteEntry.js"></script>
  </head>
  <body>
      <my-app></my-app>
  </body>
</html>

But, I only want to implement the app by only importing the remoteEntry.js like this:

<html>
  <head>
    ...
    <script defer src="http://path-to-my-app/remoteEntry.js"></script>
  </head>
  <body>
      <my-app></my-app>
  </body>
</html>

But I can't figure out how to do it and I've done a lot of research but I haven't found any example nor documentation on how to achieve this with ModuleFederationPlugin. Can someone help me on this matter?

Thanks in advance, Clydefrog


Solution

  • I ended up making my own "mounting" script. I'll share my solution to anyone who find this interesting or experiencing the same problem.

    ├ src/
    │ ├ lib/
    │ │ └ myApp.js
    │ ├ scss/
    │ │ └ styles.scss
    │ ├ index.js
    │ ├ mount.js <------ NEW MOUNT
    │ └ styles.js
    └ webpack.config.js
    

    Instead of manipulating or change the rmeoteEntry.js to auto import modules, I created a simple javascript file (mount.js) that detects its own script-tag. Then it extracts the base URL and then iterates through each file that needs to get imported with the same base URL:

    mount.js

    (() => {
      const parseUrlString = (url) => !url || url.length <= 0 ? '' : new URL(url).origin;
    
      var startsWith = '^',
        contains = '*',
        endsWith = '$',
        scriptElement = document.querySelector(`script[src${endsWith}="widgetMount.js"]`),
        url = parseUrlString(scriptElement?.getAttribute('src')),
        head = document.getElementsByTagName('head')[0];
    
      [
        'styles.css',
        'index.js',
        'remoteEntry.js',
      ].forEach((filename) => {
        var newElement;
    
        switch (filename.split('.')[1]) {
          case 'css':
            newElement = document.createElement('link');
            newElement.setAttribute('rel', 'stylesheet');
            newElement.href = `${url}/${filename}`;
            break;
          case 'js':
            newElement = document.createElement('script');
            newElement.setAttribute('defer', '');
            // newElement.setAttribute('async', '');
            newElement.type = 'module';
            newElement.src = `${url}/${filename}`;
            break;
        }
        
        head.appendChild(newElement);
      });
    })();
    

    Adding it to webpack.config.js in my remote application:

    module.exports = {
      entry: {
        ...,
        widgetMount: ['./src/mount.js'], // widget mount
        ...
      },
      ...
      plugins: [
        new ModuleFederationPlugin({
          ...,
          exposes: [
            ...,
            './myMount': './src/mount.js'
            ...
          ],
          ...,
        })
      ],
      ...
    }
    

    Last but not least, adding the mount into my hosting application:

    <html>
      <head>
        ...
        <script defer src="http://path-to-my-app/widgetMount.js"></script>
      </head>
      <body>
          <my-app></my-app>
      </body>
    </html>
    

    And Voilà:

    <html>
      <head>
        ...
        <script defer src="http://path-to-my-app/widgetMount.js"></script>
        <link rel="stylesheet" href="http://path-to-my-app/styles.css">
        <script defer src="http://path-to-my-app/index.js" type="module"></script>
        <script defer src="http://path-to-my-app/remoteEntry.js" type="module"></script>
      </head>
      <body>
          <my-app></my-app>
      </body>
    </html>