Vite + Single-SPA:  A Recap up to vite-plugin-single-spa v0.3.1

Vite + Single-SPA: A Recap up to vite-plugin-single-spa v0.3.1

Hello, everyone. I haven't been around for some time, but not because I have abandoned the subject. On the contrary, I have been working hard on my primary project. At this point in time, I have successfully converted the custom implementation we had to a set of root + micro-frontend projects joined together with single-spa. Yes, I also converted the Create React App projects we had to Vite.

In short: It looks like my primary objective has been fulfilled.

I must tell you, however, that some things changed along the way. The most notable one is that I did not convert the root project to Svelte. It remains in React for the time being. My timetable, however, contemplates full conversion of all projects.

Revised Objectives

So, because I set expectations of what would happen at the beginning of this series of articles, allow me to present the revised list of objectives, so we are all on the same page:

My primary objective remains: Migrate the entire codebase from React to Svelte with the assistance of single-spa.

My secondary objectives, however, have suffered some transformation: No longer will I go with the root project to Svelte as my first transformation, but instead I'll make the project go on-demand transformation to Svelte as the priorities arrive. The general rule to follow, and it has already proven to be a great path, is to aggressively convert the pieces that require modification/maintenance into single-spa micro-frontends with Vite + Svelte instead of correcting the React codebase.

The consequence of this rule is the spawn of additional micro-frontends that might be too fine-grained. To counter this, we will allow the number of micro-frontends to grow until a point is reached where some can coalesce into one. This will be repeated until the entire codebase is converted.

Thankfully, we are using Kubernetes, meaning we can easily deploy all these pods until everything settles into a reasonable number and scheme of micro-frontends.

Enough chit-chat, I would say, let's cover the new learnings since the last article that led to the current iteration of vite-plugin-single-spa.

CSS Injection

The previous article in the series describes our learning towards gaining full CSS injection in Vite + XXX projects. Well, just like the previous article hinted, vite-plugin-single-spa v0.1.0 introduced a dynamic ESM module that produces single-spa lifecycle functions to control the injection of CSS for us automatically when the project is built; CSS injection is not necessary while running micro-frontends in serve mode because Vite takes care of this for us.

After this, I experimented a little bit with Vite + Vue router-enabled projects. The project created using npm create vue@latest comes with dynamic imports for Vue components. This causes CSS splitting when the project is built by Vite. To account for this, vite-plugin-single-spa v0.2.0 adds a projectId property to its list of options. This identifier is added to all CSS asset names created by Vite during the building process. Then, the dynamic module uses this identifier to enable and disable the CSS link elements when the micro-frontend is mounted and unmounted.

More testing is needed for this feature, especially with other technologies such as React Router. However, it looks promising according to my primary objective's project: Routing seems to be working just fine in my React projects, which was another secondary objective. However, my project's projects don't split CSS.

If you are in the routing and CSS splitting business using vite-plugin-single-spa, make sure you report any issues you find along the way so we can all enjoy a quality plug-in.

Asset-Serving in Serve Mode

Great news! As it turned out, Vite is (and has always been) able to serve assets in serve mode. I was simply unaware of the correct setting. As of v0.2.0, vite-plugin-single-spa sets the server.origin property so images and other assets coming from micro-frontends are properly served while in serve mode.

Improving Import Mapping

While experimenting with Vue micro-frontends and a Vue root project, I decided to explore the route of shared libraries. The single-spa documentation recommends that we only import large libraries like React and Vue as shared libraries.

One great way to do this is to import these libraries from unpkg.com or a similar CDN. Combine this with Vite's (or really, rollup's) ability to exclude libraries from the project's build and we have a solution.

As of v0.3.1 (v0.3.0 had a major flaw) of vite-plugin-single-spa, the options importMap.dev and importMap.build have been redefined: These can now take an array of import map file names instead of a single file name. This allows us developers to create a third file (or more if deemed appropriate) to cater to shared libraries.

Because this is something new to this article series, allow me to pose a quick example:

Add the file src/importMap.shared.json to a root project. Declare in this file the URL's for things like Vue or React libraries.

{
    "imports": {
        "vue": "https://cdn.jsdelivr.net/npm/vue@3.3.8/+esm",
        "vue-router": "https://cdn.jsdelivr.net/npm/vue-router@4.2.5/+esm"
    }
}

This import map uses the JSDelivr network to download both vue and vue-router. We can now use this file in both development and build modes to mount built Vue micro-frontends. Why not to mount Vue micro-frontends not built? Well, because Vite server will always try to serve the vue and vue-router modules. The import maps will only be in effect when mounting built micro-frontends. When mounting micro-frontends running in serve mode, just have those NPM packages installed as dev dependencies.

Now vite.config.ts for the root project would look like this (for a Svelte root project):

import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
import vitePluginSingleSpa from 'vite-plugin-single-spa';

export default defineConfig({
  plugins: [svelte(), vitePluginSingleSpa({
    type: 'root',
    imo: '3.1.0',
    importMaps: {
      dev: ['src/importMap.Dev.json', 'src/importMap.shared.json'],
      build: ['src/ImportMap.json', 'src/importMap.shared.json']
    }
  })]
});

We are importing the shared JSON import map in both cases, without having to repeat the import maps in two different files. this is our gain.


Ok, folks, this has brought us up-to-date in the vite-plugin-single-spa topic as of today. However, there's more coming!

Our next topic will be single-spa parcels and their unique case regarding CSS mounting. Spoiler: This requires a new version of vite-plugin-single-spa which is currently in the making! The next article will explain my findings, as per usual, and how the plug-in could or would tackle the problem.

In the meantime, happy coding!