Skip to content

single-spa

@infobloxopen/devedge-ufe-single-spa adapts the MicrofrontendModule contract to single-spa lifecycles, and provides createShell, the shell-owns-session registration mechanism. The shell instantiates a SessionProvider once, gates registration on await session.getToken(), and threads the session to every micro-frontend as a single-spa custom prop.

A caller’s customProps.session can never override the shell session; the shell supplies it.

Import from @infobloxopen/devedge-ufe-single-spa.

Exports

SymbolKindSummary
SingleSpaLifecyclesinterfaceThe bootstrap, mount, unmount functions single-spa expects.
toSingleSpaLifecyclesfunctionAdapts a MicrofrontendModule to SingleSpaLifecycles.
ShellAppinterfaceA micro-frontend the shell registers: name, activeWhen, load.
ShellOptionsinterfaceOptions for createShell: the session and the apps.
ShellinterfaceThe shell handle; registerAll() mounts the apps.
createShellfunctionBuilds a shell that owns the session and gates registration.

Adapting a module

toSingleSpaLifecycles turns a MicrofrontendModule into the lifecycle object single-spa registers.

interface SingleSpaLifecycles {
  bootstrap: LifeCycleFn<HostProps>;
  mount: LifeCycleFn<HostProps>;
  unmount: LifeCycleFn<HostProps>;
}

function toSingleSpaLifecycles(
  m: MicrofrontendModule, props: HostProps,
): SingleSpaLifecycles;

The shell

createShell builds a shell around a shell-owned SessionProvider. Each ShellApp declares when it is active and how to load its lifecycles. registerAll awaits session.getToken() before it registers any app, which proves the shell holds a session before a micro-frontend mounts.

interface ShellApp {
  name: string;
  activeWhen:
    | string
    | ((location: Location) => boolean)
    | Array<string | ((location: Location) => boolean)>;
  load: () => Promise<SingleSpaLifecycles> | SingleSpaLifecycles;
}

interface ShellOptions {
  session: SessionProvider;
  apps: ShellApp[];
  customProps?: Record<string, unknown>;
}

interface Shell {
  registerAll(): Promise<void>;
}

function createShell(opts: ShellOptions): Shell;
registerAll gates on the token. It calls await session.getToken() before registering the apps, so a micro-frontend never mounts without a session behind it. See The public seam.

Activating and loading an app

Each ShellApp declares two things: when it is active (activeWhen) and how to load its lifecycles (load). The full-stack example activates on a hash route and loads through the native browser import map.

activeWhen takes a predicate over location. The example activates the app on a hash route so the edge never sees the uFE surface — single-spa reads location.hash, not the path.

function activeOnHash(route: string): (location: Location) => boolean {
  return (location) =>
    location.hash === route || location.hash.startsWith(`${route}/`);
}

load returns the app’s single-spa lifecycles. The example runs a native dynamic import against a bare specifier that the shell’s <script type="importmap"> resolves to a CDN bundle. The webpackIgnore comment keeps the specifier out of the build so the browser’s ESM loader resolves it at runtime.

function loadMfe(spec: string): Promise<SingleSpaLifecycles> {
  return import(/* webpackIgnore: true */ spec) as Promise<SingleSpaLifecycles>;
}

const shell = createShell({
  session,
  apps: [
    { name: 'notes-ufe', activeWhen: activeOnHash('#notes'), load: () => loadMfe('notes-ufe') },
  ],
});
await shell.registerAll();

The SDK does not use SystemJS. Angular’s dev server emits ESM bundles, which the native import map loads directly. For the full loading model — the import map, the CDN, and hash routing — see How micro-frontends load.