Git Product home page Git Product logo

Comments (19)

MilanKovacic avatar MilanKovacic commented on September 27, 2024 1

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.

MilanKovacic avatar MilanKovacic commented on September 27, 2024 1

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.

MilanKovacic avatar MilanKovacic commented on September 27, 2024 1

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?

image image

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.

AshwinAsp avatar AshwinAsp commented on September 27, 2024

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.

barryspearce avatar barryspearce commented on September 27, 2024

@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.

AshwinAsp avatar AshwinAsp commented on September 27, 2024

@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.

barryspearce avatar barryspearce commented on September 27, 2024

@AshwinAsp

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.

barryspearce avatar barryspearce commented on September 27, 2024

@MilanKovacic Thx for the plugin reference! Much appreciated!

from single-spa.

AshwinAsp avatar AshwinAsp commented on September 27, 2024

@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?

image

from single-spa.

barryspearce avatar barryspearce commented on September 27, 2024

@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):

  1. Place vite.svg in the src dir.
  2. place vite.svg in an assets dir below src and include as ./assets/vite.svg
  3. 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.

AshwinAsp avatar AshwinAsp commented on September 27, 2024

from single-spa.

AshwinAsp avatar AshwinAsp commented on September 27, 2024

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?

image image

from single-spa.

barryspearce avatar barryspearce commented on September 27, 2024

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.

barryspearce avatar barryspearce commented on September 27, 2024

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.

barryspearce avatar barryspearce commented on September 27, 2024

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.

MilanKovacic avatar MilanKovacic commented on September 27, 2024

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.

barryspearce avatar barryspearce commented on September 27, 2024

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.

AshwinAsp avatar AshwinAsp commented on September 27, 2024

@barryspearce @MilanKovacic

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?

Screenshot 2023-11-09 at 1 59 35 PM

from single-spa.

MilanKovacic avatar MilanKovacic commented on September 27, 2024

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)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.