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
andentryFileNames
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 invite.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!