single-spa and Vite:  Fast-Forward to vite-plugin-single-spa

single-spa and Vite: Fast-Forward to vite-plugin-single-spa

Hello, everyone! Here I am, after many weeks of "inactivity" in this area of expertise. But no worries, as I have come back with important updates regarding my efforts into understanding and conquering the desired coupling of Vite projects and single-spa.

The last time we chatted about this topic, I left you all with the impression that we were going to fully resolve resource asset loading using a proxy configuration in the root project. Initially, this was the approach until I started learning more about Vite and testing more complex configurations between Vite projects, import maps and the import-map-overrides package, where I concluded that a simpler approach seemed to exist. So let's talk about this part of my journey.

The Birth of vite-plugin-single-spa

If you have read the previous article in this series, you'll notice that the Vite configuration needs to be tweaked a bit. If it had been just that, I would have never thought about creating a Vite plug-in that did that configuration. Life usually teaches us that expectations many times differ from reality, and my case fell into this category.

When starting to explore the inclusion of import-map-overrides, I noticed a console error telling me that the module map overrides could no longer take place because some code had already started importing modules. I checked the project and nothing seemed out of the ordinary. Then I reviewed the source code of the served HTML page and there it was: Vite server injects a script called client.js as the first child of the <head> HTML element. This provides Vite's goodness, such as HMR (Hot Module Replacement).

This forced my hand: It is impossible to change the position of client.js in the HTML markup unless it is done by a Vite plug-in. This is the main reason why I created vite-plugin-single-spa.

Better Asset-Fixing Approach and Why It Doesn't Work

If you were to read Vite's documentation around the base configuration property (and the related Public Base Path explanation), you would realize that by setting the base property to the full URL your MiFe is being served from, then all asset URL's would include said base path and the asset problem would be fixed. It sounds like a perfect solution for both serving in development and building. The documentation clearly states that base is applied in both scenarios. You even see the text "Base public path when served in development or production." in Visual Studio Code's Intellisense as the description for the property. This is where all goes to hell, and to my dismay, I did not realize this until after I completed the initial release of vite-plugin-single-spa.

This doesn't work because the documentation fails to disclose some conditions for the URL configured in base to preserve its full nature.

The Truth About Vite's base Property

This is how base truly works, as of Vite v4.4.6:

  1. The value may be a relative URL, absolute URL, or full URL. A relative URL is either an empty string or ./; an absolute URL starts with /; a full URL specifies protocol and domain like this:

  2. When serving the application (running npm run dev or npm run preview), a full URL is converted to an absolute URL by doing (new URL(base, '')).pathname.

  3. When building the application (running npm run build), a full URL is properly retained as intended by you and me (the developer).

This is the unspoken truth about the base property. Because the base is not preserved during serving in development, this means that this approach only works in the local development environment (the developer's PC) by first building, then previewing. It doesn't work when serving (npm run dev). This kind of sucks because we usually want to debug while in Development, and building minifies our project's code.

If you are interested in the code details, allow me to direct your attention to line 899 of config.ts, where you find the following piece of code:

  // parse base when command is serve or base is not External URL
  if (!isBuild || !isExternal) {
    base = new URL(base, '').pathname
    // ensure leading slash
    if (base[0] !== '/') {
      base = '/' + base

If this conditional did not have the !isBuild part, then external bases would be preserved in all cases, including serve. This would then make Vite append the full URL to all asset URL's, as dictated by the aforementioned Vite documents, making us all single-spa developers, very happy.

So if this approach doesn't completely work, why do I mention it here? The short answer is that I have hopes that this can be made to work if the core Vite team approves it. I have opened a discussion in Vite's GitHub repository about this issue. If you would like to support the effort, please drop by and upvote the discussion. I am unaware of the motivation to preserve full URL's only on build operations, so I cannot tell if removing the offending condition is a small or large effort.

For now, the workaround to see assets in development is to set the base path to http://localhost:XXXX using the plug-in options, build, and then preview. But I digress. I will be explaining the use of vite-plugin-single-spa in the next article of the series after I release experimental version 0.0.3 of the plug-in.

Getting Back On Track: What Is Coming

Let's remember one more time why this blog series exists: To document my journey into re-writing a React-based micro-frontend solution as a Svelte-based solution that uses single-spa. The issues about import maps and Vite's base property have deviated us from this central topic.

The next article will explain how to use vite-plugin-single-spa in its current state in order to serve locally using preview, not serve. It will also cover its other features, namely its use in root projects and how to include import maps.

After that, and depending on whether or not we see traction from Vite's core team to change the current behavior of the base property, we will be developing a less-than-ideal version of the plug-in to take a different approach: Setting up proxy rules in the root project. This is not ideal because it doesn't work with import map overrides the way the single-spa core team recommends: To use a deployed website and then override the micro-frontend you want to test/debug/develop by using import-map-overrides. The micro-frontend will load, but the assets won't load, or the loaded assets will be coming from the deployed server.

I haven't decided on anything so far, so I cannot advance any details. In all honesty, I sincerely hope that Vite's core team will agree on the modification as it is the only true solution I see on the horizon.

Once we get a more stable plug-in, we can give the project a more stable and "final" form that can be called worthy of the efforts.

Thank you for reading, and remember to bookmark the series to never miss an article.