module-federation / core

Module Federation is a concept that allows developers to share code and resources across multiple JavaScript applications
https://module-federation.io/
MIT License
1.4k stars 206 forks source link

Feature: MFE import functions #2832

Open bh3605 opened 1 month ago

bh3605 commented 1 month ago

Clear and concise description of the problem

Angular architects has their own code to import modules using import(). https://github.com/angular-architects/module-federation-plugin/blob/main/libs/mf-runtime/src/lib/loader/dynamic-federation.ts

You have an example doing something similar for React. https://github.com/module-federation/module-federation-examples/blob/master/advanced-api/dynamic-remotes-runtime-environment-variables/host/src/hooks/useFederatedComponent.js

Would it be possible to generalize what your code uses like how angular architects has? It would be amazing if there was a single library that could leverage the high customization level the advanced mod fed plugin has and provides functions that can create a shared scope, import MFEs and even import/initialize web components. This library has everything I want to customize the remoteEntry.js file, but now it needs some helper functions to import these MFEs and share a scope object between them.

Suggested solution

There are a bunch of different ways to achieve this.

prototype code of mine using systemjs

// https://medium.com/@rkhasawneh/single-spa-with-module-federation-in-systemjs-446f0de4832b
// https://github.com/rami8k/spa-complete-example
export async function importApp(app, shareScope, importedApps) {
  let appParts = getAppParts(app)
  // if no module property then this MFE isn't using module federation
  if(!appParts.module) {
    return System.import(appParts.app);
  }
  return System.import(appParts.app).then(app => {
    if(!importedApps.includes(appParts.app)) {
      // https://webpack.js.org/concepts/module-federation/#dynamic-remote-containers
      app.init(shareScope)
    }
    if(appParts.module) {
      return app.get(appParts.module).then(module => {
        return module();
      });
    } else {
      return app;
    }
  });
}

function getAppParts(app) {
  let appParts = app.split('/')

  if(app.startsWith('@') && appParts.length > 2) {
    return {
      app: `${appParts[0]}/${appParts[1]}`,
      module: appParts.length > 2 ? appParts[2] : null
    }
  }

  return {
    app: appParts[0],
    module: appParts[1]
  }
}

You could take inspiration from how @angular-architects does it https://github.com/angular-architects/module-federation-plugin/blob/main/libs/mf-runtime/src/lib/loader/dynamic-federation.ts

This prototype of mine tries to be able to import both MFEs and module federated MFEs using @angular-architects/mod-fed and systemjs. No fear posting this code here.

let importedApps = [];
let shareScope = [];

export async function importApp(app, shareScope, importedApps) {
  let appParts = getAppParts(app);
  // if(!appParts.module) {
    // return System.import(appParts.app);
  // }
  // if(app == "consumer-services") {
  //   const s = {
  //     type: "manifest",
  //     remoteName: appParts.app,
  //     exposedModule: `./${appParts.module ? appParts.module : ''}`
  //   } as LoadRemoteModuleOptions;
  //   return loadRemoteModule(s);
    // const url = System.resolve(appParts.app);
    // const s = {
    //   type: "manifest",
    //   remoteName: appParts.app,
    //   exposedModule: `./${appParts.module ? appParts.module : ''}`
    // } as LoadRemoteModuleOptions;
    // return loadRemoteModule(s).then(
    //   (module) => {console.log(module); System.set(appParts.app, module); return module;}
    // );
    // return await import(/* webpackIgnore:true */ url).then(
    //   (see) => {
    //     System.set(appParts.app, see);
    //     return see;
    //   }
    // );
  // } else {
    return System.import(appParts.app).then(app => {
      if(!importedApps.includes(appParts.app) && app.init !== undefined) {
        // const s = {
        //   type: "manifest",
        //   remoteName: appParts.app,
        //   exposedModule: `./${appParts.module ? appParts.module : ''}`
        // } as LoadRemoteModuleOptions;
        // return loadRemoteModule(s);
        app.init(shareScope);
      }
      if(app.get !== undefined) {
        return app.get(`./${appParts.module ? appParts.module : ''}`).then(module => {
          return module();
        })
      } else {
        return app;
      }
    });
  // }
}

function getAppParts(app) {
  let appParts = app.split('/')

  if(app.startsWith('@')) {
    if(appParts.length > 2) {
      return {
        app: `${appParts[0]}/${appParts[1]}`,
        module: appParts.length > 2 ? appParts[2] : null
      }
    }
  }

  return {
    app: appParts[0],
    module: appParts.length > 2 ? appParts[1] : null
  };
}

Alternative

No response

Additional context

No response

Validations

ScriptedAlchemy commented 1 month ago

Just use module-federation/runtime. LoadRemote and init

https://module-federation.io/guide/basic/runtime.html

ScriptedAlchemy commented 1 month ago

https://github.com/module-federation/module-federation-examples/tree/master/dynamic-system-host

bh3605 commented 1 month ago

Is there a function I can call to pass my manifest json to? I prefer to using that than hardcoding. I do have it calling init(), but there's a weird build error. Also you should be exporting the object type the init method references to make typing easier. Makes it hard to type stuff.

image image

ScriptedAlchemy commented 1 month ago

Provide a repo or send a PR for the TS stuff.

ScriptedAlchemy commented 1 month ago

If you use a runtime plugin you can change the remotes on the fly.