Making your Vite Project Standalone and single-spa Simultaneously

Heredia, Costa Rica

Published: 2023-05-01

2023-08-04 UPDATE: A Vite plug-in is now available. Refer to this article. Furthermore, help us improve Vite for single-spa by upvoting this discussion.

Ok, welcome to part two of my single-spa series. In the first article, we managed to ditch create-single-spa in favor of creating our projects as we normally would, either using npm create svelte@latest for SvelteKit, or with npm create vite@latest for regular Svelte or even other frameworks (Vue, React, etc.). I also promised you could make the mife applications behave simultaneously as a single-spa mife and a standalone web application. So let's get started!

Understanding the Project

In order to achieve the promised feat, we just need to look at the mechanism that is used to create both versions (single-spa and standalone). Let's start with standalone.

Standalone Vite Projects

All Vite projects work very similarly: We ask rollup to bundle our JavaScript and CSS for faster delivery (fewer HTTP requests and less size due to minification). Basically, that's it. rollup (webpack works the same too) takes a single input file, and then spits out bundles that cover 100% of what that input file can possibly need.

The input file for Vite projects is index.html.

The bundled JavaScript for standalone applications suppresses all module exports, however, because web applications are not meant to be consumed by anyone. They just need to be run in the browser to make the magic of whichever framework you chose, happen. This is important, as this is not the case for single-spa.

single-spa Vite Projects

Ok, there is no such thing as a "single-spa Vite project". Not until now, that is.

For single-spa, index.html is irrelevant. All we care about is an entry module that exports the single-spa lifecycle functions. Knowing how standalone mode works, we should now be able to produce a rollup configuration that serves our needs.

In the first article, I recommended not to touch src/main.ts, and instead create a new file, src/spa.ts. This new file will be the input for rollup. This you can see already in the previous article. We, however, did it in a quick and dirty way. We can do better.

Modifying the Project

The first thing we need to do, if you haven't done so already, is add the src/spa.ts file and export the lifecycle functions. I'll repeat the code contents here for clarity:

import App from "./App.svelte";
import singleSpaSvelte from "single-spa-svelte";

const lc = singleSpaSvelte({
    component: App
});

export const { bootstrap, mount, unmount } = lc;

Just like that, you have your single-spa entry module. This is an example for Svelte, as it is my goal to ultimately transform my application to Svelte. Equivalent code and helper packages exist for several other frameworks, such as React or Vue.

Now, let's reconfigure rollup. To do this, we must learn just a little bit about how Vite configuration works.

What You Need to Know About Vite

Vite projects contain the vite.config.ts file that controls how your project is served or built. We are normally given a file that exports the final configuration object. We can, however, "upgrade" this export to a function that returns the configuration object. Why, exactly, would we do that? To gain access to two pieces of information: The command and the mode. These come as part of an options object given to the function via its first parameter.

The command property is a string value that tells us if the project is being served or built ('serve', or 'build'). The mode property, also a string, can be anything we want and by default, it is either 'development' or 'production'. When we run npm run dev, we are running vite serve; when we run npm run build, we are running vite build --mode production.

With this in mind, we can write logic inside the configuration-building function that sets index.html as input, or src/spa.ts as input:

import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'

export default function (opts) {
  console.log('Executing Vite %s in %s mode...', opts.command, opts.mode);
  const input = {};
  let preserveEntrySignatures: any;
  let assetFileNames: string;
  let entryFileNames: string;
  if (opts.mode === 'standalone') {
    input['index'] = 'index.html';
    preserveEntrySignatures = false;
    assetFileNames = 'assets/[name]-[hash][extname]';
    entryFileNames = '[name]-[hash].js';
  }
  else {
    console.log('SPA build.');
    input['spa'] = 'src/spa.ts';
    preserveEntrySignatures = 'exports-only';
    assetFileNames = 'assets/[name][extname]';
    entryFileNames = '[name].js';
  }
  return defineConfig({
    plugins: [svelte()],
    build: {
      target: 'es2022',
      manifest: true,
      rollupOptions: {
        input,
        preserveEntrySignatures,
        output: {
          exports: 'auto',
          assetFileNames,
          entryFileNames
        }
      }
    },
    base: '/spa01'
  });
};

The main thing here is creating the list of inputs inside the input variable depending on the mode, and for our purposes, we are defining a new mode: The standalone mode. The value of preserveEntrySignatures is also fundamental. In standalone mode, we don't want any module exports, but in non-standalone mode ("spa" mode), we do want exports.

The change in the file output file names (the assetFileNames and entryFileNames variables) is inconsequent to the topic at hand, but I left it here because I don't want a mangled name. You see, my project at work -and this blog is about migrating my work project- is served with Nginx servers, and I have discovered a way to never worry about browser caches without having to mangle bundle file names.

We are almost set. All we need to do is modify package.json. Modify the scripts section to call for standalone mode whenever we are developing:

  "scripts": {
    "dev": "vite --mode standalone",
    "build": "vite build",
    "build-sa": "vite build --mode standalone",
    "preview": "vite preview",
    "check": "svelte-check --tsconfig ./tsconfig.json"
  },

I have also created a new script, build-sa, to produce the original build (a standalone project). However, I don't foresee much use for it. Your choice.

This is it! You now have a project that works simultaneously as a regular (standalone) web application or as a single-spa mife. When you use create-single-spa to create projects, this is a luxury you don't have unless you jump through several hoops.

If you execute npm run dev, you'll see this:

> svelte-mife01@0.0.0 dev
> vite --mode standalone

Executing Vite serve in standalone mode...

If you execute npm run build, you'll see this:

> svelte-mife01@0.0.0 build
> vite build

Executing Vite build in production mode...
SPA build.
vite v4.3.3 building for production...
✓ 11 modules transformed.
dist/manifest.json           0.38 kB │ gzip: 0.16 kB
dist/assets/svelte.svg       1.95 kB │ gzip: 0.91 kB
dist/assets/spa.css          0.92 kB │ gzip: 0.48 kB
dist/assets/spa.js           8.02 kB │ gzip: 3.40 kB
✓ built in 326ms

This is what interests us. The file assets/spa.js is your single-spa entry module.

But what about running in single-spa mode when developing? As it turns out, it is super simple: You just point your module to the spa.ts file directly. Something like:

<script type="importmap">
    {
        "imports": {
            "@test/spa01": "http://localhost:5173/spa01/src/spa.ts"
        }
    }
</script>

IMPORTANT: Since I am using a base in Vite (see the base property in vite.config.ts), the URL has this base after the host. In reality, this is in preparation for using a proxy configuration that resolves the issue of missing assets like images. This is the topic of an upcoming article.

Conclusions

Vite is a great framework to work with, flexible and powerful. We can harness this power and flexibility to create a dually-behaved project for our single-spa and development needs.


More stuff is coming your way. We will be fixing the problem of the images we saw in the introductory article using Vite's proxy, and we'll be exploring a solution (or two, perhaps) to get our mife's CSS into play.

Once we do this, we'll explore some other things, like single-spa parcels, testing mixed technologies, especially mixing SvelteKit, Svelte and React and making sure things work, and maybe even exploring the possibility of using Svelte transitions to animate the mounting and unmounting of mifes.

Remember to bookmark the series if you are interested.

Happy coding!

12
Subscribe to my newsletter

Read articles from José Pablo Ramírez Vargas directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

José Pablo Ramírez Vargas
José Pablo Ramírez Vargas