Giter VIP home page Giter VIP logo

namecheap / ilc Goto Github PK

View Code? Open in Web Editor NEW
688.0 22.0 44.0 16.37 MB

Enterprise-ready framework for Micro Frontends composition into SPA with SSR & i18n support

Home Page: http://ilc.namecheap.technology

License: Other

JavaScript 57.06% HTML 3.42% TypeScript 39.36% Dockerfile 0.09% Shell 0.06% Smarty 0.01%
microservices frontend micro-frontends fragment-services page-composition microfrontends microframework

ilc's Introduction

Isomorphic Layout Composer logo


Latest version Demo website Actions Status Docker Pulls Docker Pulls

Isomorphic Layout Composer (ILC) - a layout service that composes a web page from fragment services. It supports client/server-based page composition.

Its competitive advantage over other solutions is an isomorphic page composition. It means that ILC assembles a page on the server-side using apps that support Server-side rendering (SSR), and after that, the page is passed on the client-side, so the client-side rendering handles all further navigation.

This approach allows to combine advantages of the Micro Frontends, SPA, and Server-side rendering approaches.

This repository also contains an example of how you can create a frontend that is composed from multiple applications that work in concert and deliver a unified experience.

Why do I need ILC?

Microservices get a lot of traction these days. They allow multiple teams to work independently, choose their technology stacks, and establish release cycles. Unfortunately, frontend development doesn't take full advantage of the microservices' benefits. The common practice for building websites is still "a monolith" - a single frontend codebase that consumes multiple APIs.

What if we introduce microservices on the frontend? It would allow frontend developers to work together with their backend counterparts on the same feature and independently deploy parts of the website ("fragments"), such as header, product, and footer. To bring microservices to the frontend, you need a layout service that can "stitch" a website out of fragments. This is where ILC comes into play.

Key features

🚀 Quick start

!!! tip "Demo website" For a quick preview, check out our demo website

To quickstart with ILC locally, follow the steps below:

  1. Clone the namecheap/ilc repository.

  2. Run npm install

    1. OPTIONAL Switch database to PostgreSQL by changing environment variable DB_CLIENT to pg in services registry_worker and registry
  3. Run docker compose up -d. Wait for the process to complete:

    [+] Running 6/6
     ⠿ Network ilc_default                  Created     0.1s
     ⠿ Container ilc-ilc-1                  Started     0.8s
     ⠿ Container ilc-mysql-1                Started     0.8s
     ⠿ Container ilc-registry-1             Started     20.0s
     ⠿ Container ilc-demo-apps-1            Started     1.4s
     ⠿ Container ilc-registry_worker-1      Started     19.8s
    
  4. Run docker compose run registry npm run seed. Wait for the process to complete:

    [+] Running 1/1
     ⠿ Container ilc-mysql-1                Recreated   3.8s
    [+] Running 1/1
     ⠿ Container ilc-mysql-1                Started     0.4s
    
    > [email protected] seed /codebase
    > knex --knexfile config/knexfile.ts seed:run
    
    Requiring external module ts-node/register
    Working directory changed to /codebase/config
    WARNING: NODE_ENV value of 'production' did not match any deployment config file names.
    WARNING: See https://github.com/lorenwest/node-config/wiki/Strict-Mode
    Ran 8 seed files
  5. Open your browser and navigate to ILC or Registry UI:

!!! tip "Additional commands" * View logs: docker compose logs -f --tail=10 * Shutdown local ILC: docker compose down

!!! note "" You can find more information about demo applications for this quick start in the namecheap/ilc-demo-apps repository.

Architecture overview

ILC Architecture overview

Repository structure

The namecheap/ilc repository consists of the following parts:

  • ilc: code of the Isomorphic Layout Composer
  • registry: app that contains configuration that ILC uses: a list of micro-fragments, routes, etc.

Further reading

🔌 Adapters

ILC relies on the adapters provided within the single-spa ecosystem to connect various frameworks. However, to ensure better integration with ILC, some of the original adapters were extended:

Notes

@portal/ prefix

ILC uses webpack (a static module bundler) to build each application for our micro-frontend approach. Webpack requires access to everything it needs to include in the bundle at build time. It means when an app imports a service (for example, planets import the fetchWithCache service), webpack tries to bundle the service into the planets bundle.

The built-in way to prevent this behavior is webpack externals. This approach works well but to avoid adding a regex to each service ILC uses the @portal prefix to instruct both webpack and developers that the import is another micro-app/service/frontend.

Code splitting

Code splitting is a complicated topic. In ILC, code splitting is even more complicated. The reason is that the webpack module format expects the loading of extra modules from the website root, which will always fail until a place from where to load extra modules is configured. In ILC, you can see an example of this approach in the demo people application.

Sockets timeout to fragments

If you experience socket timeouts during requests to fragments, plese checkout this workaround #444

ilc's People

Contributors

alxc avatar asamolion avatar b1ff avatar bandantonio avatar blackrabbit99 avatar dammned avatar dependabot[bot] avatar ifree92 avatar nightnei avatar oleh-momot avatar pokhodenko avatar stas-nc avatar stylet avatar tgarlanger avatar themcmurder avatar valikdev avatar yehor-manzhula avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ilc's Issues

Any Next.js examples?

This looks like a really cool project! Just curious if there are any example projects which use Next.js SSR? If not can you see any issues with setting up a Next.js microfrontend?

Demo Website is Erroring

http://demo.microfrontends.online/news/ is currently returning a 500 error.

Here's the relevant output in the console:
common.5ea796b17e7f7fc9ad92.css:1 Failed to load resource: the server responded with a status of 503 (Service Unavailable)

noticeError.js:20 {"type":"Error","message":"application 'navbar__at__navbar' died in status LOADING_SOURCE_CODE: Error loading http://demo.microfrontends.online:8235/navbar.js (SystemJS Error#3 https://git.io/JvFET#3)","stack":["Error: application 'navbar__at__navbar' died in status LOADING_SOURCE_CODE: Error loading http://demo.microfrontends.online:8235/navbar.js (SystemJS Error#3 https://git.io/JvFET#3)"," at HTMLScriptElement.<anonymous> (http://demo.microfrontends.online/_ilc/client.js:4:5951)"],"additionalInfo":{"type":"FRAGMENT_ERROR","appName":"@portal/navbar","slotName":"navbar","errorId":"bd5591a3-bb36-49cf-8651-f4afb361b0b4"}} Error: application 'navbar__at__navbar' died in status LOADING_SOURCE_CODE: Error loading http://demo.microfrontends.online:8235/navbar.js (SystemJS Error#3 https://git.io/JvFET#3) at HTMLScriptElement.<anonymous> (client.js:4) O @ client.js:90

Gatsby

Would it be possible to render a Gatsby app via ILC?

Vite Support

Im using Vue3 with Vite, is their any support or tutorial to make it work with ILC?

Multiples socket hang up and errors 500 related on high traffics [solved via R&D]

Hi,

We are using your app heavily and we have millions of users per month on our website.
We plugged DataDog agent into the app to analyse some datas to response quick.

We came to a point where we have so much traffic. Socket hang up where throw pretty much randomly on all renderers at differents timestamp.

After digging further more, we found out a correlation between the socket hang up and the SSR timeout from the ILC.

We found a solution.

We wanted to share that to the community. If any others people are stuck to that error.
image
The socket hang up that are popping up after the fix.. we suspect there are not correlated with that error.

So, regarding the solution, let say you have all microfrontend SSR timeout to 1000ms, modify the freeSocketTimeout on all agents to less then the 1000ms.

cfa/ilc/server/tailor/request-fragment.js

const kaAgent = new Agent({ freeSocketTimeout: 750 });
const kaAgentHttps = new HttpsAgent({ freeSocketTimeout: 750 });

Article inspired to solve the issue

Hope somes people get their fix as well! :-)

Regards

QWIK framework support ILC

Hi,

we recently stood upon the HTML-first framework for frontend that is Qwik.

We would like to know how it would be possible to integrate that with ILC.

Thanks!

ILC - Dynamic routing improvement [Proposal]

Hi!

I want to propose/suggest an improvement to the actual routing that is inside the ILC.

Currently your app support the cases :

  • Static routing (i.e. /news)
  • Simple dynamic routing (i.e. /jedi/*)

We are using your app and we are facing a challenge. What if we want /superhero/*/abilities and a template for it ?

The actual solution that we came up to solve our problem was something like this :
ilc/common/router/Router.js

[...]
#compiler = (routes) => {
        return routes.map(v => {
-            const route = this.#escapeStringRegexp(v.route);
+            let route = this.#escapeStringRegexp(v.route);
+            if (v.meta && v.meta.isSpecialPattern) {
+                route = v.route
+            }
[...]
+              v.meta.isSpecialPattern = true;
            } else {
                let basePath = route;

                if (v.route[v.route.length - 1] === '/') {

ilc/common/router/Router.spec.js

describe('when a route contain `/(.*)`', () => {
    const routeWithSpecialPattern = Object.freeze({
        route: '/test/(.*)/test',
        next: false,
        template: 'specialPatternTemplate',
        slots: {
            specialPattern: {
                appName: 'specialPattern',
                props: {
                },
                kind: 'primary',
            },
        },
        meta: {
            isSpecialPattern: true,
        },
    });

    const registryConfigWithSpecialRoute = Object.freeze({
        ...registryConfig,
        routes: [
            routeWithSpecialPattern,
            ...registryConfig.routes
        ],
    });

    it('should match request url template', () => {
        const router = new Router(registryConfigWithSpecialRoute);
        const reqUrl = '/test/launchpad/test';

        chai.expect(router.match(reqUrl)).to.be.eql({
            route: '/test/(.*)/test',
            basePath: '/test/launchpad/test',
            reqUrl,
            template: 'specialPatternTemplate',
            specialRole: null,
            slots: routeWithSpecialPattern.slots,
            meta: {
                isSpecialPattern: true,
            },
        });
    });
});

Inside the ILC Registry specify that it is a special case in the meta info. The regex pattern is important to catch all possibilities :)
image

We also noticed that Metadata field actually is a text field in SQL. [may want it as json field]


[OPTIONAL]

For our MFEs we wanted to have more infos from the context ilcSdk.

We added a utils function to breakout the infos from the request.

function generateRouteProps(currentRoute) {
    if (!currentRoute || !currentRoute.route || !currentRoute.meta || !currentRoute.meta.isSpecialPattern) {
        return
    }

    // avoid changing MFEs behaviour default routing
    const tempCurrentRoute = { ...currentRoute };

    if (currentRoute.route.includes('/*') || currentRoute.route.includes('//*')) {
        currentRoute.route = currentRoute.route.replace('//', '/');
        currentRoute.route = currentRoute.route.replace('/*', '/(.*)')
        currentRoute.basePath = currentRoute.reqUrl && currentRoute.reqUrl.split(/[?#]/)[0]
    }

    if (!currentRoute.meta.dynamicProps) {
        currentRoute = tempCurrentRoute;
        return { isSpecialRoute: true }
    }

    const urlAllPaths = currentRoute.basePath.split('/').filter(Boolean);
    const urlStaticPaths = currentRoute.route.split('/(.*)').reduce((acc, path) => {
        const paths = path.split('/').filter(Boolean)
        return [...acc, ...paths]
    }, []);

    const urlDynamicPaths = urlAllPaths.filter(item => !urlStaticPaths.includes(item));
    const urlDynamicProps = currentRoute.meta.dynamicProps.reduce((acc, dynamicPart, index) => {
        return {
            ...acc,
            [dynamicPart]: urlDynamicPaths[index],
        };
    }, {});

    currentRoute = tempCurrentRoute;
    return { isSpecialRoute: true, urlDynamicProps }
}

describe('generateRouteProps', () => {
        const isSpecial = { isSpecialRoute: true }

        const specialRoute = {
            basePath: '/static-first-part/dynamic/static-second-part',
            reqUrl: '/static-first-part/dynamic/static-second-part',
            routeId: 10,
            route: '/static-first-part/(.*)/static-second-part',
            template: 'template',
            specialRole: null,
            meta: { isSpecialPattern: true, dynamicProps: ['dynamicProp'] },
            slots: {
                slot: { appName: 'appName', props: {}, kind: null }
            }
        }

        const expectedProps = {
            ...isSpecial,
            urlDynamicProps: { dynamicProp: "dynamic" },
        }

        const dynamicProps = specialRoute.meta.dynamicProps
        const routeDynamicMiddleAndEnd = "/static-first-part/(.*)/static-second-part/(.*)"

        test('given no route, should return nothing', () => {
            expect(generateRouteProps()).not.toBeDefined()
        });

        test('given a route that is not special, should return nothing', () => {
            expect(generateRouteProps({ meta: {} })).not.toBeDefined()
        });

        test('given a route that is dynamic in the end but dont have dynamicProps, should return isSpecial', () => {
            expect(generateRouteProps({ route: '/news//*', meta: { isSpecialPattern: true } })).toStrictEqual(isSpecial)
        });

        test('given a route that is dynamic in the middle but dont have dynamicProps, should return isSpecial', () => {
            delete specialRoute.meta.dynamicProps

            expect(generateRouteProps(specialRoute)).toStrictEqual(isSpecial)
            specialRoute.meta.dynamicProps = dynamicProps
        });

        test('given a route that is dynamic in the middle and also in the end but dont have dynamicProps, should return isSpecial', () => {
            specialRoute.route = routeDynamicMiddleAndEnd
            delete specialRoute.meta.dynamicProps

            expect(generateRouteProps(specialRoute)).toStrictEqual(isSpecial)
            specialRoute.meta.dynamicProps = dynamicProps
        });


        test('given a route that is dynamic in the end and have dynamicProps, should return routeProps correctly', () => {
            specialRoute.route = '/static//*'
            specialRoute.basePath = '/static/dynamic'
            specialRoute.reqUrl = '/static/dynamic?test=true'

            expect(generateRouteProps(specialRoute)).toStrictEqual(expectedProps)
        });

        test('given a route that is dynamic in the middle, should return routeProps correctly', () => {
            expect(generateRouteProps(specialRoute)).toStrictEqual(expectedProps)
        });

        test('given a route that is dynamic in the middle and start with 2 subpath, should return routeProps correctly', () => {
            specialRoute.route = '/partone/parttwo/(.*)/partthree'
            specialRoute.basePath = '/partone/parttwo/dynamic/partthree'
            specialRoute.reqUrl = '/partone/parttwo/dynamic/partthree?test=true'
            expect(generateRouteProps(specialRoute)).toStrictEqual(expectedProps)
        });

        test('given a route that is dynamic in the middle and end with 2 subpath, should return routeProps correctly', () => {
            specialRoute.route = '/partone/(.*)/parttwo/partthree'
            specialRoute.basePath = '/partone/dynamic/parttwo/partthree'
            specialRoute.reqUrl = '/partone/dynamic/parttwo/partthree?test=true'
            expect(generateRouteProps(specialRoute)).toStrictEqual(expectedProps)
        });

        test('given a route that is dynamic and have multiples before and after subpath, should return routeProps correctly', () => {
            specialRoute.route = '/partone/parttwo/partthree/(.*)/partfour/partfive/partsix'
            specialRoute.basePath = '/partone/parttwo/partthree/dynamic/partfour/partfive/partsix'
            specialRoute.reqUrl = '/partone/parttwo/partthree/dynamic/partfour/partfive/partsix?test=true'
            expect(generateRouteProps(specialRoute)).toStrictEqual(expectedProps)
        });

        test('given a route that is special and more complicated, should return routeProps correctly', () => {
            specialRoute.route = routeDynamicMiddleAndEnd
            specialRoute.basePath = '/static-first-part/dynamic-part-one/static-second-part/dynamic-part-two'
            specialRoute.reqUrl = '/static-first-part/dynamic-part-one/static-second-part/dynamic-part-two'
            specialRoute.meta.dynamicProps = ['dynamicProp', 'dynamicProp2']
            expectedProps.urlDynamicProps = { dynamicProp: "dynamic-part-one", dynamicProp2: "dynamic-part-two" }

            expect(generateRouteProps(specialRoute)).toStrictEqual(expectedProps)
        });
    });

ilc/server/tailor/request-fragment.js

[...]
 const currRoute = request.router.getRoute();

 const routeProps = generateRouteProps(currRoute);

[...]
props: { ...wrapperConf.props, ...routeProps },
[...]
props: { ...wrapperConf.props, ...routeProps },
[...]

function makeFragmentUrl({
  route,
  baseUrl,
  appId,
  props,
  ignoreBasePath = false,
}) {
  const url = new URL(baseUrl);

    if (route.basePath && route.basePath === route.reqUrl && props.isSpecialRoute && !props.urlDynamicProps) {
      // Legacy case we want default basePath i.e. without last path 
      route.basePath = route.basePath.split(route.basePath.split('/').pop())[0]
    
  }

Thanks :)

routerProps loop to 500

Hi, I've an issue within following scenario:

I downloaded you demo setup and it runs pretty smooth.
Then I added one more application to the demo apps (based on the nuxt framework). I also added a new route for this.
But when I try to access it via browser, I run into an error 500 and in the ilc container log I see a kind of loop.

It begins like this: /nuxt/?routerProps=eyJiYXNlUGF0aCI6Ii9udXh0IiwicmVxVXJsIjoiL251eHQiLCJmcmFnbWVudE5hbWUiOiJudXh0X19hdF9fYm9keSJ9&appProps=e30%3D
and with every loop the routerProps argument becomes longer and longer until the loop breaks.

At the end it looks like: /nuxt/?routerProps=eyJiYXNlUGF0aCI6Ii9udXh0IiwicmVxVXJsIjoiL251eHQvP3JvdXRlclByb3BzPWV5SmlZWE5sVUdGMGFDSTZJaTl1ZFhoMElpd2ljbVZ4VlhKc0lqb2lMMjUxZUhRdlAzSnZkWFJsY2xCeWIzQnpQV1Y1U21sWldFNXNWVWRHTUdGRFNUWkphVGwxWkZob01FbHBkMmxqYlZaNFZsaEtjMGxxYjJsTU1qVXhaVWhSZGxBelNuWmtXRkpzWTJ4Q2VXSXpRbnBRVjFZMVUyMXNXbGRGTlhOV1ZXUkhUVWRHUkZOVVdrcGhWR3d4V2tab2IwMUZiSEJrTW14cVlsWmFORlpzYUV0ak1HeHhZakpzVFUxcVZYaGFWV2hTWkd4QmVsTnVXbXRYUmtweldUSjRRMlZYU1hwUmJuQlJWakZaTVZVeU1YTlhiR1JHVGxoT1YxWlhVa2hVVldSSFVrWk9WVmRyY0doV1IzZDRWMnRhYjJJd01VWmlTRUpyVFcxNGNWbHNXbUZPUmxwellVVjBhazFIZUhoWmFrcHpWRlV4Y1ZaWWFHRldWMmhUV2tkNFFtVnNUblZYYlhSWVVtdHdlbGRVU2pSUk1sWllVMWh3VW1KdVFsSldha1phVFZaVmVVMVlUbGhpUjFKSFZHeG9UMVl4V2xoVmEyaFZWbGRTU0ZWcldrOVdWbVJ5WTBkb1YxSXpaRFJXTW5SaFlqSkpkMDFWV21sVFJVcHlWRmN4TkdOV2JITlhiVVpQVW14d2VsbFZWakJoYXpGSVpVaG9XbUZyY0hwV1JsVjRZMVphV1dGSFJsZFdNbWhVVjJ0a05GRnRWbk5VYmxaWVlsaFNXVlZ0ZEhkbGJHUlZVMnBTVWsxc1dsbFZNV2gzVlcxS2RWRnNTbGRoYTFwaFZGWmFWbVZWTVZsVWJHaHBVakZLU0ZaSGVHOVVNVmw0VjJ4b1ZtRXlhRlpXYkdSVFUwWldjbGRyT1ZkV2JWSjVXVEJrYjFZeFNYcGFSRkpYVFc1U2FGbHFTa3BrTURGV1YyMXNWRkpWY0hsV1J...

And an error from tailor Error: Request fragment error. statusCode: 431; statusMessage: Request Header Fields Too Large

If I connect to the ilc container and try to fetch the nuxt SRR URL with wget, the response looks fine.

Do you have any idea why I'm running into such a loop?

Some Settings:

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.