webpack / webpack

A bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting allows for loading parts of the application on demand. Through "loaders", modules can be CommonJs, AMD, ES6 modules, CSS, Images, JSON, Coffeescript, LESS, ... and your custom stuff.
https://webpack.js.org
MIT License
64.62k stars 8.79k forks source link

Federated Modules: Dynamic Remotes #11033

Open ahmadsholehin opened 4 years ago

ahmadsholehin commented 4 years ago

Feature request

What is the expected behavior? It seems that although dynamic imports are supported in federated modules, and it is possible to obtain remoteEntry.js on runtime, the remotes are still statically defined in webpack config.

new ModuleFederationPlugin({
      name: 'application_b',
      library: { type: 'var', name: 'application_b' },
      filename: 'remoteEntry.js',
      exposes: {
        'SayHelloFromB': './src/app',
      },
      remotes: {
        'application_a': 'application_a',
      },
      shared: ['react', 'react-dom'],
    }),

Here, application_b is statically defining application_a as a remote.

Is there a way that remotes can be defined dynamically instead?

What is motivation or use case for adding/changing the behavior?

A plugin system that spreads its plugins over many microservices.

How should this be implemented in your opinion?

Similar to lazy loading in code splitting, could dynamic imports (remotes) be preemptively prepared?

Are you willing to work on this yourself? I am not very well-versed in webpack's internals, unfortunately.

sokra commented 4 years ago

You can dynamically load remote modules using this:

// Initializes the share scope.
// This fills it with known provided modules from this build and all remotes
await __webpack_init_sharing__("default");
// TODO: load the script tag somehow. e. g. with a script loader
const container = window.application_a;
// Initialize the container, it may provide shared modules
await container.init(__webpack_share_scopes__.default);
const module = await container.get("./module");
ScriptedAlchemy commented 4 years ago

await __webpack_init_sharing__("default"); not initialize

sokra commented 4 years ago

Thanks fixed

shaodahong commented 4 years ago

In the actual project, services in remote host, and we may be exposed multiple module, can we do this?

const { Routes, Button } = await __webpack_init_module_federation_remote('https://localhost/order/remoteEntry.js');
davewthompson commented 3 years ago

@shaodahong It's completely possible to dynamically import modules; we use the following code to load the compiled code into the window object dynamically:

   await new Promise((resolve, reject) => {
      const element = document.createElement('script');

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

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

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

  const factory = await window[webpackModuleName].get(component);
  return factory();

Note this doesn't seem to work properly when you have multiple plugins with identical component names running in development mode with HMR.

shaodahong commented 3 years ago

yup, it's broken when using dev hmr

ScriptedAlchemy commented 3 years ago

HMR and MF do not work currently.

shaodahong commented 3 years ago

HMR and MF do not work currently.

@ScriptedAlchemy https://github.com/webpack/webpack-dev-server/issues/2692 Any plan?

royriojas commented 3 years ago

hi @ScriptedAlchemy and @sokra

Just wanted to confirm if it is actually possible to load dynamically federated modules from a dynamically loaded remote. It seems it is possible. I will give it a try to the code @sokra put above, but would be nice to get a confirmation from you that it is actually possible.

davewthompson commented 3 years ago

@royriojas check my code above: https://github.com/webpack/webpack/issues/11033#issuecomment-711693729. We have this working fine in dev loading federated modules just using a URL.

ScriptedAlchemy commented 3 years ago

You're likely not running remote containers over wds. The issue appears when WDS is used to host containers. I've experienced this issue and it still remains. Though I've not tested with webpack-cli/serve

I've not looked into webpack dev server or this issue further. A workaround you could apply is to add startup code and manually define the container to the window. Since WDS adds a module to the end, we can use startup code to add our own module and basically set it ourselves.

https://link.medium.com/ierojo0DKab

royriojas commented 3 years ago

Hi @ScriptedAlchemy based on all that I've found I came with this for dynamic loading modules from remotes (without hardcoding them in the webpack config)

https://github.com/royriojas/mfe-webpack-demo/tree/attempt_dynamic

But here is something very weird I noticed:

React from app_03 is resolved to the same React in that remote if not loaded before executing any code from the app_01. Not sure if it is a bug or it is intended to be like that. But since what I want is to load arbitrary code without caring about the order, this might be a deal breaker. Well not really, but it will force us to think of a workaround (like preload certain dependencies in a vendors.js file maybe?

If you comment this line here: https://github.com/royriojas/mfe-webpack-demo/blob/attempt_dynamic/packages/app-01/src/index.jsx#L8 you will see that the app fails to load the app_03

For now preloading it solves my issue, but it would be really nice to not need that.

The error that I get when I don't preload that given remote is that there are 2 React versions in the page :(

royriojas commented 3 years ago

This is the error, if @sokra or @ScriptedAlchemy or anybody knows why does this error happen.

react.development.js:1465 Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.
    at resolveDispatcher (react.development.js:1465)
    at useDebugValue (react.development.js:1529)
    at styled-components.browser.esm.js:1
    at styled.button (styled-components.browser.esm.js:1)
    at renderWithHooks (react-dom.development.js:14825)
    at updateForwardRef (react-dom.development.js:16840)
    at mountLazyComponent (react-dom.development.js:17392)
    at beginWork (react-dom.development.js:18635)
    at HTMLUnknownElement.callCallback (react-dom.development.js:188)
    at Object.invokeGuardedCallbackDev (react-dom.development.js:237)
resolveDispatcher @ react.development.js:1465
useDebugValue @ react.development.js:1529
(anonymous) @ styled-components.browser.esm.js:1
styled.button @ styled-components.browser.esm.js:1
renderWithHooks @ react-dom.development.js:14825
updateForwardRef @ react-dom.development.js:16840
mountLazyComponent @ react-dom.development.js:17392
beginWork @ react-dom.development.js:18635
callCallback @ react-dom.development.js:188
invokeGuardedCallbackDev @ react-dom.development.js:237
invokeGuardedCallback @ react-dom.development.js:292
beginWork$1 @ react-dom.development.js:23234
performUnitOfWork @ react-dom.development.js:22185
workLoopSync @ react-dom.development.js:22161
performSyncWorkOnRoot @ react-dom.development.js:21787
(anonymous) @ react-dom.development.js:11111
unstable_runWithPriority @ scheduler.development.js:653
runWithPriority$1 @ react-dom.development.js:11061
flushSyncCallbackQueueImpl @ react-dom.development.js:11106
workLoop @ scheduler.development.js:597
flushWork @ scheduler.development.js:552
performWorkUntilDeadline @ scheduler.development.js:164
react_devtools_backend.js:2273 The above error occurred in the <styled.button> component:
    in styled.button (created by UiLibraryPage)
    in Suspense (created by UiLibraryPage)
    in div (created by Page)
    in div (created by Page)
    in Page (created by UiLibraryPage)
    in UiLibraryPage (created by Context.Consumer)
    in Route (created by Routes)
    in Switch (created by Routes)
    in Routes (created by App)
    in div (created by App)
    in Router (created by HashRouter)
    in HashRouter (created by App)
    in App

Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://fb.me/react-error-boundaries to learn more about error boundaries.
overrideMethod @ react_devtools_backend.js:2273
logCapturedError @ react-dom.development.js:19560
logError @ react-dom.development.js:19597
update.callback @ react-dom.development.js:20741
callCallback @ react-dom.development.js:12512
commitUpdateQueue @ react-dom.development.js:12533
commitLifeCycles @ react-dom.development.js:19916
commitLayoutEffects @ react-dom.development.js:22834
callCallback @ react-dom.development.js:188
invokeGuardedCallbackDev @ react-dom.development.js:237
invokeGuardedCallback @ react-dom.development.js:292
commitRootImpl @ react-dom.development.js:22572
unstable_runWithPriority @ scheduler.development.js:653
runWithPriority$1 @ react-dom.development.js:11061
commitRoot @ react-dom.development.js:22412
finishSyncRender @ react-dom.development.js:21838
performSyncWorkOnRoot @ react-dom.development.js:21824
(anonymous) @ react-dom.development.js:11111
unstable_runWithPriority @ scheduler.development.js:653
runWithPriority$1 @ react-dom.development.js:11061
flushSyncCallbackQueueImpl @ react-dom.development.js:11106
workLoop @ scheduler.development.js:597
flushWork @ scheduler.development.js:552
performWorkUntilDeadline @ scheduler.development.js:164
ScriptedAlchemy commented 3 years ago

You can't unload react and magically load another copy. Webpack can only negotiate versions sharing of singletons upfront.

Only use module-federation-examples for configuring. Your webpack config is old and you're sharing api isn't right for what you're trying to do.

Please refer to my repos under the MF organization.

One look at your repo, you're not using semver sharing, webpack probably doesn't know if it can / should share. React also needs to be a singleton to function.

You can upgrade to react 17 if you need multiple copies of react for whatever reason

royriojas commented 3 years ago

@ScriptedAlchemy I don't want to load two versions of react. I actually want to share the same React version across the board. I will check the webpack config, but I was sure I recently upgraded it.

I'm not trying to share anything yet. I was just trying to understand what was possible to do with module federation. I will take a look a semver sharing.

Thank you for your feedback.

royriojas commented 3 years ago

Thanks for your feedback @ScriptedAlchemy. Using a shared singleton fixed all the issues I had. Thank u. 👍

https://github.com/royriojas/mfe-webpack-demo/commit/4711f371dfc8a69f47b69c57070158f1a3405c9e#diff-3d864b4f5a6e000cbf1420a6b94dd5ce3688b24b01a3cff1ecb817e8e5f44708R46

ScriptedAlchemy commented 3 years ago

Yeeeeee!!! 🥳

Thrilled it resolved the problem. Where did you read about the old config? Or is this just an old copy from beta 16 that you've been using.

Need to know if I should actually update my articles or not 😂😂

royriojas commented 3 years ago

@ScriptedAlchemy Old example forked from https://github.com/mizx/mfe-webpack-demo which was using the beta version

ScriptedAlchemy commented 3 years ago

Ahhh yep. He helped me build out some examples, he was the first outside user. Module federation examples was actually based on his initial effort to help us make example projects. Will DM and see if he wants to drop a link or update some examples

ksrb commented 3 years ago

For anyone googling into this, the module federation documentation has been updated and a example added:

So I have issue, I have code loading in modules 'synchronously' so something like:

import urls from "remoteModule/constants";

async function getSomething(params) {
  // ...Other processing
  return fetch(urls.aThing);
}

Unless I'm misunderstanding something, the technique used in the example wouldn't work for what I'm trying to do?

Or I would have to restructure my code to be something like:

async function getSomething(params) {
  const { aThing } = await someKindOfDynamicLoading("remoteModule/constants");
    // ...Other processing
  return fetch(urls.aThing);
}

The thing is I really don't want to do that, I guess the argument could be made that it should be explicit whether a module is being loaded externally via a function like someKindOfDynamicLoading, but for 'middleware'/non-jsx code it seems out of place?

Is there a way to modify the example(s) given so that I could load my modules synchronously? Or am I'm using federated modules in a 'unintended'/bad way?

Update: Several community members helped and figured out the issue mentioned in this comment, a example is now available under: advanced-api/dynamic-remotes-synchronous-imports

webpack-bot commented 3 years ago

This issue had no activity for at least three months.

It's subject to automatic issue closing if there is no activity in the next 15 days.

somsharp commented 3 years ago

You can dynamically load remote modules using this:

// Initializes the share scope.
// This fills it with known provided modules from this build and all remotes
await __webpack_init_sharing__("default");
// TODO: load the script tag somehow. e. g. with a script loader
const container = window.application_a;
// Initialize the container, it may provide shared modules
await container.init(__webpack_share_scopes__.default);
const module = await container.get("./module");

I am able to load remote module dynamically with the above approach. Just curious if I can pass props to the remote component? If yes, refer some sample code please!

royriojas commented 3 years ago
const factory = await container.get(moduleToLoad); // where moduleToLoad is the path to the module (including a `/.` if I recall correctly
const Module = factory();

Module.default(/* pass props here*/); // this is the function, or component or whatever that is being federated
somsharp commented 3 years ago
const factory = await container.get(moduleToLoad); // where moduleToLoad is the path to the module (including a `/.` if I recall correctly
const Module = factory();

Module.default(/* pass props here*/); // this is the function, or component or whatever that is being federated

@royriojas thank you for your reply. I was trying the follow the steps you mentioned. But now luck. In my component I have a property called 'name' with a type of string I was trying to pass the data like Module.default({name:'abcd'}) which did not work Also I tried with Module.default(name='abcd') which is also not working. Can you please help me here.

royriojas commented 3 years ago

How are you exporting your component? is that the default? otherwise you might not need the default keyword

Roy Ronald Riojas Montenegro

On Mon, Apr 12, 2021 at 5:04 PM Somnath @.***> wrote:

const factory = await container.get(moduleToLoad); // where moduleToLoad is the path to the module (including a /. if I recall correctlyconst Module = factory(); Module.default(/ pass props here/); // this is the function, or component or whatever that is being federated

@royriojas https://github.com/royriojas thank you for your reply. I was trying the follow the steps you mentioned. But now luck. In my component I have a property called 'name' with a type of string I was trying to pass the data like Module.default({name:'abcd'}) which did not work Also I tried with Module.default(name='abcd') which is also not working. Can you please help me here.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/webpack/webpack/issues/11033#issuecomment-818320826, or unsubscribe https://github.com/notifications/unsubscribe-auth/AABABWSINU5VQRE6Z2QPYSTTIODCXANCNFSM4N5DZLIA .

somsharp commented 3 years ago

How are you exporting your component? is that the default? otherwise you might not need the default keyword Roy Ronald Riojas Montenegro On Mon, Apr 12, 2021 at 5:04 PM Somnath @.**> wrote: const factory = await container.get(moduleToLoad); // where moduleToLoad is the path to the module (including a /. if I recall correctlyconst Module = factory(); Module.default(/ pass props here*/); // this is the function, or component or whatever that is being federated @royriojas https://github.com/royriojas thank you for your reply. I was trying the follow the steps you mentioned. But now luck. In my component I have a property called 'name' with a type of string I was trying to pass the data like Module.default({name:'abcd'}) which did not work Also I tried with Module.default(name='abcd') which is also not working. Can you please help me here. — You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub <#11033 (comment)>, or unsubscribe https://github.com/notifications/unsubscribe-auth/AABABWSINU5VQRE6Z2QPYSTTIODCXANCNFSM4N5DZLIA .

I am exporting as default. Here is the snippet :

import * as React from "react"; interface OwnProps { name: string; } const Widget: (React.FC) = props => { const renderMain = (): JSX.Element => { return ( <div // my additional implementation goes here {props.children}

); } return renderMain(); } export default Widget;

I am loading the above component from my host app port 3001 and remote component is running in port 3002

royriojas commented 3 years ago

Could you share your webpack config for remotes?

Roy Ronald Riojas Montenegro

On Mon, Apr 12, 2021 at 5:22 PM Somnath @.***> wrote:

How are you exporting your component? is that the default? otherwise you might not need the default keyword Roy Ronald Riojas Montenegro … <#m2640954308268869088> On Mon, Apr 12, 2021 at 5:04 PM Somnath @.*> wrote: const factory = await container.get(moduleToLoad); // where moduleToLoad is the path to the module (including a /. if I recall correctlyconst Module = factory(); Module.default(/ pass props here/); // this is the function, or component or whatever that is being federated @royriojas https://github.com/royriojas https://github.com/royriojas thank you for your reply. I was trying the follow the steps you mentioned. But now luck. In my component I have a property called 'name' with a type of string I was trying to pass the data like Module.default({name:'abcd'}) which did not work Also I tried with Module.default(name='abcd') which is also not working. Can you please help me here. — You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub <#11033 (comment) https://github.com/webpack/webpack/issues/11033#issuecomment-818320826>, or unsubscribe https://github.com/notifications/unsubscribe-auth/AABABWSINU5VQRE6Z2QPYSTTIODCXANCNFSM4N5DZLIA .

I am exporting as default. Here is the snippet :

import * as React from "react"; interface OwnProps { name: string; } const Widget: (React.FC) = props => { const renderMain = (): JSX.Element => { return ( <div // my additional implementation goes here {props.children}

); } return renderMain(); } export default Widget;

I am loading the above component from my host app port 3001 and remote component is running in port 3002

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/webpack/webpack/issues/11033#issuecomment-818327792, or unsubscribe https://github.com/notifications/unsubscribe-auth/AABABWRZMCU7N3GFD7RA3ZLTIOFGBANCNFSM4N5DZLIA .

somsharp commented 3 years ago

Could you share your webpack config for remotes? Roy Ronald Riojas Montenegro On Mon, Apr 12, 2021 at 5:22 PM Somnath @.*> wrote: How are you exporting your component? is that the default? otherwise you might not need the default keyword Roy Ronald Riojas Montenegro … <#m2640954308268869088> On Mon, Apr 12, 2021 at 5:04 PM Somnath @.> wrote: const factory = await container.get(moduleToLoad); // where moduleToLoad is the path to the module (including a /. if I recall correctlyconst Module = factory(); Module.default(/ pass props here/); // this is the function, or component or whatever that is being federated @royriojas https://github.com/royriojas https://github.com/royriojas thank you for your reply. I was trying the follow the steps you mentioned. But now luck. In my component I have a property called 'name' with a type of string I was trying to pass the data like Module.default({name:'abcd'}) which did not work Also I tried with Module.default(name='abcd') which is also not working. Can you please help me here. — You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub <#11033 (comment) <#11033 (comment)>>, or unsubscribe https://github.com/notifications/unsubscribe-auth/AABABWSINU5VQRE6Z2QPYSTTIODCXANCNFSM4N5DZLIA . I am exporting as default. Here is the snippet : import as React from "react"; interface OwnProps { name: string; } const Widget: (React.FC) = props => { const renderMain = (): JSX.Element => { return ( <div // my additional implementation goes here {props.children} ); } return renderMain(); } export default Widget; I am loading the above component from my host app port 3001 and remote component is running in port 3002 — You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub <#11033 (comment)>, or unsubscribe https://github.com/notifications/unsubscribe-auth/AABABWRZMCU7N3GFD7RA3ZLTIOFGBANCNFSM4N5DZLIA .

const HtmlWebpackPlugin = require("html-webpack-plugin"); const ModuleFederationPlugin = require("webpack").container .ModuleFederationPlugin; const path = require("path");

module.exports = { entry: "./src/index", mode: "development", devServer: { contentBase: path.join(__dirname, "dist"), port: 3002, }, output: { publicPath: "auto", }, resolve: { extensions: [".ts", ".tsx", ".js"], }, module: { rules: [ { test: /bootstrap.tsx$/, loader: "bundle-loader", options: { lazy: true, }, }, { test: /.tsx?$/, loader: "babel-loader", exclude: /node_modules/, options: { presets: ["@babel/preset-react", "@babel/preset-typescript"], }, }, ], }, plugins: [ new ModuleFederationPlugin({ name: "app2", filename: "remoteEntry.js", exposes: { "./Button": "./src/Button", "./Widget": "./src/Widget", }, shared: ["react", "react-dom"], }), new HtmlWebpackPlugin({ template: "./public/index.html", }), ], };

grzegorzjudas commented 3 years ago

For people not sure how to implement this for React lazy loaded components, assuming you have the following config available (hardcode it, preload it, add using SSR, or whatever other method of choice):

/* This is basically the same object you'd have in webpack config */
window.__remotes__ = {
    'myapp': 'myapp@http://localhost:9001/remoteEntry.js'
};

do:

declare global {
    interface Window {
        __remotes__: Record<string, string>;
    }

    const __webpack_init_sharing__: any;
    const __webpack_share_scopes__: any;
}
async function dynamicImport (path: string) {
    const [ remoteName, remoteUrl ] = Object.entries(window.__remotes__).find(([ r ]) => path.startsWith(r));

    if (!remoteName) throw new Error(`URL not configured for remote '${path}'.`);
    if (remoteUrl.split('@').length !== 2) throw new Error(`URL misconfigured for remote '${path}'`);

    const [ moduleName, moduleUrl ] = remoteUrl.split('@');

    await __webpack_init_sharing__('default');

    await new Promise<void>((resolve, reject) => {
        const element = document.createElement('script');

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

        element.onload = () => {
            element.parentElement.removeChild(element);
            resolve();
        };

        element.onerror = (err) => {
            element.parentElement.removeChild(element);
            reject(err);
        };

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

    const container = window[moduleName];
    await container.init(__webpack_share_scopes__.default);

    const component = `.${path.replace(remoteName, '')}`;
    const factory = await container.get(component);

    return factory();
}

and then just replace import(...) with dynamicImport:

const MyComponent = React.lazy(() => dynamicImport('myapp/MyComponent'));
grzegorzjudas commented 3 years ago

Btw, @sokra @ScriptedAlchemy - are these __webpack_init_sharing__ and __webpack_share_scopes__ going to be added to @types/webpack-env? Is that package owned by webpack team?

sokra commented 3 years ago

Is that package owned by webpack team?

nope, but we could add that to our own typings...

levanify commented 3 years ago

@sokra @ScriptedAlchemy I'm able to load the component dynamically, however, it seems like host container cannot share the React Context with the Remote container. Do you have any idea why? I have been stuck at this for some time

I tried to combine these 2 examples to enable the remote containers to consume context from the host container, but it doesn't seem to work:

ScriptedAlchemy commented 3 years ago

It should work. Is more than one copy of react getting loaded? If not then I don't see a reason why it wouldn't work.

Are you perhaps not sharing something that depends on react context. Like a node module

levanify commented 3 years ago

Thank you @ScriptedAlchemy for your reply. Really appreciate it!

Is more than one copy of react getting loaded?

I'm loading just one copy of react, I'm setting the webpack config for both host and remote containers to be

react: { singleton: true, requiredVersion: deps['react'] }

If I understand correctly, singleton would ensure that only one copy of react is loaded

Are you perhaps not sharing something that depends on react context. Like a node module

May I clarify what do you mean by this?

EDIT: I managed to make it work with the example repo. However, I realise that the example repo is using yarn workspaces. I'm having difficulty in replicating it in my own project using npm + lerna setup. Do you know whether this could be the source of the issue?

ScriptedAlchemy commented 3 years ago

Regarding dynamic remotes. They might be getting initialized too late or early, a remote can only be initialized once so if it depends on something from another remotes share scope as a singleton, the scare scopes cannot be reinitialized with more modules at a later stage.

pbn04001 commented 2 years ago

I have tried the mentioned by @grzegorzjudas, but I can't get past this error TypeError: __webpack_init_sharing__ is not a function

Is there something else I am needing to expose these values?

ScriptedAlchemy commented 2 years ago

That won't work if your consuming app doesn't have anything shared. If you're not planning to share any modules from the consuming app then you don't want to init sharing since there's nothing to share.

MichaelDurfey commented 2 years ago

I created a library that abstracts some of the business logic and works for suspense, loadable or others https://github.com/MichaelDurfey/mf-dynamic-remote-component

ScriptedAlchemy commented 2 years ago

@MichaelDurfey nice. Would love to house this under the federation git organization if you’d want to maintain it there. Utilities like this make or break the experience :)

MichaelDurfey commented 2 years ago

@ScriptedAlchemy Sounds good to me!

DhruvilPatel19 commented 2 years ago

@ScriptedAlchemy I Want to know how we can dynamically fetch remotes on server-side (in node server) also If we are able to fetch remote components on server then I believe it's async process then how we can make it synchronous process as we can't add 'await' to renderToString() ?

ScriptedAlchemy commented 2 years ago

The node technology is currently proprietary.

There's a package on npm called node-mf, it's the least buggy option.

I do plan to release module-federation/node in the near future.

Our internal technology is the the most stable and powers many enterprise clients. It's also what's powering SSR support on next.js and is the core tech running module-federation/aegis (hexagonal backend architecture that enables federation for any language on any compute primitive)

Also works sync or async

tzachbon commented 1 year ago

For people not sure how to implement this for React lazy loaded components, assuming you have the following config available (hardcode it, preload it, add using SSR, or whatever other method of choice):

/* This is basically the same object you'd have in webpack config */
window.__remotes__ = {
    'myapp': 'myapp@http://localhost:9001/remoteEntry.js'
};

do:

declare global {
    interface Window {
        __remotes__: Record<string, string>;
    }

    const __webpack_init_sharing__: any;
    const __webpack_share_scopes__: any;
}
async function dynamicImport (path: string) {
    const [ remoteName, remoteUrl ] = Object.entries(window.__remotes__).find(([ r ]) => path.startsWith(r));

    if (!remoteName) throw new Error(`URL not configured for remote '${path}'.`);
    if (remoteUrl.split('@').length !== 2) throw new Error(`URL misconfigured for remote '${path}'`);

    const [ moduleName, moduleUrl ] = remoteUrl.split('@');

    await __webpack_init_sharing__('default');

    await new Promise<void>((resolve, reject) => {
        const element = document.createElement('script');

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

        element.onload = () => {
            element.parentElement.removeChild(element);
            resolve();
        };

        element.onerror = (err) => {
            element.parentElement.removeChild(element);
            reject(err);
        };

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

    const container = window[moduleName];
    await container.init(__webpack_share_scopes__.default);

    const component = `.${path.replace(remoteName, '')}`;
    const factory = await container.get(component);

    return factory();
}

and then just replace import(...) with dynamicImport:

const MyComponent = React.lazy(() => dynamicImport('myapp/MyComponent'));

How do you handle errors from the script tag? For example, there was an issue loading the remote entry because of a missing chunk.

digitalhank commented 1 year ago

How do you handle errors from the script tag? For example, there was an issue loading the remote entry because of a missing chunk.

@tzachbon Try defining this in the remote's Webpack config

output {
  publicPath: 'auto'
}

I believe the remote is trying to fetch its resources on the host instead of its own server. Webpack is clever enough to resolve this for you. See here

ScriptedAlchemy commented 1 year ago

If you only need dynamic connection code, im working on delegate modules - have just released it for next.js and will work on normal support.

This lets you use static imports, but can dynamically resolve the location of the remote container as needed.

https://github.com/module-federation/module-federation-examples/pull/2756 https://github.com/module-federation/universe/tree/main/packages/nextjs-mf#beta-delegate-modules

grzegorzjudas commented 1 year ago

@tzachbon You could return a custom function that wraps factory() in try/catch clause. Haven't tried it, but something along this should work:

return () => {
    try {
        factory();
    } catch (err) {
        // handle the error somehow
    }
};
goldyfruit commented 12 months ago

I got stuck with Vite for few days https://github.com/originjs/vite-plugin-federation/issues/518 (and I started to be desperate) until I found this issue @MichaelDurfey library.

This library was a gem to me :+1:

Thanks again!

FaureWu commented 3 months ago

Currently, I have a solution to the problem of dynamic remote: The general effect is as follows:

// You can pull the remote url configuration here through code or requests
window.remote_urls = {
    app: 'http://localhost:3000/',
}

import('./bootstrap')

And you can use it as follows:

// Suppose./Button is exported in the app
import Button from 'remote:app/Button'
function Demo() {
    return <Button />;
}

I have currently implemented this pattern in my own scaffolding, and if you are interested, I will take the time to introduce it completely as above.