node-mf icon indicating copy to clipboard operation
node-mf copied to clipboard

Eager ssr references

Open MaximilianKlein opened this issue 4 years ago • 2 comments

This is necessary to share any dependency eagerly in SSR. Otherwise every eagerly shared dependency throws a Shared module is not available for eager consumption. This is not because the share cannot be loaded eagerly, but because references to remotes always return promises (or the init function is a promise). The exception is thrown here: https://github.com/webpack/webpack/blob/main/lib/sharing/ConsumeSharedRuntimeModule.js#L275

  • If rpcProcess returns a promise this module here is a Promise and will cause the exception above: https://github.com/webpack/webpack/blob/main/lib/sharing/ShareRuntimeModule.js#L100
  • If init returns a promise this initRes will be a promise and will cause the exception above https://github.com/webpack/webpack/blob/main/lib/sharing/ShareRuntimeModule.js#L107

I thought about caching the remote, getRes or initRes for every succeeding get call, but in the end you always want to have the most recent version of the remote script (see caching remark in the bottom).

Sharing dependencies does not yet work with nextjs and SSR because next handles all nodejs dependencies as externals. I had to monkey path those for now like this (this also might help with sharing react the "normal" way on SSR?).

// inside your next.config.js
webpack(config, options) {
        const { webpack } = options;

        if (options.isServer) {
          /// nextjs adds a function into externals that explicitly turns node_module requires
          /// into standard 'require' statements. That happens in the hook `NormalModuleFactory.factorize`
          /// which normally would create the share definition for ModuleFederation shares.
          /// `factorize` is a Bail-Hook meaning that if one tapped in function returns something
          /// it will stop processing it (order matters here and nextjs puts its ExternalModuleFactoryPlugin
          /// before ours and we cannot do anything agains that).
          /// We now need to make sure that this function does not prevent the ModuleFederation Plugin to do
          /// its work. For that we short-circuit the next externals when we find our shared module.
          const originalExternals = config.externals[0]; // maybe not the best way to 'find' the handleExternal function
          config.externals[0] = ({ context , request , dependencyType , getResolve  }) => {
            
            if (request == 'react-intl') { // example with react-intl, you probably want to fix all shares here
              /// return something empty to prevent bailing (it is a bit more complicated, but this works)
              return Promise.resolve();
            }
            return originalExternals({ context , request , dependencyType , getResolve  });
          }
        }
// ... 

I maybe create a feature request for next to allow to disable some externals in their handleExternals function

(Caching-Remark This change is btw. also necessary if you want to enable cache-invalidation on the server side (which you probably want, to have consistent CSR and SSR renderings), otherwise even clearing the webpack and require cache won't update your SSR-components. Clearing the Webpack cache is currently only possible by writing your own webpack plugin. I did it, but it is not yet ready to share it)

MaximilianKlein avatar Sep 13 '21 06:09 MaximilianKlein

Simpy for the record: ~https://github.com/vercel/next.js/issues/29050~ https://github.com/vercel/next.js/discussions/29051

MaximilianKlein avatar Sep 13 '21 06:09 MaximilianKlein

Hi @MaximilianKlein , Thanks for raising this. I need some time to investigate the issue. Did I get the point, that for such dependency tree: ModuleFederation(A) --> uses ModuleFederation(B) --> uses eager 3pp C, ModuleFederation(A) --> uses eager 3pp C. B can't eagerly use C, right? So in my current implementation only A can eagerly use C. Also this part:

      const originalExternals = config.externals[0]; // maybe not the best way to 'find' the handleExternal function
      config.externals[0] = ({ context , request , dependencyType , getResolve  }) => {
        
        if (request == 'react-intl') { // example with react-intl, you probably want to fix all shares here
          /// return something empty to prevent bailing (it is a bit more complicated, but this works)
          return Promise.resolve();
        }
        return originalExternals({ context , request , dependencyType , getResolve  });
      }

Don't understand for now (need to read about handleExternals for next).

telenko avatar Sep 17 '21 08:09 telenko