module-federation / core

Module Federation is a concept that allows developers to share code and resources across multiple JavaScript applications
https://module-federation.io/
MIT License
1.52k stars 235 forks source link

Request: Federation Runtime CSS asset Link insert hook #2878

Open hrmcdonald opened 2 months ago

hrmcdonald commented 2 months ago

Clear and concise description of the problem

Issue

Today the federation runtime seems to handle the loading and insertion of CSS files on our behalf. Even in projects where we specifically remove and re-append the CssExtractRspackPlugin so that we can define our own insert callback, that seems to be ignored since the runtime appears to be loading and inserting CSS asset links instead.

There are many places where this seems to possibly occur today. Essentially anywhere where document.head.appendChild is called to append styles. The location of where JS assets get appended shouldn't really be relevant here though.

Why is this an issue?

In an MFE platform we are building out for our wider organization (with MF 2.0 and the federation runtime which are awsome so far!) we need to make use of Shadow DOMs. Each top-level "micro-app" (which we have defined as anything with it's own route/sub-routes) gets loaded into its own isolated shadow root. We need this for two reasons:

The issue here is that the Module Federation Runtime is inserting CSS asset links in the head of the document on our behalf. This leads to these styles now affecting everything except the isolated shadow root where it is needed (the opposite of what we want to have happen 😆).

Suggested solution

If we could get some kind of callback hook that we could pass to preloadRemote and hopefully even loadRemote that'd be amazing. Ideally this hook works like the insert function of CssExtractRspackPlugin where it is just called with each link element that needs to be inserted.

This approach would allow us to define the implementation for how link elements get appended to the DOM. In our case this means we could append the Link elements to the shadow root created for a given micro-app, but also possibly cache it to reinsert it across multiple instances of a micro-app being initialized across the platform.

Alternative

There is not a good alternative right now. This would block us from being able to use MF manifests and any value they provide.

Eventually I assume we'd consider trying to develop some custom solution for importing CSS assets that allows us to manage them better at build and run time, but just being able to control the insertion of these seems far more simple.

Additional context

No response

Validations

zackarychapple commented 2 months ago

Really interesting idea. Would love to chat through this more and bring in some folks for a design discussion. Want to hop on a meetup sometime next week?

hrmcdonald commented 2 months ago

Would love to sync and discuss this some more with anyone @zackarychapple! Unfortunately we're pretty booked up next week. The week after or beyond should be fine though (September onwards). I just joined the module-federation discord and can maybe reach out to you there if that'd be easier to schedule something?

2heal1 commented 2 months ago

Thanks your advice , it's very usefule ,so you want mf to provider a hook which likes insert hook to control the css/js element ? Am i understand right ?

zackarychapple commented 2 months ago

Would love to sync and discuss this some more with anyone @zackarychapple! Unfortunately we're pretty booked up next week. The week after or beyond should be fine though (September onwards). I just joined the module-federation discord and can maybe reach out to you there if that'd be easier to schedule something?

I think i shot you a dm. If not can you shoot me one zmanc on there.

hrmcdonald commented 2 months ago

Thanks your advice , it's very usefule ,so you want mf to provider a hook which likes insert hook to control the css/js element ? Am i understand right ?

Yes, that hook actually isn't even that great because it's run completely out of context from the rest of the page. So being able to pass in some kind of function hook like this alongside a call to loadRemote/preloadRemote would actually even help solve that problem for us.

The goal of the callback would be the same though; be called with any <link> elements that need to be appended to the DOM for some incoming module(s) and then implement the logic that does append them where we want them.

We might even be able to investigate loading those CSS assets JS-side to initialize them as constructed styles sheets so they could be shared across any repeat instances of a "micro-app", but that'd be a separate investigation and not a requirement.

hrmcdonald commented 2 months ago

@2heal1 here is a super simplified example to showcase the basic idea behind the scenario I outlined above. Imagine logic contained inside appContext.mount() is just where something like a React root would be created for a component and hydrated as needed. It's not really important for the ask here.

const mountApp = async (outlet: Element, route: string) => {
  const moduleMetadata = appManifest.get(route);
  const shadowRoot = outlet.shadowRoot ?? outlet.attachShadow({ mode: 'open' });

  const { default: remoteModuleConfig } = await loadRemote(`${moduleMetadata.scope}/${moduleMetadata.module}`, { 
    insertLink: (linkTag: HTMLLinkElement) => shadowRoot.append(linkTag)
  });

  const appContext = remoteModuleConfig();
  await appContext.mount(shadowRoot);
}

The idea here is that the insertLink callback could be passed down into the manifest loader logic and be called where that attaches link asset elements to the DOM instead of always appending them to the document head. This would let us ensure an application's styles are always loaded and scoped inside of its parent shadowRoot instead of outside of it where they cannot affect it at all.

hrmcdonald commented 2 months ago

After looking into the docs a bit more, I do see there is a plugin system for the FederationRuntime we might be able to user here it seems? I see a createScript hook in the docs but no createLink. It seems like there are types for a createLink hook though, is that something public we could tap into here?

ScriptedAlchemy commented 2 months ago

CreateLink is techincally controlled by "webpack runtime" not federation runtime. So this would be something in mini-css runtime module. We may be able to provide some way to patch the function and provide a insert hook though, maybe

hrmcdonald commented 2 weeks ago

CreateLink is techincally controlled by "webpack runtime" not federation runtime. So this would be something in mini-css runtime module. We may be able to provide some way to patch the function and provide a insert hook though, maybe

mini-css as in something similar to the classic mini-css-extract-plugin? Could you maybe point me to where in the src that occurs? When we have some time we can play around with some potential implementations until maybe y'all have time to get around to looking into this further.

ScriptedAlchemy commented 2 weeks ago

Mini css has insert option. That's what controls style inject location. My runtime doesn't actually inject the css. It's the bundlers runtime. I only inject the remote, the bundler injects the chunks

ScriptedAlchemy commented 2 weeks ago

Just go to mini css plugin Readme. It's one of the options.

hrmcdonald commented 2 weeks ago

We're using Rsbuild/Rspack everywhere for this at the moment. So if I'm understanding correctly you're referring to the CssExtractRspackPlugin right?

We have that configured on the "remotes" to log the insert so we could see if we can work with that. While that does get called if loaded in directly via classic remoteEntry.js, it isn't called when the remote manifest is loaded via mf runtime (from mf-manifest.json). When loaded via runtime is it handled by the "host's" version of the plugin I guess? We'll have to check that out.

ScriptedAlchemy commented 2 weeks ago

Okay so this is with the manifest specifically?

hrmcdonald commented 2 weeks ago

Okay so this is with the manifest specifically?

Yes, when loaded from mf-manifest.json the CssExtractRspackPlugin insert hook is never called. I might be wrong, but it appears that somehow something else was loading the link assets in there. I assumed it was tied to how link assets are loaded with manifests so that they can also potentially be preloaded.

We can probably hack something around this by intercepting document.head.appendChild calls somehow, but if it's simple enough to add a hook that works with loadRemote/manifests that'd be nice given it seems to be bypassing the css build plugin somehow.