Making your Vite Project Standalone and single-spa Simultaneously
Tags: vite, singlespa, svelte, tutorial
Published: 2023-05-01 20:53:07

> Heredia, Costa Rica
> 
> Published: 2023-05-01

2023-08-04 UPDATE: A **Vite** plug-in is now available. Refer to [this article](https://webjose.hashnode.dev/vite-plugin-single-spa-vite-projects-made-single-spa-compatible-with-one-plug-in). Furthermore, help us improve Vite for single-spa by [upvoting this discussion](https://github.com/vitejs/vite/discussions/13927).

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:

```typescript
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:

```typescript
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:

```json
  "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:

```bash
> 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:

```bash
> 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:

```xml
<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](https://webjose.hashnode.dev/series/single-spa-and-svelte) if you are interested.

Happy coding!