Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

federated store example fails #3536

Open
wibed opened this issue Jan 13, 2024 · 7 comments
Open

federated store example fails #3536

wibed opened this issue Jan 13, 2024 · 7 comments

Comments

@wibed
Copy link
Contributor

wibed commented Jan 13, 2024

based on my recent comment here:
#2042

i tried to proof my theory, yet fail to do so. now i am asking myself if i have made a mistake elsewhere:
https://github.com/wibed/mf-example_federated-store

in short:
i try to avoid an approach like this

const dynamicFederation = async (scope, module) => {
  const container = window[scope];

  // or get the container somewhere else
  // Initialize the container, it may provide shared modules
  await container.init(__webpack_share_scopes__.default);
  return container.get(module).then(factory => {
    const Module = factory();
    return Module;
  });
};

and rather use

...
// server
    exposes: {
      './store': './src/client/store',
    },
...
// client
    exposes: {
      './App': './src/bootstrap',
    },    
...

i tried using the file extension as well /src/bootstrap.tsx, but it fails to load. even passing an empty object in case the store not loading in time causes some issue.

EDIT: i try not to change the store in any way. just passing it down the tree to see if i can implement stateful applications remotely. later on i would like to inject reducers as in the example
https://github.com/module-federation/module-federation-examples/tree/master/redux-reducer-injection

@ScriptedAlchemy
Copy link
Member

@ScriptedAlchemy
Copy link
Member

I need to update the redux example still to use new apis etc from federation

@wibed
Copy link
Contributor Author

wibed commented Jan 14, 2024

id love to chip in, yet dont know if you meant it as a "wow theres so much to it, i might do this myself" or more of a "good if this gets an update sometime"

@ScriptedAlchemy
Copy link
Member

Well I'm trying to update as many as I can but it's very repetitive and takes a long time. But the tasks are not particularly difficult to port. There's just a lot. By all means feel free to look at any of the examples. All webpack ones still use v1 MF from webpack. But rspack ships with enhanced built in. If you want to give it a stab I'll push to a branch etc if needed.

@wibed
Copy link
Contributor Author

wibed commented Jan 15, 2024

Resources

here is the official documentation:
https://webpack.js.org/concepts/module-federation/

i am going to link mentioned resources here just for further reference:
https://github.com/module-federation/universe/tree/main/packages/runtime
https://github.com/module-federation/universe/tree/main/packages/enhanced

id like to include the video and its resources as well:
webpack/webpack.js.org#3757
https://www.youtube.com/watch?v=9F7GoDMrzeQ

Contention

after having a look i assume the dynamic remote pattern applies for it is the remote which should override the mutated store, due to injecting its reducer into it, making it accessible to the host via the App component.

Reasoning

from the official documentation

Each build acts as a container and also consumes other builds as containers. This way, each build is able to access any other exposed module by loading it from its container.

Shared modules are modules that are both overridable and provided as overrides to nested containers. They usually point to the same module in each build, e.g., the same library.

the loadComponent function

function loadComponent(scope, module) {

// from example dynamic-remotes
function loadComponent(scope, module) {
  return async () => {
    // Initializes the share scope. This fills it with known provided modules from this build and all remotes
    const Module = await loadRemote(`${scope}/${module.slice(2)}`);
    return Module;
  };
}

which relies on the loadRemote function
https://github.com/module-federation/universe/blob/b129098e1004a2577bc43c9bf2c5004ce928b399/packages/runtime/src/core.ts#L556

async loadRemote<T>(
  id: string,
  options?: { loadFactory?: boolean; from: 'build' | 'runtime' },
): Promise<T | null> {

  try {
    const { loadFactory = true } = options || { loadFactory: true };

    // 1. Validate the parameters of the retrieved module. There are two module request methods: pkgName + expose and alias + expose.
    // 2. Request the snapshot information of the current host and globally store the obtained snapshot information. The retrieved module information is partially offline and partially online. The online module information will retrieve the modules used online.
    // 3. Retrieve the detailed information of the current module from global (remoteEntry address, expose resource address)
    // 4. After retrieving remoteEntry, call the init of the module, and then retrieve the exported content of the module through get
    // id: pkgName(@federation/app1) + expose(button) = @federation/app1/button
    // id: alias(app1) + expose(button) = app1/button
    // id: alias(app1/utils) + expose(loadash/sort) = app1/utils/loadash/sort

    const { module, moduleOptions, remoteMatchInfo } =
      await this._getRemoteModuleAndOptions(id);
        
    const { pkgNameOrAlias, remote, expose, id: idRes } = remoteMatchInfo;
    const moduleOrFactory = (await module.get(expose, options)) as T;

    await this.hooks.lifecycle.onLoad.emit({
      id: idRes,
      pkgNameOrAlias,
      expose,
      exposeModule: loadFactory ? moduleOrFactory : undefined,
      exposeModuleFactory: loadFactory ? undefined : moduleOrFactory,
      remote,
      options: moduleOptions,
      moduleInstance: module,
      origin: this,
    });

    return moduleOrFactory;

  } catch (error) {
  
    const { from = 'runtime' } = options || { from: 'runtime' };
    const failOver = await this.hooks.lifecycle.errorLoadRemote.emit({
      id,
      error,
      from,
      origin: this,
    });

    if (!failOver) {
      throw error;
    }

    return failOver as T;
  }
}

should load a shared container and initialize sharing on webpack

//  ./core/src/api/api.ts
16 export async function loadAndInitializeRemote(
17   remoteOptions: RemoteOptions,
18 ): Promise<RemoteContainer> {
19   const globalScope = getScope();
20   const containerKey = getContainerKey(remoteOptions);
21 
22   // TODO: Make this generic, possibly the runtime?
23   const asyncContainer = loadScript(containerKey, remoteOptions);
24 
25   if (!asyncContainer) {
26     throw new Error('Unable to load remote container');
27   }
28 
29   // TODO: look at init tokens, pass to getSharingScope
30 
31   if (!globalScope.__sharing_scope__) {
32     // TODO: Make this generic, possibly the runtime?
33     globalScope.__sharing_scope__ = await initializeSharingScope();
34   }
35 
36   return initContainer(asyncContainer, globalScope.__sharing_scope__);
37 }


// ./core/src/integrations/webpack/factory.ts
5 export async function initializeSharingScope(
6   scopeName = 'default',
7 ): Promise<SharedScope> {
8   const webpackShareScopes = __webpack_share_scopes__ as unknown as SharedScope;
9 
10   if (!webpackShareScopes?.default) {
11     await __webpack_init_sharing__(scopeName);
12   }
13 
14   // TODO: Why would we reference __webpack_require and not __webpack_share_scopes__ ?
15   return (__webpack_require__ as unknown as WebpackRequire)
16     .S as unknown as SharedScope;
17 }

exactly as the function would.

what i dont get, which leads me to my initial question, is how do these approaches differ from one another?

One declared within the confines of a webpack configuration and called using an import statement

// remote2/config/module-federation.js
...
    exposes: {
      './App': './src/bootstrap',
    },
...    

// remote1/src/App.js
...
const RemoteApp = React.lazy(
  () => import('remote2/App') as Promise<{ default: React.FC<{ store: AppStore }> }>,
);
...

there while the other within the App.js file (as above)

function loadComponent(scope, module) {

// from example dynamic-remotes
function loadComponent(scope, module) {
  return async () => {
    // Initializes the share scope. This fills it with known provided modules from this build and all remotes
    const Module = await loadRemote(`${scope}/${module.slice(2)}`);
    return Module;
  };
}

sorry for updating the comment so frequently 😅

@wibed
Copy link
Contributor Author

wibed commented Jan 18, 2024

trying to use dynamicFederation as in

const dynamicFederation = async (scope, module) => {
  const container = window[scope];

  // or get the container somewhere else
  // Initialize the container, it may provide shared modules
  await container.init(__webpack_share_scopes__.default);
  return container.get(module).then(factory => {
    const Module = factory();
    return Module;
  });
};

sadly fails as the module scoped in window is only the current one, but id like to import from the remote one.
removing ssr is not an option.

@wibed
Copy link
Contributor Author

wibed commented Jan 18, 2024

related #3580

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants