How we solved the third party app problem for NPWD, and how you can use the same approach

A few months ago, we at Project Error decided to try and support third party apps without actually knowing how. We discussed multiple solutions, and we’ll cover them all in this post.

After having used Webpack’s Module Federation solution at work, we decided it would be the go-to solution for NPWD as well, but before we move on let’s figure out what Module Federation is.
Module Federation is a plugin from Webpack that let’s you transport code from one project to another, no matter the stack. This is not to be confused with microfrontends, which is a UI architecture pattern.

Let’s get some important keywords down:

  • remotes: Code that lives in a different project, and is imported to the project that defines the remotes
  • exposes: Code that is being exposed to other projects

Quick example
Let’s say I have a React project with an component, I can expose it to other React projects that may use it.

export const CoolButton = () => (
  <button>Click me<button>
)

// webpack.config.js
// plugins
new ModuleFederation({
  name: 'remote_app',
  exposes: {
    './Button': './src/Button.tsx'
  }
})

Here we are exposing the Button component, which we then can import in other projects

import { Button } from 'remote_app'

<Button />

That’s how easy it really is.

Third party apps

So how did we accomplish handling third party apps? Remote dynamic imports in runtime. We’ll get back to this later.

In our first version, we had a simple *.js file where we would add the ‘cfx nui’ url to the resource iframe

/// config.app.js
module.exports = {
  "remote_app": () => {
     return import("remote_app/button")
   }
} 

This would then be imported into our webpack config, and added to the remotes field in the ModuleFederation plugin.

Now, this worked well…until we realized that it required rebuild of NPWD each time a new app was added. This is not optimal.

Finally, we figured out that Webpack has the option to grab any sort of remote from the window object. This meant that we could access the modules from window, and then load through code in runtime. This required to add the cfx nui url to the HTML head element.

const loadScript = async (url, scope, module) => {
    await new Promise((resolve, reject) => {
      const element = document.createElement('script');

      element.src = url;
      element.type = 'text/javascript';
      element.async = true;

      element.onload = (): void => {
        element.parentElement.removeChild(element);
        resolve(true);
      };
      element.onerror = (error) => {
        element.parentElement.removeChild(element);
        reject(error);
      };

      document.head.appendChild(element);
    });
  };

Now, all we had to do is load the script, and add the app config to the actual codebase.

// Where to get the remote code from
const url = IN_GAME
        ? `https://cfx-nui-${appName}/web/dist/remoteEntry.js`
        : 'http://localhost:3002/remoteEntry.js';
      const scope = appName;
      const module = './config';

      // load the script and add to head element
      await loadScript(url, scope, module);
    
      // find any shared remotes
      await __webpack_init_sharing__('default');
      const container = window[scope];
      
      await container.init(__webpack_share_scopes__.default);
     
      // Call factory and return a new object with original app config.
      const factory = await window[scope].get(module);
      const Module = factory();

      const appConfig = Module.default();

      const config = appConfig;
     // Here we construct the rest of the app

So…should I use this?

If you have a big codebase, with a lot of NUI resources using the same UI libraries or components, using a microfront pattern with Module Federation might be useful. If you want to load any sort of external code in runtime like we do, then yes.

What about Vite?

Vite does also have a module federation like-solution, but this doesn’t support dynamic imports in runtimes at the moment. If you don’t need it, use Vite.

The end

This was a quick overview of Microfrontends and Module federation in FiveM. If you have any questions, please ask me and I’ll try to answer.

10 Likes

Adding some links:
Webpack Module Federation docs: Module Federation | webpack
NPWD Implementation: npwd/useExternalApps.tsx at master · project-error/npwd · GitHub
Article we based our solution on: Let's Dynamic Remote modules with Webpack Module Federation - DEV Community 👩‍💻👨‍💻

2 Likes

Very clever solution and a great post! It’s nice to see more advanced topics being discussed here.

Keep up the good work at PE. :slight_smile: