Comments (19)
Hi, single-spa uses webpack by default (if you created the projects using create-single-spa) — tailwind configuration can be added like with any other webpack project.
Please also check https://single-spa.js.org/docs/ecosystem-css/#single-spa-css.
There are various ways project utilize tailwind, so it would be helpful to know what exactly you are trying to do — just have access to tailwind classes in all projects, creating a styleguide, etc?
In regards to Vite, I utilize the same approach of CSS injection into JS, and it works fine.
@AshwinAsp in your Vite usage example, check if the correct port is specified, and with Vite (during development) you would typically directly access the entry point, for example "@orgName/spa": "//localhost:5000/src/spa.tsx"
.
from single-spa.
I have a sample Vite repository here: https://github.com/MilanKovacic/single-spa-vite
You can also use my Vite plugin that supports externalizing dependencies in development: https://github.com/MilanKovacic/vite-plugin-externalize-dependencies
from single-spa.
You can see the import having trouble even under public folder. Does static assets from MFE not get loaded when accessed through root? Or all the assets have to live in both MFE and root?
No, the assets don't have to be present in both the root-config, and microfrontends, as that would create the coupling.
You can use the server.origin property during development. During build, (when "lib" option is used), assets will be inlined. This is because all single-spa modules, aside from root-config act as libraries as opposed to applications.
I don't think running an SPA in dev and the MFE in dev is going to be an easy thing to solve, but more to the point, why would we want to do that? The SPA and the MFEs should be independent. So we develop one or the other rather than both simultaneously.
This is already possible with single-spa. Recommendation for development is to use import-map-overrides to access the deployed version of the application. In large projects there is often a need to run/develop multiple modules locally, for example when a change spans multiple modules (root-config + microfrontend(s), utility module + microfrontend(s)), etc. See https://single-spa.js.org/docs/recommended-setup/#local-development
We should be able to develop it without running the SPA the whole time, where it needs to interact with other services we can use the integration server services.
Using import-map-overrides supports this flow. You can either access the deployed version of the application, and override the URLs of modules you want to develop to point to local development server, or run the (root-config + modules in development) locally.
from single-spa.
Really need a proper response for this. Not able to import/add base styles. CSS is not loaded into the project at all and using single-spa-css throws errors irrelevant error. The adoption of single-spa depends on tailwind or some css to be used for any given MFE application. If such a basic functionality is not sorted out, not sure how we can go for this.
Minimum reproducible link:
https://github.com/AshwinAsp/mfe-single-spa
from single-spa.
@AshwinAsp
I use vite as a build. Vite provides for chunking css and auto-loading as required, but it doesn't do this for the top level css. So if a separate top level CSS is created it isn't pulled in with single-spa. I did look at single-spa-css but this then create more issues in terms of timing etc.
The solution that worked for me (at least for my proof of concept) was to inject the CSS into the JS so I only have to import the JS. With vite this is done by:
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
and then adding the plugins:
plugins: [
vue(),
cssInjectedByJsPlugin({
relativeCSSInjection: true,
suppressUnusedCssWarning: true,
jsAssetsFilterFunction: (entry) => entry.name === "prod",
})
],
Maybe this is an option for you?
In terms of deployment and loading of assets everything in the MFE is then pulled in by the initial JS import.
I have a proof of concept using Vue, Svelte, and React all using Tailwind CSS and this is working well for me. Although depending on CSS size I may be inclined to import the CSS separately to leverage caching.
Just some thoughts that may help.
from single-spa.
@barryspearce Im very much in favor to using Vite but it didnt integrate smoothly with the non vite MFE root project. Can you add a link of how you configure it? This is how I tried.
Vite.config.js in one repo
import react from '@vitejs/plugin-react'
import vitePluginSingleSpa from 'vite-plugin-single-spa';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), vitePluginSingleSpa({
serverPort: 4101,
spaEntryPoint: 'src/spa.tsx'
})],
});
Non vite MFE index.ejs in different repo
{
"imports": {
"@orgName/spa": "//localhost:5000/spa.js"
}
}
from single-spa.
I notice you are using React in your root-config (shell application). I would suggest removing all frameworks from your shell (recommended setup). This is the dependencies for my shell:
"dependencies": {
"single-spa": "^5.9.5",
"single-spa-layout": "^2.2.0"
},
"devDependencies": {
"typescript": "^5.0.2",
"vite": "^4.4.5"
},
To these you would add any utility modules of course. In my set up I pull in 3 utility modules which I then inject into the MFEs using customProps. Keep the shell (root config) really lean.
I generate my root config using vite: npm create vite@latest
My root configs are Vanilla JS, with Typescript. I add tailwind to projects as needed (root config/MFEs) using the standard Tailwind Instructions for adding to vite/framework of choice.
I do not use vite-single-spa plugin at all. My vite.config.ts is pretty bare for the shell application apart from one fix which is needed if you register appplications manually rather than using layout. This is my entire proof of concept vite.config.ts:
// Workaround for externalImports via importmap in dev.
// https://github.com/vitejs/vite/issues/6393
//
import { defineConfig } from 'vite'
// https://vitejs.dev/config/
const externalImports = [
'vue',
'nav',
'footer',
'app1',
'app2',
'app3',
'widget1',
'widget2'
]
export default defineConfig({
plugins: [viteIgnoreStaticImport(externalImports)],
build: {
rollupOptions: {
external: externalImports,
input: {
full: '/src/main.ts'
}
}
}
});
function viteIgnoreStaticImport(importKeys) {
return {
name: "vite-plugin-ignore-static-import",
enforce: "pre",
// 1. insert to optimizeDeps.exclude to prevent pre-transform
config(config) {
config.optimizeDeps = {
...(config.optimizeDeps ?? {}),
exclude: [...(config.optimizeDeps?.exclude ?? []), ...importKeys],
};
},
// 2. push a plugin to rewrite the 'vite:import-analysis' prefix
configResolved(resolvedConfig) {
const VALID_ID_PREFIX = `/@id/`;
const reg = new RegExp(
`${VALID_ID_PREFIX}(${importKeys.join("|")})`,
"g"
);
resolvedConfig.plugins.push({
name: "vite-plugin-ignore-static-import-replace-idprefix",
transform: (code) =>
reg.test(code) ? code.replace(reg, (m, s1) => s1) : code,
});
},
// 3. rewrite the id before 'vite:resolve' plugin transform to 'node_modules/...'
resolveId: (id) => {
if (importKeys.includes(id)) {
return { id, external: true };
}
},
};
}
As @MilanKovacic suggests, during development my import map also refers to raw source "shell": "/src/main.ts",
which vite processes in the dev server.
from single-spa.
@MilanKovacic Thx for the plugin reference! Much appreciated!
from single-spa.
@MilanKovacic Im still having trouble integrating static assets with the repo you provided.
In microfrontend/src/App.tsx
import viteImg from './vite.svg';
function App() {
return <div>
<h1>Hello from Microfrontend!</h1>
<img src={viteImg} />
</div>;
}
export default App
And add an image in mircofrontend/src
The image doesnt get loaded when running root, because its trying to get it from localhost:3000/src/*.svg which is not correct. Can you help with loading static assets with Vite?
from single-spa.
@AshwinAsp
Vite is doing the right thing - you told it ./vite.svg and ./ refers to src because that is where your tsx is located.
I therefore assume you do not have vite.svg in the src directory.
You have three options here (at least! the list of possible things to do is quite long):
- Place vite.svg in the src dir.
- place vite.svg in an assets dir below src and include as ./assets/vite.svg
- Place vite.svg in the public dir and then include as /vite.svg
Option 1 and 2 will build to place both .js and .svg into the dist/assets dir and will be hashed. If you do not need the svg to be hashed then option 3 will result in the .js going to dist/assets and vite.svg going into dist. More info about this on Vite's static asset handling here.
You can use build.assetsDir to control whether the assets are placed in a sub dir in dist (or in the top level, the same as the public assets, by setting build.assetsDir to ''. Vite config reference: build.assetsDir
from single-spa.
from single-spa.
You can see the import having trouble even under public folder. Does static assets from MFE not get loaded when accessed through root? Or all the assets have to live in both MFE and root?
from single-spa.
I have cloned the repo and are doing what you are doing.
The problem is caused by running two dev servers and expecting one to deliver assets to the other. If we example the source code we can see what was inserted by the mcirofrontend is <img src="/src/vite.svg">
. Which it will because its view of the world is it is serving locally. But you are trying to access it from a "non-local" server (port 3000 not 3001).
I don't think running an SPA in dev and the MFE in dev is going to be an easy thing to solve, but more to the point, why would we want to do that? The SPA and the MFEs should be independent. So we develop one or the other rather than both simultaneously. Thus if running the MFE in dev we should be using production built assets from the SPA (and other MFEs), and if working on the SPA then the MFE assets should be production built. A well-designed architecture should mean that this split is natural and easy. If the SPA and MFE become co-dependent then its probably an indicator that refactoring is needed.
One way of achieving this in the development environment is to have an integration server. This server provides the production built assets of the SPA and other MFEs. With our MFE we develop it in isolation. We should be able to develop it without running the SPA the whole time, where it needs to interact with other services we can use the integration server services. How this works precisely depends on how you structure service discovery and your import maps.
To produce a more compact environment for exploring we obviously don't want to go so far as having an integration server and full infrastructure. So here is what I do. (I am currently working this up to a point where I can publish it on a repo with full docs - but that will be a few more days before I am ready to do that)...anyway...here is my setup for a PoC.
I have an SPA. For a compact PoC the production assets of the MFEs are stored in the /public/assets of the SPA. I copy them in by hand - these would be on the integration server but without that we need somewhere to serve them from.
I also store my import map here. I am using external import maps via es-module-shims (which I prefer to SystemJS for a couple of reasons).
So when developing the SPA we can run up a dev server, all MFE assets & utility module assets are served by that server and everything just works, including assets needed by the SPA (my SPAs are designed to run under a corporate CMS so I have no assets for the SPA itself. The SPA is used to provide structure, loading and routing only).
Then I have my MFEs. My MFEs have different config and main.ts between dev and production. This is because my SPA will inject suppliers for publish/subscribe, comms, and configuration into my MFEs. Rather than having to run my MFE through the SPA all the time, I create this and inject by hand in my main-dev.ts. This is useful as it allows mocking for testing. By using these the aim is I can develop either in complete isolation (via mocking) or against an integration server. This is how the normal npm run dev works for my stuff.
Then I have npm run dev-spa. In my MFE I have another html file. It is in fact just a copy of the SPA html file. I could use the vite transform HTML to load it from the SPA if I wanted but that would meaning having to run an SPA dev server whenever I need it as the index.html for the SPA is not used in any production environment. What differs here is my vite configuration proxy separates out the local MFE assets from the remote ones, and it patches the SPA importmap on the fly (when proxied) to ensure that the local MFE assets are served locally rather than from the server the SPA is running on. Now that probably doesn't mean much talking about it - so here is the chunk of vite config that does this: (remember I am still working on this)
server: {
open: '/full.html',
proxy: {
'/import-map.json': {
target: 'http://127.0.0.1:5174/assets/im',
changeOrigin: false,
selfHandleResponse: true,
configure: (proxy) => {
proxy.on("proxyRes", (proxyRes, req, res) => {
const chunks = [];
proxyRes.on("data", (chunk) => chunks.push(chunk));
proxyRes.on("end", () => {
const buffer = Buffer.concat(chunks);
const encoding = proxyRes.headers["content-encoding"];
// if (encoding === "gzip" || encoding === "deflate") {
// zlib.unzip(buffer, (err, buffer) => {
// if (!err) {
let remoteBody = buffer.toString();
console.log('REMOTE: ' + remoteBody)
const modifiedBody = remoteBody
.replace(/\/assets\/footer\/assets\/.*\.js/, '/src/main-spa.ts')
.replace(/\/src\/main\.ts/, 'http://127.0.0.1:5174/assets/shell/shell-a3a1a9cc.js')
res.write(modifiedBody);
res.end();
console.log('MODIFIED: ' + modifiedBody)
// } else {
// console.error(err);
// }
// });
// }
});
});
},
},
'/assets': {
target: 'http://127.0.0.1:5174'
}
},
}
So we can see that for general assets it uses the SPA server (integration server), and we have also patched the importmap on the fly for the locally developed MFE. When running npm run dev-spa this is actually what instigates the local main-spa.ts. When using the SPA from the MFE I use production built shell SPA. Thus the shell SPA itself has been added to the SPA assets folder after having been built for production - so I patch the importmap from the SPA dev server to use the production asset.
I then have a third config for npm run build.
So to summarise, I have an SPA, with 7 MFEs, and 3 libs (one external pub/sub, the comms & config libs written by me). [just to mention the config lib which is injected provides pooled configuration to MFEs and obtains its config from the MSA config server]
When developing the SPA I can run it in a dev mode against production assets, purely for the development of the SPA. Assets are served correctly for both the SPA and the MFEs (they behave as they would in production). The SPA has two build configs: npm run dev, npm run build.
When developing the MFEs there are three configs: npm run dev for local development independent of the SPA, npm run dev-spa for running within the SPA using an integration server (or an SPA dev server) to provide the required assets, and npm run build for the production build.
Here local assets are served as expected in normal dev and in SPA mode external assets are also served correctly.
So whereas you are running the SPA to view the MFE, I am focused on the MFE and it pulls in the SPA environment if needed.
Sorry for the long response - it is quite involved to get all this playing nicely - but worth it. I hope that at least provides some explanation of what is going wrong for you and some ideas for potential solutions. The way I architect the front-ends/SPA and dev/integration split may not be 100% applicable for your scenario, but should provide at least some way forward for you. Hope that helps!
from single-spa.
I don't think running an SPA in dev and the MFE in dev is going to be an easy thing to solve, but more to the point, why would we want to do that? The SPA and the MFEs should be independent. So we develop one or the other rather than both simultaneously.
This is already possible with single-spa. Recommendation for development is to use import-map-overrides to access the deployed version of the application.
Yes, but that comes with an expectation of how import-maps are used. They must be delivered internal to the HTML. That is a system constraint inherent in the recommended setup. However, there are solutions that do not include that constraint.
In large projects there is often a need to run/develop multiple modules locally, for example when a change spans multiple modules (root-config + microfrontend(s), utility module + microfrontend(s)), etc. See https://single-spa.js.org/docs/recommended-setup/#local-development
Yes, I am able to do the same.
Whilst some changes can span multiple modules this is of course an indicator that the MFE/MS split may need refactoring. One of the over-arching aims of MSA is reduce this. If it happens a lot then the deployment of one module becomes tied to another and what you have is monolith in everything except name. So whilst it needs to be possible I would suggest that needing to run a full infrastructure as the standard method to develop and test an MFE results in the negatives of MSA but not gaining the full benefits.
In my architecture I use external import maps. This has the advantage that the HTML loading the SPA never has to change. If the import map is loaded into the HTML the HTML delivered now needs to change in some way. When that HTML is delivered in a CMS that causes additional deployment issues. However by using a tiny amount of code to initiate the SPA + an external import map the CMS code never changes. All application updates are external to the CMS with no integrations required - the external import map does all the heavy lifting.
One of the biggest issues is importmap-overrides does not support external import maps.
Thus I achieve this same functionality a different way. I can simply alter the import map - it is after all completely external, thus I can substitute entirely different import maps and thus importmap overrides functionality is achieved in a different way - and with less dependencies.
We should be able to develop it without running the SPA the whole time, where it needs to interact with other services we can use the integration server services.
Using import-map-overrides supports this flow. You can either access the deployed version of the application, and override the URLs of modules you want to develop to point to local development server, or run the (root-config + modules in development) locally.
Yes - and so does my approach. The importmap-overrides approach as the default way of working however does not actively discourage close coupling - it does the opposite.
I have all the same functionality and DX but with a different approach - and one less external dependency.
Just different ways/approaches of achieving the same thing each has its pros/cons.
from single-spa.
I think it is also worth pointing out my MFE do not build as libs. In my package.json type = module. Everything I do is based on ES modules and I am using es-module-shims. Perhaps this also explains some of the differences in approach?
from single-spa.
Yes, but that comes with an expectation of how import-maps are used. They must be delivered internal to the HTML. That is a system constraint inherent in the recommended setup. However, there are solutions that do not include that constraint.
es-module-shims in shim mode, and SystemJS support external import maps.
Whilst some changes can span multiple modules this is of course an indicator that the MFE/MS split may need refactoring. One of the over-arching aims of MSA is reduce this. If it happens a lot then the deployment of one module becomes tied to another and what you have is monolith in everything except name. So whilst it needs to be possible I would suggest that needing to run a full infrastructure as the standard method to develop and test an MFE results in the negatives of MSA but not gaining the full benefits.
Often times, changes (especially infrastructural) require running multiple modules to test the impact.
One of the biggest issues is importmap-overrides does not support external import maps.
It is supported, see https://github.com/single-spa/import-map-overrides/blob/main/docs/configuration.md
I think it is also worth pointing out my MFE do not build as libs. In my package.json type = module. Everything I do is based on ES modules and I am using es-module-shims. Perhaps this also explains some of the differences in approach?
There are a lot of ways to utilize single-spa. From my experience, I have found the approach of using import-map-overrides the most optimal, both in terms of organizational, and development practices.
from single-spa.
Yes, but that comes with an expectation of how import-maps are used. They must be delivered internal to the HTML. That is a system constraint inherent in the recommended setup. However, there are solutions that do not include that constraint.
es-module-shims in shim mode, and SystemJS support external import maps.
Yes, I use es-module-shims in shim mode.
Whilst some changes can span multiple modules this is of course an indicator that the MFE/MS split may need refactoring. One of the over-arching aims of MSA is reduce this. If it happens a lot then the deployment of one module becomes tied to another and what you have is monolith in everything except name. So whilst it needs to be possible I would suggest that needing to run a full infrastructure as the standard method to develop and test an MFE results in the negatives of MSA but not gaining the full benefits.
Often times, changes (especially infrastructural) require running multiple modules to test the impact.
True, there are times when it is necessary.
One of the biggest issues is importmap-overrides does not support external import maps.
It is supported, see https://github.com/single-spa/import-map-overrides/blob/main/docs/configuration.md
Aha. The first time I tried to integrate it, it wasn't happy and complained about external maps not being support - hence my comments. Perhaps this was an ordering issue, I thought I had configured it correctly, but I must have mis-configured it. However, I now have it working.
Thank you for making me look at this again.
There are a lot of ways to utilize single-spa. From my experience, I have found the approach of using import-map-overrides the most optimal, both in terms of organizational, and development practices.
I will certainly spend more time with it!
from single-spa.
I don't think running an SPA in dev and the MFE in dev is going to be an easy thing to solve, but more to the point, why would we want to do that? The SPA and the MFEs should be independent. So we develop one or the other rather than both simultaneously. Thus if running the MFE in dev we should be using production built assets from the SPA (and other MFEs), and if working on the SPA then the MFE assets should be production built. A well-designed architecture should mean that this split is natural and easy. If the SPA and MFE become co-dependent then its probably an indicator that refactoring is needed.
Well I was following the recommended dev approach in single-spa and expected it to work in your repo as well. I agree the development of SPA and MFE should be independent. But if i try to run only the microfrontend repo it throws the following error. Thats why I went with the single-spa approach to run your repo. Is this a misconfig or am I accessing it wrong?
from single-spa.
Closing the issue since discussion diverged from the original question. Feel free to open new issues with specific problems.
from single-spa.
Related Issues (20)
- Logging SystemJS Error#3 HOT 1
- Single Spa and Angular v 17 HOT 1
- Single Spa + Vue3 vue-router triggers route guard multiple times HOT 1
- Add a option to ignore route change HOT 1
- window.removeEventListener does not work in sometimes case HOT 3
- Multiple versions of @mui/x-data-grid on the same project. HOT 2
- single-spa refreshing page causes 'page not found' error HOT 2
- Why does this project feel abandoned? HOT 8
- Overlapping Micro-Apps When Mounting App1 and App2 on Single-Spa Routes HOT 2
- Deploying single spa with 2 angular applications HOT 1
- Registerapplication is not a function error HOT 1
- Unable to Import Variables from Shared Utility Module HOT 1
- How to load libs from npm instead of from cdn.jsdelivr.net in index.ejs HOT 4
- SAFARI issue with SSO between single-spa apps HOT 2
- Vuetify is not working with my SPA when I use it as shared dependency HOT 3
- How to render/load a Single Spa angular app within another Single Spa angular app. HOT 1
- Caching Options and Best Practices HOT 1
- Tailwind CSS not working. HOT 1
- Single SPA with code splitting based in react-router HOT 1
- Can't import svg as React component in Webpack 5 with Single SPA HOT 2
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from single-spa.