From 42202f824431a74a28f4ce8347c2c19dad537dbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ramirez=20Vargas=2C=20Jos=C3=A9=20Pablo?= Date: Fri, 10 Jul 2026 09:41:08 -0600 Subject: [PATCH] chore: Remove dependabot and update README with CollageJS --- .github/dependabot.yml | 6 - README.md | 517 ++--------------------------------------- 2 files changed, 16 insertions(+), 507 deletions(-) delete mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index d1f0d08..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,6 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "npm" - directory: "/" - schedule: - interval: "daily" diff --git a/README.md b/README.md index af85389..3dd2f52 100644 --- a/README.md +++ b/README.md @@ -1,511 +1,26 @@ # vite-plugin-single-spa -> Vite plug-in to convert Vite-based projects to single-spa root or micro-frontend applications. +As you may or may not know, `single-spa` creator [Joel/Jolyn Denning passed away in 2025](https://github.com/single-spa/single-spa/issues/1361#issuecomment-3991093643) ([obituary](https://www.broomheadfuneralhome.com/obituary/joel-denning)). This, *in my very own personal opinion*, also means the death of `single-spa`. -This Vite plug-in is an **opinionated** way of making Vite-based front-end projects work with -[single-spa](https://github.com/single-spa/single-spa). +It's been a fun ride, and it is now time to move on. -> [!IMPORTANT] -> Due to false accusations raised against me by the author of `single-spa` Jolyn Denning, I will discontinue this -> plug-in. As of today, 2025-04-13, this plug-in will only accept bug reports and will only get bug fixes. No new -> features will be developed for this plug-in. -> -> For reference, this is what Jolyn wrote at the `single-spa` Slack channel. -> ![Jolyn Denning's Message](docs/Jolyn%20Denning.png) -> -> Whatever problems Jolyn may be going through right now do not invalidate `single-spa`, at least as of the current -> version. I will not, however, continue the journey as `single-spa` progresses (if it progresses at all). -> -> I will instead be publishing a new library for micro-frontends since I still have the need at work. I will be -> pushing `single-spa` out of my work projects because I fear the long term stability of `single-spa`. +![CollageJS](https://raw.githubusercontent.com/collagejs/core/HEAD/src/logos/collagejs-512.svg) -## Quickstart +## You're Invited to CollageJS! -> **NOTE**: This document assumes the use of TypeScript Vite projects. However, all of this applies to JavaScript -projects as well. Just know that any file name with extension `.ts` probably exists as a `.js` file. +I would like to let you all know that I have created an alternative, inspired by the concept of `single-spa` parcels. I have named it *CollageJS*. -Install the NPM package as a development dependency. +This is the main repository: [CollageJS Repository][https://github.com/collagejs/collagejs] -```bash -npm i -D vite-plugin-single-spa -``` +I strongly believe that `single-spa` users will feel it very familiar, especially the ones using `vite-plugin-single-spa` (this plug-in). Its documentation site is not yet up and running but should go live soon. -> In reality, what is installed as development dependency is a matter of how you build your final product. For -example, if you want to use `npm` with `--omit=dev` to build the project, then you'll need all packages used by Vite's -build command to have been installed as regular dependencies. It is up to you how you end up installing the package -(dev or regular). +Head over to *CollageJS* and give it a try. While more focused and smaller, it does pack very interesting new features, such as: -Now, in `vite.config.ts`, import the `vitePluginSingleSpa` function from it. It is the default export but it is also -a named export: - -```typescript -// Either works. -import vitePluginSingleSpa from 'vite-plugin-single-spa'; -import { vitePluginSingleSpa } from 'vite-plugin-single-spa'; -``` - -This is the plug-in factory function. Create the plug-in by invoking the function. Pass the plug-in options as -argument to the function call. The following is the content of `vite.config.ts` of a **Vite + Svelte** project -created with `npm create vite@latest`. - -```typescript -import { svelte } from '@sveltejs/vite-plugin-svelte'; -import { defineConfig } from 'vite'; -import vitePluginSingleSpa from 'vite-plugin-single-spa'; - -// https://vitejs.dev/config/ -export default defineConfig({ - plugins: [svelte(), vitePluginSingleSpa({ /* options go here */})], -}); -``` - -The options passed to the plug-in factory function determine the type of project (root or micro-frontend). For -micro-frontend projects, the server port is required, while for root projects the type is required. - -Additionally for micro-frontend projects, the file `src/spa.ts|js|jsx|tsx` must be created. This file becomes the -main export of the project and should export the `single-spa` lifecycle functions. - -## single-spa Root Projects - -The `single-spa` *root project* (referred to as *root config* within the `single-spa` documentation) is the project -that loads all other micro-frontends and the one that has the `single-spa` package installed, and the one that -typically calls `registerApplication()` and `start()`. The `single-spa` developers advertise as a best practice to -make a root project that uses no framework. In other words, that your root project be devoided of all user interface -elements. This is a view I don't share, and this plug-in is capable of making a suitable Vite + XXX root project. In -the end, the choice is yours. - -### Root Project Options - -The plug-in options available to root projects are dictated by the following TypeScript type: - -```typescript -export type SingleSpaRootPluginOptions = { - type: 'root'; - importMaps?: { - type?: 'importmap' | 'overridable-importmap' | 'systemjs-importmap' | 'importmap-shim'; - dev?: string | string[]; - build?: string | string[]; - }; - imo?: boolean | string | (() => string); - imoUi?: boolean | 'full' | 'popup' | 'list' | { - variant?: boolean | 'full' | 'popup' | 'list'; - buttonPos?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'; - localStorageKey?: string; - }; -}; -``` - -The `type` property is mandatory for root projects and must be the string `'root'`, as seen. The other options -pertain to the use of import maps and the `import-map-overrides` package. Long story short: Vite, while in -development mode, inserts the script `@vite/client` as first child in the HTML page's `` element, and this makes -the import maps non-functional, at least for the native `importmap` type. The solution: Make this plug-in add both -the import map script and the `import-map-overrides` package as first children of the `` HTML element. Why did -I tell the story? Just so you know that it is impossible to use `import-map-overrides` with Vite "by hand" in `serve` -mode. - -The `imo` option is used to control the inclusion of `import-map-overrides`. Set it to `false` to exclude it; set to -`true` to include its latest version from **JSDelivr**. However, production deployments should never let unknown -versions of packages to be loaded without prior testing, so it really isn't good practice to just say "include the -latest version". Instead, specify the desired package version as a string. - -> Check for [releases of import-map-overrides](https://github.com/single-spa/import-map-overrides/releases). - -```typescript -vitePluginSingleSpa({ - type: 'root', - imo: '4.2.0' -}) -``` - -Just like the case of the latest version, this will also use the **JSDelivr** network. - -> **IMPORTANT**: Even if you request `import-map-overrides` to be included, it won't be included if no import maps -are present. - -If you wish to change the source of the package from **JSDelivr** to something else, then provide a function that -returns the package's URL. - -```typescript -vitePluginSingleSpa({ - type: 'root', - imo: () => `https://my.cdn.example.com/import-map-overrides@4.2.0` -}) -``` - -The `imoUi` property controls the inclusion of the `import-map-overrides` user interface. The property's default -value is `true`, which is equivalent to `full`. The values `full`, `popup` and `list` refer to the type of user -interface. Refer to the package's documentation in case this is confusing. - -By default, the user interface will be configured to appear in the bottom right corner and become visible on the -presence of the `imo-ui` local storage variable. If any of this is inconvenient, specify the value of `imoUi` as an -object with the `variant`, `buttonPos` and `localStorageKey` properties set to your liking. - -We finally reach the `importMaps` section of the options. Use this section to specify the import map type and file -names. The default behavior is to automatically import maps from the file `src/importMap.dev.json` whenever Vite runs -in `serve` mode (when you run the project with `npm run dev`), or the file `src/importMap.json` whenever vite runs in -`build` mode (when you run `npm run build`). Note, however, that if you have no need to have different import maps, -then you can omit `src/importMap.dev.json` and just create `src/importMap.json`. - -Usually, the development import map would look like this: - -```json -{ - "imports": { - "@learnSspa/spa01": "http://localhost:4101/src/spa.ts", - "@learnSspa/spa02": "http://localhost:4102/src/spa.ts" - } -} -``` - -This is because, while using `npm run dev`, no bundling takes place, so we directly reference the module in the `/src` -folder. - -Building the micro-frontend, on the other hand, produces a `spa.js` bundle at the root of the `dist` folder, so the -import map for builds would look like this: - -```json -{ - "imports": { - "@learnSspa/spa01": "http://localhost:4101/spa.js", - "@learnSspa/spa02": "http://localhost:4102/spa.js" - } -} -``` - -Of course, that would be if you were building to **test locally** the build using `npm run preview`. Whenever -building for deployment, it will be more like this: - -```json -{ - "imports": { - "@learnSspa/spa01": "/spa01-prefix/spa.js", - "@learnSspa/spa02": "/spa02-prefix/spa.js" - } -} -``` - -The above is a popular Kubernetes deployment option: Set the K8s ingress up so that requests that start with the -micro-frontend prefix are routed to the pod that serves that micro-frontend. In the end the final look of the import -map is up to you and your deployment setup. - -Now, what if you want or need to specify a different file name for your import maps? No problem. Use -`importMaps.dev` to specify the serve-time import map file; use `importMaps.buid` to specify the build-time import map -file. - -As seen in the TypeScript definition, you can specify the type of import map you want. The four choices are the four -possible options for the `import-map-overrides` package, and if not specified, it will default to -`overridable-importmap`. Once again, I deviate from `single-spa`'s recommendation of using `SystemJS` as the module -import solution. Long story short: Native import maps, except for one bug, seem to work just fine, and I am pro -minimizing package dependencies in projects. Furthermore, if you read the -[Vite ecosystem page](https://single-spa.js.org/docs/ecosystem-vite/), you'll see that `SystemJS` is recommended -because, and I quote: - -> since browser support for Import Maps is still pending - -This is no longer the case as seen in the [caniuse](https://caniuse.com/?search=import%20map) website. - -If you're confused about all this import map type thing, read all about this import map topic in the -[import-map-overrides](https://github.com/single-spa/import-map-overrides) home page. - -**UPDATE**: It seems that `single-spa` v7 is aiming towards working with native modules and native import maps, as -several peripheral tools have already received updates to default away from `SystemJS` (`create-single-spa`, for -instance). What will the final shape be? I don't know. We'll have to wait and see when the time comes, and if this -plug-in continues to be required at all. - -#### Using More Import Map Files - -> Since **v0.3.0** - -In `single-spa` applications, it is common to need shared modules, and it so happens to be very practical to list them -as import map entries. For example, one could have something like this: - -```json -{ - "imports": { - "vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js", - "react": "https://cdn.jsdelivr.net/npm/react@18.2.0/+esm", - "react-dom": "https://cdn.jsdelivr.net/npm/react-dom@18.2.0/+esm", - "@learnSspa/mifeA": "http://localhost:4101/src/spa.tsx" - } -} -``` - -Because those shared entries (`vue`, `react`, `react-dom`) need to be specified for both Vite modes (`serve` and -`build`), the most practical thing is to have a third import map file that is used in both scenarios. To support this -kind of import map construction, the properties `importMaps.dev` and `importMaps.build` can also accept an array of -string values to specify multiple file names. If you opt for this option, there is no "default file by omission" and -you must specify all your import map files explicitly. - -Create 3 import map JSON files: `src/importMap.json`, `src/importMap.dev.json` and `src/importMap.shared.json`. Now -specify the import map files as an array: - -```typescript -vitePluginSingleSpa({ - type: 'root', - importMaps: { - dev: ['src/importMap.dev.json', 'src/importMap.shared.json'], - build: ['src/importMap.json', 'src/importMap.shared.json'], - } -}) -``` - -This is of course a mere suggestion. Feel free to arrange your import maps any way you feel is best. Two, three or -50 import map files. Create import map files until your heart is content. - -## single-spa Micro-Frontend Projects - -A micro-frontend project in `single-spa` is referred to as a micro-frontend or a *parcel*. Micro-frontends are the -ultimate goal: Pieces of user interface living as entirely separate projects. - -### Micro-Frontend Project Options - -The plug-in options available to micro-frontend projects are dictated by the following TypeScript type: - -```typescript -export type SingleSpaMifePluginOptions = { - type?: 'mife'; - serverPort: number; - spaEntryPoints?: string | string[]; - projectId?: string; - assetFileNames?: string; - cssStrategy?: 'singleMife' | 'multiMife' | 'none'; -}; -``` - -The `type` property may be omitted, but if specified, it must be the string `'mife'`. - -The `serverPort` property is relevant to the correct configuration of `single-spa` and therefore, it is something this -package must know, or things will become harder to configure between the micro-frontends and the root project. - -Specifically, micro-frontend projects are linked to the root project by means of its import map. During development, -the import map will usually point to the micro-frontend's entry file with a full URL similar to -`http://localhost:4444/src/spa.ts`, where `4444` is the server's port number. It is very difficult to imagine a -`single-spa` project that works with dynamic ports. - -> Since **v0.4.0** - -The `spaEntryPoints` property has a default value of `src/spa.ts` and is used to specify all the files that export -`single-spa` modules (modules that export `bootstrap`, `mount` and `unmount`). Most of the time, there is only one -such file (the one that exports the micro-frontend), but if the micro-frontend exposes parcels, those are specified -here as well. If you need to specify more than one file, use an array of strings. - -The default file name is certainly handy, but it is opinionated: It is a TypeScript file (the plug-in's author's -preference). If your file name differs, even if only by file extension, use `spaEntryPoints` to specify it. - -> Since **v0.6.0** - -The `assetFileNames` option works as documented in [Rollup's documentation](https://rollupjs.org/configuration-options/#output-assetfilenames), -with one exception: It can only be a string. Functions may not be passed at this time. If this is something that -people need, open an issue in the project's repository and request it. - -> `projectId`: Since **v0.2.0**; `cssStrategy`: Since **v0.4.0** - -At the bottom we see `projectId` and `cssStrategy`. These are necessary for CSS mounting and unmounting. In a -`single-spa`-enabled application, there will be (potentially) many micro-frontends coming and going in and out of the -application's page. The project ID is used to name the CSS bundles during the micro-frontend building process. Then, -at runtime, this identifier is used to discriminate among all the CSS link elements in the page to properly mount and -unmount them. If not provided, the plug-in will use the project's name from `package.json`. Make sure the project -has a name, or provide a value for this property. - -> **NOTE**: The `projectId` value will be trimmed to 20 characters if a longer value is found/specified. - -The `cssStrategy` has to do with what you, the developer, want to do with the exported micro-frontend/parcel. Read -the following section to understand this fully. - -## Mounting and Unmounting CSS - -> Since **v0.4.0** - -> **IMPORTANT**: CSS lifecycle logic has been completely replaced by a new algorithm as of **v0.4.0**. The previous -> `cssLifecycle` object is no longer available, and in its stead you can import the `cssLifecycleFactory` function. - -This plug-in provides an extension module that provides ready-to-use `single-spa` lifecycle functions for the bundled -CSS. It is very simple to use. The following is an example of the spa entry file `src/spa.tsx` of a *Vite + React* -project created with `npm create vite@latest`: - -```typescript -import React from 'react'; -import ReactDOMClient from 'react-dom/client'; -// @ts-expect-error -import singleSpaReact from 'single-spa-react'; -import App from './App'; -import { cssLifecycleFactory } from 'vite-plugin-single-spa/ex'; - -const lc = singleSpaReact({ - React, - ReactDOMClient, - rootComponent: App, - errorBoundary(err: any, _info: any, _props: any) { - return
Error: {err}
- } -}); -// IMPORTANT: Because the file is named spa.tsx, the string 'spa' -// must be passed to the call to cssLifecycleFactory. -const cssLc = cssLifecycleFactory('spa', /* optional factory options */); -export const bootstrap = [cssLc.bootstrap, lc.bootstrap]; -export const mount = [cssLc.mount, lc.mount]; -export const unmount = [cssLc.unmount, lc.unmount]; -``` - -The lifecycle factory algorithm needs to know which entry point it should be creating the lifecycle object for, so it -is very important that the name passed to the factory coincides *exactly* with the file name (minus the extension). - -The object created by the factory (in the example, stored in the `cssLc` variable), **must** be used for every -exported/created `single-spa` lifecycle object that comes out of the same file (module). - -**NOTE**: The optional factory options control the behavior of the FOUC-prevention feature and what is logged to the -console. - -### Console Logging - -> Since **v0.7.0** - -The option named `logger` in the options parameter of the `cssLifecycleFactory()` function can be used to control what -gets logged to the browser's console from the CSS mounting algorithms. Its default value is `true` and means "log to -the console". If set to `false`, then nothing is logged to the console. - -There is a third possiblity: To provide a custom logger object. The logger only needs to implement the 4 console -methods `debug()`, `info()`, `warn()` and `error()`. Use this approach if you need fine-grained control over what -gets logged to the console, or to even do fancier logging, such as posting the console entries to a remote structured -log storage, like a **Seq** server. - -### CSS Strategy - -The `cssLifecycleFactory` function covers both CSS strategies (`singleMife` and `multiMife`). - -This new CSS mounting algorithm supports multiple SPA entry points, with single or multiple exports per entry point, -and mixing micro-frontends with parcels in the same project should also be possible. If you intend to either mount -multiple instances of the same parcel or micro-frontend, or export more than one `single-spa` lifecycle object, you -**must** set the options' `cssStrategy` property to `multiMife`. - -If, however, your project simply exports a single micro-frontend or a single parcel that doesn't expect to be -instantiated more than once simultaneously, then `cssStrategy` can be set to `singleMife`, which is the default value -for this property. - -> **IMPORTANT**: Most likely, the default value in v1.0.0 will be `multiMife`. It is actually feasible that the -`singleMife` strategy might disappear completely, since nobody seems to be using `single-spa`'s `unloadApplication` -function. - -#### Deactivating CSS Mounting - -> Since **v0.6.0** - -The CSS mounting algorithm in this package relies on some naming convention around bundled CSS files. If you plan to -roll out your own CSS mounting algorithm, you may set `cssStrategy` to `none`. This will effectively deactivate the -CSS bundle renaming that takes place during building. This also deactivates `cssLifecycleFactory`. - -> Currently investigating if usage of `cssLifecycleFactory` can be detected in order to emit a warning. - -### FOUC Prevention - -> Since **v0.7.0** - -The object created with the `cssLifecycleFactory()` function prevents FOUC (flash of unstyled content) by delaying the -mounting operation until all the CSS files that the plug-in handles (the CSS bundles created by Vite) are loaded and -ready to be used by the browser. This is implemented by subscribing to the LINK element's `load` and `error` events, -and only returning if the CSS loads, an error occurs, or the specified time elapses (a timeout event). - -The following options, which are set when calling `cssLifecycleFactory()`, control the behavior of this feature: - -```typescript -export type CssLifecycleFactoryOptions = { - logger?: boolean | ILogger; - loadTimeout?: number; - failOnTimeout?: boolean; - failOnError?: boolean; -}; -``` - -> Refer to **Console Logging** previously in this document to see about the `logger` option. - -Set the `loadTimeout` property to the amount of time to wait for the `load` or `error` events to fire before taking -action. Which action? That depends on the other two properties. - -When set to `true`, `failOnTimeout` throws an error that aborts the micro-frontend mounting process, but when set to -`false`, only a warning is logged to the console. - -The last property, `failOnError`, works identically to `failOnTimeout`, except that it applies to the times where the -CSS fetching process fails. - -The default values are, respectively, 1500ms, `false` and `false`. - -#### FOUC Prevention Known Facts & Issues - -1. `single-spa`, by default, emits minified error message #31 if the mounting process takes 3000ms or more. Avoid -setting `loadTimeout` to 3000ms or higher. -2. The CSS mounting algorithm dismounts CSS by disabling CSS LINK elements, and then, if necessary, these are -re-enabled. Even though a network call might be generated in the Network tab of the developer tools at the time of -re-enabling, a `load` event is never fired, meaning that FOUC prevention can only work on the very first time the CSS -is mounted. - -### Important Notes About Generating Multiple Instances of a Parcel or Micro-Frontend - -While an incredible library, `single-spa` was not designed to support the mounting of more than one instance of a -micro-frontend simultaneously, but some of us like this idea. This CSS mounting algorithm pretend to support as much -as possible this scenario, as well as the mounting of multiple instances of the same parcel. However, `single-spa` is -not really prepared for this, so do this at your own risk. - -Speaking of which, there is a bug in the `singleSpaSvelte()` function exported by the `single-spa-svelte` NPM package -that prevents exporting the way `single-spa` recommends. Detailed information can be found in -[this blog post](https://webjose.hashnode.dev/single-spa-parcels-and-vite-plugin-single-spa) or in the -[logged issue at GitHub](https://github.com/single-spa/single-spa-svelte/issues/28). The blog post and the issue were -written prior to the existence of `vite-plugin-single-spa` **v0.4.0**, so make the necessary changes in the proposed -workaround. - -While I haven't tested every helper for every framework/library, there is the possibility that the bug found in the -`single-spa-svelte` package may exist in others, in which case the factory workaround may work for those too. - -### Why You Must Choose the CSS Strategy - -The new algorithm is robust and seems to work just fine under the conditions set by the `singleMife` strategy, but -there is a price to pay: This new algorithm is incompatible with the idea of using `single-spa`'s `unloadApplication` -for the purposes of HMR. In order to allow people to keep the possibility of using `unloadApplication`, the user can -choose the `singleMife` strategy to keep this HMR ability. - -> **IMPORTANT, AGAIN**: Nobody seems to care about `unloadApplication`, so CSS strategies might disappear in v1.0.0. - -## Vite Environment Information - -> Since **v0.2.0** - -The same extension module that provides CSS lifecycle functions also provides basic information about the Vite -environment. Specifically, it exports the `viteEnv` object which is described as: - -```typescript -export const viteEnv: { - serving: boolean, - built: boolean, - mode: string -}; -``` - -The `serving` property will be true if the project is being served by Vite in `serve` mode. The `built` property will -be true if the project is being run after being built. The `mode` property is just the mode used when Vite was run, -and by default is the string `development` for `serve` mode, and `production` for builds. - -> Unlike `cssLifecycleFactory` which is only useful to micro-frontend projects, `viteEnv` is available to root -projects as well. - -To make use of it for whatever purpose, import it: - -```typescript -import { viteEnv } from 'vite-plugin-single-spa/ex'; -``` - -## In-Depth Coverage On the Topic - -This plug-in was born as the result of investigating `single-spa`, **Svelte** and the general use of Vite-based -projects. This investigation was documented in the form of a blog -[in hashnode](https://webjose.hashnode.dev/series/single-spa-and-svelte). Feel free to read it in order to fully -understand how this plug-in works and the reasons behind its behavior. - -## Roadmap - -- [x] Multiple import map files per Vite command (to support shared dependencies marked `external` in Vite) -- [x] Single-SPA parcels -- [x] Multiple `single-spa` entry points -- [x] Logging options -- [x] Asset file name pattern -- [x] CSS `load` event to prevent FOUC -- [x] Logger object for cssLifecycleFactory to allow full control of what renders in the console -- [x] Closed to further enhancements. +1. Auto-externalization of anything in the import map while running Vite's dev server (`npm run dev`) +2. Static imports from micro-frontends (named *CollageJS* pieces) is allowed; no more dynamic `import()` everywhere! +3. Ability to mount *CollageJS* pieces in shadow DOM +4. Ability to mount multiple copies of the same *CollageJS* piece +5. All written in TypeScript from day 1 +6. A modern version of `import-map-overrides` named `@collagejs/imo` +7. The ability to package *CollageJS* pieces in NPM packages (`@collagejs/imo`'s user interface is a *CollageJS* piece) +8. More to come, hopefully including SSR support \ No newline at end of file