vercel / next.js

The React Framework
https://nextjs.org
MIT License
126.87k stars 26.97k forks source link

Module Federation Support #16368

Closed ScriptedAlchemy closed 2 years ago

ScriptedAlchemy commented 4 years ago

Feature request

@Timer @rauchg @timneutkens In order to stop my "Twitter Driven Development" (still love that quote) I have updated the description of this issue to hopefully help move this challenge forwards.

Is your feature request related to a problem? Please describe.

Support Module Federation ability to share vendor code, like react

Describe the solution you'd like

The bare minimum to get this working client or server-side would be to have some flag that changes how the client entrypoint gets started. If the client.js looked like this https://github.com/module-federation/module-federation-examples/blob/master/bi-directional/app1/src/index.js then there's a high likelihood we could hold up the application until whatever shared modules with static imports were ready before hydration begins.

Current workaround

My workaround no longer works for SSR (since 10.x), but works for CSR - is to alias react as some external global https://github.com/module-federation/nextjs-mf/blob/main/react.js

~I then patchSharing which is an ultra hack that literally inlines react into the head of _document as a script https://github.com/module-federation/nextjs-mf/blob/main/patchSharing.js~ I use resolve.alias against react$ to point react imports to my custom react file https://github.com/module-federation/nextjs-mf/blob/main/withModuleFederation.js#L27

~This is not optimal for production-grade use but the limits of next give us little choice~ Seems okay for production grade apps

Even with Webpack 5, what we have done to get our federated AB testing, Tag manager, and AB testing engine to work with next is apply a shim that creates a fake sharing API that attaches react to share scope manually. This mechanism is the same one that I used a year ago when next was still webpack 4 based and I was using webpack 5 remotes against v4 hosts.

import logger from "../logger";
/*  This logic is needed because we're not using webpack 5 yet */

const sharedExports = {};

const shareExports = (exports, remote = "") => {
  /*
  exports is expected to be an object
  {
    "react": ()=> Promise.resolve(()=>require("react"))
  }

  The value should be:
  - a function that returns a promise
  - that promise should resolve to a function
  - that function should return the module
  */

  // if no remote is given "" is used and that will apply to all remote entries
  // This method is additive, there is no "unshare"
  sharedExports[remote] = Object.assign(sharedExports[remote] || {}, exports);
};

const applySharedExports = (remote) => {
  if (typeof window !== "undefined") {
    // Given a remote entry, we gather all the modules that the host application has already
    const mergedExports = {
      ...sharedExports?.[""], // apply globally shared modules
      ...sharedExports?.[remote] // modules shared only for this remote entry
    };

    // Logic for applying @ScriptedAlchemy  
    // check if using new webpack API
    const override = window?.[remote]?.init
      ? window?.[remote]?.init
      : window?.[remote]?.override;
    if (window?.[remote]?.init) {
      Object.assign(
        mergedExports,
        Object.entries(mergedExports).reduce((acc, [key, value]) => {
          if (typeof value === "function") {
            let version = "16.13.1";
            try {
              // eslint-disable-next-line
              version = value.version;
            } catch (error) {
              logger.error("Error retrieving shared version", error);
            }

            Object.assign(acc, {
              [key]: {
                [version]: {
                  get: value,
                  loaded: true,
                  from: "dar"
                }
              }
            });
          }
          return acc;
        }, {})
      );
    }
    if (override) {
      try {
        override(Object.assign(mergedExports, __webpack_require__.O));
      } catch (error) {
        logger.warn(`Unable to apply webpack 5 shim of shared exports`, error);
      }
    }
  }
};

export { shareExports, applySharedExports };
lancej1022 commented 2 years ago

@ScriptedAlchemy did you ever explored if it's possible to use MF with Remix?

Yeah it is. Jacob used to be on my platform team at lulu. Hems at remixx now. He knows MF better than anyone else outside of webpack, together we got MF working on next and in the process he prototyped it on remixx

The trick is you pretty much have a sidecar build (same as next) that's webpack based. So the remote entry and exposed modules go thru webpack. Then you can hook share scope up to ESM modules from esbuild.

Process is near the same for how we make it work in nextjs. Sometime in December we were planning to try and make something out of it.

Spoke with him about it last week actually so this is something we are actively thinking about

@ScriptedAlchemy do you know if there are any plans to add an example of this to the Module Federation examples on GitHub and/or release a plugin to facilitate this?

We already use Module Federation at work but are currently doing client-rendering which has obvious paint points. Ive wanted to move us to Next for some time now, but the ongoing lack of support for Module Federation means we would need to do some drastic reworking of our architecture that just wouldn't be worth it.

If Remix looks winds up with more realistic Module Federation support in the nearer future, then that's ultimately the path for our company to head down.

yumauri commented 2 years ago

If Remix looks winds up with more realistic Module Federation support in the nearer future, then that's ultimately the path for our company to head down.

@lancej1022 I pretty much doubt Remix will adopt MF in near future. Reason is dead simple — Remix doesn't use Webpack.

keropodium commented 2 years ago

Support for this is a must!

balazsorban44 commented 2 years ago

We appreciate everyone letting us know they’re looking for a solution here. However, we'd like to ask you to reserve comments for additional information to move this discussion forward. If you would like to show your support, please add a :thumbsup: emoji on the initial comment instead. Thank you.