# Create an host application

Let's begin by creating the application that will serve as the entry point for our modular application and host the application modules.

# Install the packages

Create a new application (we'll refer to ours as host), then open a terminal at the root of the new solution and install the following packages:

pnpm add -D @workleap/swc-configs @workleap/browserslist-config @squide/firefly-webpack-configs webpack webpack-dev-server webpack-cli @swc/core @swc/helpers browserslist postcss typescript @types/react @types/react-dom
pnpm add @squide/firefly react react-dom react-router-dom @tanstack/react-query
yarn add -D @workleap/swc-configs @workleap/browserslist-config @squide/firefly-webpack-configs webpack webpack-dev-server webpack-cli @swc/core @swc/helpers browserslist postcss typescript @types/react @types/react-dom
yarn add @squide/firefly react react-dom react-router-dom @tanstack/react-query
npm install -D @workleap/swc-configs @workleap/browserslist-config @squide/firefly-webpack-configs webpack webpack-dev-server webpack-cli @swc/core @swc/helpers browserslist postcss typescript @types/react @types/react-dom
npm install @squide/firefly react react-dom react-router-dom @tanstack/react-query

# Setup the application

First, create the following files:

host
├── public
├──── index.html
├── src
├──── App.tsx
├──── RootLayout.tsx
├──── HomePage.tsx
├──── bootstrap.tsx
├──── index.ts
├──── register.tsx
├── .browserslistrc
├── swc.dev.js
├── swc.build.js
├── webpack.dev.js
├── webpack.build.js
├── package.json

Then, ensure that you are developing your application using ESM syntax by specifying type: module in your package.json file:

host/package.json
{
    "type": "module"
}

Finally, use a dynamic import to add an async boundary:

host/src/index.ts
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore doesn't support file extension.
import("./bootstrap");

// TS1208: 'index.tsx' cannot be compiled under '--isolatedModules' because it is considered a global script file. Add an import, export, or an
// empty 'export {}' statement to make it a module.
export {};

To learn more about this async boundary and the bootstrap.tsx file, read the following article.

# Module registration

Next, to register the modules, instanciate a shell FireflyRuntime instance and register the remote module with the bootstrap function (the configuration of the remote module will be covered in the next section):

host/src/bootstrap.tsx
import { createRoot } from "react-dom/client";
import { ConsoleLogger, RuntimeContext, FireflyRuntime, bootstrap, type RemoteDefinition } from "@squide/firefly";
import { App } from "./App.tsx";

// Define the remote modules.
const Remotes: RemoteDefinition[] = [
    { name: "remote1" }
];

// Create the shell runtime.
const runtime = new FireflyRuntime({
    loggers: [x => new ConsoleLogger(x)]
});

// Register the remote module.
await bootstrap(runtime, {
    remotes: Remotes
});

const root = createRoot(document.getElementById("root")!);

root.render(
    <RuntimeContext.Provider value={runtime}>
        <App />
    </RuntimeContext.Provider>
);

Then, render the AppRouter component to define a React Router browser instance configured with the registered routes:

host/src/App.tsx
import { AppRouter } from "@squide/firefly";
import { RouterProvider, createBrowserRouter } from "react-router-dom";

export function App() {
    return (
        <AppRouter waitForMsw={false}>
            {({ rootRoute, registeredRoutes, routerProviderProps }) => {
                return (
                    <RouterProvider
                        router={createBrowserRouter([
                            {
                                element: rootRoute,
                                children: registeredRoutes
                            }
                        ])}
                        {...routerProviderProps}
                    />
                );
            }}
        </AppRouter>
    );
}

# Navigation items

Next, create a layout component to render the navigation items. In many applications, multiple pages often share a common layout that includes elements such as a navigation bar, a user profile menu, and a main content section. In a React Router application, this shared layout is commonly referred to as a RootLayout:

host/src/RootLayout.tsx
import { Suspense } from "react";
import { Link, Outlet } from "react-router-dom";
import { 
    useNavigationItems,
    useRenderedNavigationItems,
    isNavigationLink,
    type RenderItemFunction,
    type RenderSectionFunction
} from "@squide/firefly";

const renderItem: RenderItemFunction = (item, key) => {
    // To keep thing simple, this sample doesn't support nested navigation items.
    // For an example including support for nested navigation items, have a look at
    // https://gsoft-inc.github.io/wl-squide/reference/routing/userenderednavigationitems/
    if (!isNavigationLink(item)) {
        return null;
    }

    const { label, linkProps, additionalProps } = item;

    return (
        <li key={key}>
            <Link {...linkProps} {...additionalProps}>
                {label}
            </Link>
        </li>
    );
};

const renderSection: RenderSectionFunction = (elements, key) => {
    return (
        <ul key={key}>
            {elements}
        </ul>
    );
};

export function RootLayout() {
    // Retrieve the navigation items registered by the remote modules.
    const navigationItems = useNavigationItems();

    // Transform the navigation items into React elements.
    const navigationElements = useRenderedNavigationItems(navigationItems, renderItem, renderSection);

    return (
        <>
            <nav>{navigationElements}</nav>
            <Suspense fallback={<div>Loading...</div>}>
                <Outlet />
            </Suspense>
        </>
    );
}

The RootLayout component created in the previous sample will serves as the default layout for the homepage as well as for every page (route) registered by a module that are not nested under a parent route with either the parentPath or the parentId option.

# Homepage

Next, create the HomePage component that will serve as the homepage:

host/src/HomePage.tsx
export function HomePage() {
    return (
        <div>Hello from the Home page!</div>
    );
}

Then, add a local module at the root of the host application to register the homepage:

host/src/register.tsx
import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";
import { HomePage } from "./HomePage.tsx";

export const registerHost: ModuleRegisterFunction<FireflyRuntime> = runtime => {
    runtime.registerRoute({
        index: true,
        element: <HomePage />
    });
};

And an hoisted route to render the RootLayout with the PublicRoutes and ProtectedRoutes placeholders:

host/src/register.tsx
import { PublicRoutes, ProtectedRoutes, type ModuleRegisterFunction, type FireflyRuntime } from "@squide/firefly";
import { HomePage } from "./HomePage.tsx";
import { RootLayout } from "./RootLayout.tsx";

export const registerHost: ModuleRegisterFunction<FireflyRuntime> = runtime => {
    runtime.registerRoute({
        // Pathless route to declare a root layout.
        element: <RootLayout />,
        children: [
            // Placeholders indicating where non hoisted or nested public and protected routes will be rendered.
            PublicRoutes,
            ProtectedRoutes
        ]
    }, {
        hoist: true
    });

    runtime.registerRoute({
        index: true,
        element: <HomePage />
    });
};

Finally, update the bootstrapping code to register the newly created local module:

host/src/bootstrap.tsx
import { createRoot } from "react-dom/client";
import { ConsoleLogger, RuntimeContext, FireflyRuntime, bootstrap, type RemoteDefinition } from "@squide/firefly";
import { App } from "./App.tsx";
import { registerHost } from "./register.tsx";

// Define the remote modules.
const Remotes: RemoteDefinition[] = [
    { name: "remote1" }
];

// Create the shell runtime.
const runtime = new FireflyRuntime({
    loggers: [x => new ConsoleLogger(x)]
});

// Register the modules.
await bootstrap(runtime, {
    localModules: [registerHost],
    remotes: Remotes
});

const root = createRoot(document.getElementById("root")!);

root.render(
    <RuntimeContext.Provider value={runtime}>
        <App />
    </RuntimeContext.Provider>
);

# Not found page (404)

Now, let's ensure that users who enter a wrong URL end up somewhere by registering a custom no-match route. First, create the NotFoundPage component, which will serve as the page for handling not found routes:

host/src/NotFoundPage.tsx
export function NotFoundPage() {
    return (
        <div>Not found! Please try another page.</div>
    );
}

Then, register the newly created component as the * route:

host/src/register.tsx
import { PublicRoutes, ProtectedRoutes, type ModuleRegisterFunction, type FireflyRuntime } from "@squide/firefly";
import { HomePage } from "./HomePage.tsx";
import { NotFoundPage } from "./NotFoundPage.tsx";
import { RootLayout } from "./RootLayout.tsx";

export const registerHost: ModuleRegisterFunction<FireflyRuntime> = runtime => {
    runtime.registerRoute({
        element: <RootLayout />,
        children: [
            // Placeholders indicating where non hoisted or nested public and protected routes will be rendered.
            PublicRoutes,
            ProtectedRoutes
        ]
    }, {
        hoist: true
    });

    runtime.registerPublicRoute({
        path: "*",
        element: <NotFoundPage />
    });

    runtime.registerRoute({
        index: true,
        element: <HomePage />
    });
};

# Configure webpack

First, open the public/index.html file created at the beginning of this guide and copy/paste the following HtmlWebpackPlugin template:

host/public/index.html
<!DOCTYPE html>
<html>
    <head>
    </head>
    <body>
        <div id="root"></div>
    </body>
</html>

Then, open the .browserslist file and copy/paste the following content:

host/.browserslistrc
extends @workleap/browserslist-config

# Development configuration

To configure webpack for a development environment, first open the swc.dev.js file and copy/paste the following code:

host/swc.dev.js
// @ts-check

import { browserslistToSwc, defineDevConfig } from "@workleap/swc-configs";

const targets = browserslistToSwc();

export const swcConfig = defineDevConfig(targets);

Then, open the webpack.dev.js file and use the defineDevHostConfig function to configure webpack:

host/webpack.dev.js
// @ts-check

import { defineDevHostConfig } from "@squide/firefly-webpack-configs";
import { swcConfig } from "./swc.dev.js";

/**
 * @typedef {import("@squide/firefly-webpack-configs").RemoteDefinition[]}
 */
const Remotes = [
    { name: "remote1", url: "http://localhost:8081" }
];

export default defineDevHostConfig(swcConfig, 8080, Remotes);

If you are having issues with the wepack configuration that are not related to module federation, refer to the @workleap/webpack-configs documentation.

# Build configuration

To configure webpack for a build environment, first open the swc.build.js file and copy/paste the following code:

host/swc.build.js
// @ts-check

import { browserslistToSwc, defineBuildConfig } from "@workleap/swc-configs";

const targets = browserslistToSwc();

export const swcConfig = defineBuildConfig(targets);

Then, open the webpack.build.js file and use the defineBuildHostConfig function to configure webpack:

host/webpack.build.js
// @ts-check

import { defineBuildHostConfig } from "@squide/firefly-webpack-configs";
import { swcConfig } from "./swc.build.js";

/**
 * @typedef {import("@squide/firefly-webpack-configs").RemoteDefinition[]}
 */
const Remotes = [
    { name: "remote1", url: "http://localhost:8081" }
];

export default defineBuildHostConfig(swcConfig, Remotes);

If you are having issues with the wepack configuration that are not related to module federation, refer to the @workleap/webpack-configs documentation.

# Add CLI scripts

To initiate the development server, add the following script to the application package.json file:

host/package.json
{
    "dev": "webpack serve --config webpack.dev.js"
}

To build the application, add the following script to the application package.json file:

host/package.json
{
    "build": "webpack --config webpack.build.js"
}

# Try it 🚀

Start the application in a development environment using the dev script. You should see the homepage. Even if the remote module application is not yet available, the host application will gracefully (and ignore the remote module).

# Troubleshoot issues

If you are experiencing issues with this guide:

  • Open the DevTools console. You'll find a log entry for each registration that occurs and error messages if something went wrong:
    • [squide] Found 4 local modules to register.
    • [squide] 1/4 Registering local module.
    • [squide] 1/4 Local module registration completed.
    • [squide] Found 1 remote module to register.
    • [squide] 1/1 Loading module "register" of "remote1".
    • [squide] 1/1 Registering module "register" of remote "remote1".
    • [squide] 1/1 The registration of the remote "remote1" is completed.
  • Refer to a working example on GitHub.
  • Refer to the troubleshooting page.