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

feat: add moduleFederation option #938

Open
wants to merge 13 commits into
base: main
Choose a base branch
from

Conversation

brunos3d
Copy link

@brunos3d brunos3d commented Dec 14, 2022

Summary

This PR was created to add support for Module Federation it is originated from this issue: #926

It aims to add a new option to the loadabel babel-plugin called moduleFederation, it's a boolean option so we just need to set it to true to enable the support for Module Federation.

{
  "plugins": [
    ["@loadable/babel-plugin", { "moduleFederation": true }],
    // ...
  ]
  // ...
}

This PR also adds some JSDoc typedefs to the plugin, which will help users to understand the plugin better.

Test plan

To test it you can clone the sample project using this branch as base for the test

The branch with our samples fixed
https://github.com/brunos3d/loadable-module-federation-react-ssr/tree/demo-loadable-fix

The PR of the fixed branch to check the diff
brunos3d/loadable-module-federation-react-ssr#1

})
return imports
}
/**
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The golden rule of development is to never refactor and change simultaneously.
Look like this file is not changed, however from the git point of view - everything changed.

I also cannot spot any change, but that does not mean there isn't any.

While I do appreciate typings and docs

  • please remove unrelated changes

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, thanks for the tip! Done 🚀

properties.some(
prop =>
prop.isObjectProperty() &&
prop.get('key').isIdentifier({ name: 'federated' }) &&
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how this work and when it should be used.

Please keep in mind that in no circumstances we can use federated template on the client-side bundle - that will effectively disable code splitting.
Right now, there is a big probability that it will not happen.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, make sense, the idea now is to use moduleFederation: true option only on server-side compiler

@theKashey
Copy link
Collaborator

Per my understanding, the essence of this change is to disable code splitting for federation, not to "support" federation keeping the current constraints.

The question is how to ensure this configuration is used properly - only for server-side bundle.

@brunos3d brunos3d requested a review from theKashey December 15, 2022 17:43
@brunos3d brunos3d force-pushed the feat/add-support-to-module-federation branch from 1c056ca to 6c24c14 Compare December 15, 2022 17:50
@ScriptedAlchemy
Copy link

woud we set a DefinePlugin like 'process.isBrowser' and if its false webpack could remove the dead code? Not sure if that would work or not

@brunos3d
Copy link
Author

@theKashey yeah, your right about using it only on server-side, I will work to make it work as expected, don't have ideas on how we can make it

@brunos3d
Copy link
Author

@theKashey just removed every unrelated change and forced push it to avoid overwriting the git history, I also removed the federated option, now we need to figure out how to ensure that it uses require(ID) only on server-side

@theKashey
Copy link
Collaborator

theKashey commented Dec 15, 2022

👍 @brunos3d, now the change is short and sound.


Define Plugin stuff might now work, as long as in case of process.isBrowser is not defined it will be kept "as it", while we need condition to be removed to hide require from webpack.

What we can do - add a check for browser/clientside/webpack-target and ➡️ throw an error in case of misconfiguration.
So not fail the build, but fail the runtime.

That would put customers without tests into a little dangerous situation, so we might also not document new property for now

@brunos3d
Copy link
Author

@theKashey just added the suggested warnings. As I'm not too used to babel/webpack plugins I will ask you for code suggestions to solve this issue without workarounds 😄

Comment on lines 56 to 60
// eslint-disable-next-line no-console
console.warn(
'It\'s recommended to use "isBrowser" global variable to detect the environment\n' +
'Try to add "webpack.DefinePlugin({ \'process.isBrowser\': true })" to your webpack config',
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there something internal to a "client side" Webpack target that can be read automatically? For example, as a developer if I set target: "web" (the default) or another client-targetted value, then this plugin should probably just error out here (or have a "force" option to override).

Or does Webpack not provide a standard way to expose "is browser" and every webpack tool has to invent their own? (like here)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One can use package.json:browser field to detect browser target - https://github.com/theKashey/detect-node/blob/master/package.json#L6-L10
But this is a run-time operation.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These solutions only work during runtime, @theKashey do you know if webpack provides some context to babel during the compilation?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to babel-loader source - no, it does not.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, this begs the question then: can this be an argument to LoadablePlugin webpack plugin instead, and then read somehow by the Babel plugin? Is that possible?

That would alleviate most concerns of misuse, as it could be documented to just use this option for your client-side Webpack build, which is something already natural to loadable components.

With a Babel plugin option, users have to implement some logic to modify their Babel options dynamically based on whether it's being used by the Webpack server build or client build, which while not weird is an additional configuration workflow added to use loadable, and they also have to implement some DefinePlugin thing, which again is another workflow change.

No big deal, just wondering if that could simplify things

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 BINGO!
LoadablePlugin can automatically call DefinePlugin with the variables required. As webpack plugin is expected to be used only for client side it can secure dead code elimination (in production only) and other checks to guide proper configuration.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome @theKashey! I will work on that here! 🚀

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@theKashey added DefinePlugin to LoadablePlugin and updated the warning messages

@brunos3d brunos3d requested review from bwhitty and removed request for theKashey December 22, 2022 18:00
Copy link

@bwhitty bwhitty left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that there's a webpack plugin integration, what's the purpose of the Babel plugin argument? Is there a use case for a standalone Babel build for Loadable without Webpack? If not, then it seems the Webpack plugin can be the singular configuration point.

I'm thinking that this federation config can now be set up in one place: the server-side Webpack configuration's LoadablePlugin. Like so:

// webpack.config.server.js

plugins: [
  new LoadablePlugin({
    serverSideModuleFederation: true
  }),
]

Then internally the plugin would set process.serverSideModuleFederation = true, and then @loadable/babel-plugin can just read that. There's no need to set up a separate config on the Babel plugin, as it's configured via Webpack.

Comment on lines 106 to 110
const defs = {
'process.isBrowser':
compiler.options.target === 'web' ||
compiler.options.target === undefined,
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To go with my suggestion of adding a serverSideModuleFederation argument to the webpack plugin, if we do then this logic could throw an error for misconfiguration:

Suggested change
const defs = {
'process.isBrowser':
compiler.options.target === 'web' ||
compiler.options.target === undefined,
}
if (this.opts.serverSideModuleFederation === true &&
compiler.options.target === 'web' ||
compiler.options.target === undefined
) {
throw new Error(`Enabling server-side module federation support for a client-side Webpack target (${compiler.options.target}) is unnecessary and will effectively disable all code-splitting.`
}

Comment on lines 113 to 115
new ((webpack && webpack.DefinePlugin) || require('webpack').DefinePlugin)(
defs,
).apply(compiler)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with my suggestion this would become

Suggested change
new ((webpack && webpack.DefinePlugin) || require('webpack').DefinePlugin)(
defs,
).apply(compiler)
if (this.opts.serverSideModuleFederation) {
// eslint-disable-next-line global-require
new ((webpack && webpack.DefinePlugin) || require('webpack').DefinePlugin)({
serverSideModuleFederation: true
}).apply(compiler)
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice job @bwhitty, I will test it here!! 🚀

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@theKashey I am not being able to get the value of serverSideModuleFederation passed to DefinePlugin during the execution of the resolveProperty function, do you have some idea on how can I access this value?
I already tried with:

  • process.serverSideModuleFederation
  • global.serverSideModuleFederation
  • only serverSideModuleFederation

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ScriptedAlchemy I got these snippets from NodeFederationPlugin do you have some idea why I can't get the serverSideModuleFederation during babel compilation execution?

Comment on lines 115 to 117
(this.opts.serverSideModuleFederation === true &&
compiler.options.target === 'web') ||
compiler.options.target === undefined
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these parens make this logic wrong I think? should be

Suggested change
(this.opts.serverSideModuleFederation === true &&
compiler.options.target === 'web') ||
compiler.options.target === undefined
this.opts.serverSideModuleFederation === true &&
(compiler.options.target === 'web' || compiler.options.target === undefined)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it was not correct, just fixed, but it was not the problem

@brunos3d
Copy link
Author

Hey, folks as we didn't find a proper way to pass the serverSideModuleFederation option from the LoadablePlugin (webpack) to babel, I just decide to inject the value to process using directly, as it will be used only during the compilation time, I can't see any problems on that solution. Excited to see your suggestions about it
cc: @theKashey @bwhitty @ScriptedAlchemy

@ScriptedAlchemy
Copy link

@theKashey any input here - would be great to move this forwards if we can

}

if (this.opts.serverSideModuleFederation) {
Object.defineProperty(process, 'serverSideModuleFederation', {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 that's a fancy way to "teleport" settings.
However, the particular way you did it will break some assumptions of node creating a hard to understand "gap" for a few (rare) users of thread-loader or similar stuff.

Ease to fix. Just store what is required in process.env - a special place designed for this.

@theKashey
Copy link
Collaborator

Before we processed with the merge, I want to ask one question - Where is the best place to handle this stuff?
Having the problem of "federation gap", ie one's inability to simply require yet undefined stuff, there are two options:

  • make a sync require in the loadable (this PR). That would solve problem only for loadable and in a very specific way.
  • preload all known components in the loadable (not which are not currently tracked)
  • preload all federated sources via ModuleFederationPlugin. Ie go via all known remotes and import them before starting a server

@fivethreeo
Copy link
Contributor

This does module federation for all modules. Should we do it as a option on loadable call?

@brunos3d
Copy link
Author

brunos3d commented Jan 9, 2023

This does module federation for all modules. Should we do it as a option on loadable call?

It does, but should be only used on the server-side, in the past I had put it as an option for the loadable function, it was a option called 'federated'. But the problem is, MF plugin changes the way imports happen even non-federated containers get these __webpack_modules__[moduleId] is not a function errors

@theKashey
Copy link
Collaborator

So that is the question: if one technology should adapt to support another technology - could there be a third technology to change as well?
For example, @fivethreeo is working on React 18 update with a "proper suspense" on the server side - #923 - which technically can allow us to use plain import without require.resolve

@ScriptedAlchemy
Copy link

Proper suspense would work for everything moving forwards, but we would still need a solve for async-node target / lower react versions :P

@brunos3d
Copy link
Author

@theKashey @ScriptedAlchemy An issue we're facing right now is when we try to consume a container from some remote using loadable and the remote is down, MF tries to generate a fake remote and a fake container, but loadable throws multiple errors saying that the provided module is not a react component (cause it's an empty object), an a bunch of other errors.

image

image

image

image

By now I'm using the following workaround for local linked and patched @loadable/component

// on InnerLoadable component at the top of the "render" function

if (result && typeof result === 'object' && Object.keys(result).length === 0) {
  console.log('probably the fake container')
  result = `<span>failed to load remote container</span>`
}
// ...

It checks if the result (the loaded/resolved module) is not undefined and verifies if its an empty object (the fake container generated by MF) then it replaces it with the warning message

@theKashey
Copy link
Collaborator

There is a resolveComponent option you can provide to loadable, and the one capable to check imported object and throw and error (to emulate load failure, as it is a failure). Wondering if can call another check just after it and throw an error if component was resolved to nothing. That should trigger "normal" error branches (fallback or ErrorBoundary), not break createElement.

A little different issue, but quite related

@ScriptedAlchemy
Copy link

@brunos3d i might be able to help with the fake remote which returns null. We could modify node federation or allow it to throw a rejection. Generally I avoid that since throwing errors in sync imported components can cause webpack runtime to crash

@ranshamay
Copy link

great work!
@gregberge any plan to merge it soon? :)

@brunos3d
Copy link
Author

@ranshamay Probably we will not merge this PR, but I'm working on some stuff:

I will post the updates here soon as I finish everything, but its also good to keep an eye on https://valor-software.com/articles too 🚀

@oguzhanaslan
Copy link

@brunos3d I saw this example that you prepared: https://github.com/module-federation/module-federation-examples/tree/88c252fbc3fdc3262e0d8e3d03a945276bd0a165/loadable-react-16

In my server-side micro frontend application, I will use module federation to combine shared libraries (react, react-dom, axios). Is it problematic to use this? I am eagerly waiting for your answer.

@stale
Copy link

stale bot commented Aug 13, 2023

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the wontfix label Aug 13, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants