Cross-framework remotes
Module Federation federates JS modules, not React components — the transport is already framework-agnostic. What used to tie a JORVEL remote to React was the contract: a remote exposed a React component and the host rendered it into its React tree. JORVEL now also speaks a framework-neutral mount contract, so a host can embed a remote built with any framework.
The contract
A remote exposes a mount(ctx) / unmount(el) module (@jorvel/mount). The host hands it a DOM node + context; the remote bootstraps whatever framework it wants into that node and returns a disposer. The host never imports the remote's framework.
export interface JorvelMountModule {
mount(ctx: JorvelMountContext): void | JorvelUnmount | Promise<void | JorvelUnmount>;
unmount?(el: HTMLElement): void;
}
export interface JorvelMountContext {
el: HTMLElement; // the node the remote owns
subpath: string; // path relative to the mount prefix
basePath: string; // the prefix the host mounted under
params: Record<string, string>; // route params matched by the host
props?: Record<string, unknown>; // host-passed props
signal?: AbortSignal; // aborted on unmount / navigation
}React remotes
React remotes get the contract for free with @jorvel/adapter-react. Wrap your root and the remote is mountable by any host:
import { defineReactRemote } from '@jorvel/adapter-react';
import { RemoteApp } from '@jorvel/runtime';
import { pages } from './jorvel.routes.js';
export default defineReactRemote(({ subpath }) => (
<RemoteApp subpath={subpath} pages={pages} />
));Back-compat preserved
export default a React component keep working unchanged —RemoteOutlet renders a component default directly and only takes the mount path when it detects a mount module. Nothing to migrate.Other frameworks
Implement the contract directly — here a Vue remote:
import type { JorvelMountModule } from '@jorvel/mount';
import { createApp } from 'vue';
import Root from './Root.vue';
const remote: JorvelMountModule = {
mount({ el, subpath }) {
const app = createApp(Root, { subpath });
app.mount(el);
return () => app.unmount();
},
};
export default remote;Solid, Svelte (customElement), Angular (ApplicationRef), or plain DOM follow the same shape.
On the host
Nothing changes. RemoteOutlet auto-detects: a mount module is bridged into a DOM node it owns (with the neutral context), a React-component default is rendered inline. A plain-DOM host can mount without React using mountRemoteModule:
import { asMountModule, mountRemoteModule } from '@jorvel/mount';
const mod = asMountModule(await importRemote());
if (mod) {
const dispose = mountRemoteModule(mod, { el, subpath, basePath, params });
// …on navigation away:
dispose();
}Embed anywhere: Web Component mode
For a host that isn't a JORVEL app — plain HTML, a CMS, or a page owned by another framework — wrap any mount module as a custom element. It reads routing context from attributes and drives the mount lifecycle from connect/disconnect:
import { defineCustomElement } from '@jorvel/mount';
import remote from './remote.js'; // any JorvelMountModule
defineCustomElement('jorvel-pricing', remote); // light DOM (host CSS applies)
// defineCustomElement('jorvel-pricing', remote, { shadow: true }); // isolated shadow DOM<!-- Now usable in ANY page, no framework required -->
<jorvel-pricing subpath="/plans" basepath="/pricing" params='{"tier":"pro"}'></jorvel-pricing>Extra attributes can be forwarded as props via { observedAttributes: ['data-theme'] } (kebab-case arrives camelCased). The element re-mounts when subpath changes and tears down on removal.
Cross-framework SSR
Server-render each framework's fragment, stitch them into one document, and hydrate each on the client — all through the neutral @jorvel/mount/ssr primitives.
1. Expose a server module. Alongside the client mount module, a remote exposes a renderToString (React does this via @jorvel/adapter-react/server):
import { defineReactServerRemote } from '@jorvel/adapter-react/server';
import Root from './Root';
export default defineReactServerRemote(Root, { getState: (ctx) => ({ id: ctx.params.id }) });2. Render + stitch on the server.Run each remote's renderer and compose the fragments into a document:
import { renderFragment, composeFragments } from '@jorvel/mount/ssr';
const ctx = { subpath: '/plans', basePath: '/pricing', params: {} };
const fragments = await Promise.all([
renderFragment('pricing', pricingServer, ctx), // Vue
renderFragment('reports', reportsServer, ctx), // Angular
]);
const { html } = composeFragments(fragments, { template: shellHtml }); // {{head}} {{body}} {{state}}Each fragment is wrapped in a marked mount point (data-jorvel-fragment="pricing") and its hydration state is serialized (XSS-safe) into a single script tag.
3. Hydrate on the client. One call finds every fragment, loads its remote, and re-mounts with hydrate: true — reusing the server DOM and seeding initialState:
import { hydrateFragments } from '@jorvel/mount/ssr';
await hydrateFragments({
pricing: () => import('pricing/App'),
reports: () => import('reports/App'),
});Every framework ships a server entry — @jorvel/adapter-<fw>/server with define<Fw>ServerRemote (React via react-dom/server, Vue via @vue/server-renderer, Solid via solid-js/web, Svelte via svelte/server, Angular via @angular/platform-server). On the client, adapters honor ctx.hydrate: React hydrateRoot, Vue createSSRApp, Solid hydrate, Svelte hydrate. Angular renders server-side and mounts on the client (app-level provideClientHydration for full DOM reuse).
Trade-offs
- Shared runtime. The React singleton only dedupes React remotes. A Vue remote ships Vue, an Angular remote ships Angular — polyglot means a heavier baseline. Worth it only if you genuinely run mixed stacks.
- Communication is neutral. No shared React context across the boundary — use
@jorvel/event-bus/@jorvel/state(plain-JS pub/sub) or DOMCustomEvents. - Typed routes stay React-only. Foreign frameworks get the neutral contract; full TS route typing is a React-adapter perk.
- SSR. Each framework has its own server renderer — cross-framework SSR is opt-in per adapter; CSR-first for non-React remotes.
