@jorvel/runtime
Singleton-safe router, remote loader, hooks, guards, telemetry. Every export below is importable from @jorvel/runtime; the package is configured as a Module Federation singleton so host and remote observe the same router instance.
Type-safety
Router
| Symbol | Signature |
|---|---|
getRouter | (opts?: RouterOptions) => Router — returns the singleton, creating it on first call |
createRouter | (opts?: RouterOptions) => Router — fresh instance, for tests |
useRouter | () => Router — hook |
usePathname | () => string — pathname + search + hash |
dispatchJorvelNavigate | (detail: NavigateDetail) => void |
attachJorvelNavigateListener | () => () => void — returns disposer |
type NavigateDetail = {
to: string; // pathname + optional ?search and #hash
mode?: 'push' | 'replace'; // default 'push'
state?: unknown; // stored via history.{push,replace}State
};
type RouterOptions = {
basePath?: string; // ignore paths outside this prefix
listenToNavigateEvents?: boolean; // default true
};Routing components
| Component | Props |
|---|---|
<NavLink> | to, label, prefetch?, activeStyle?, className?, activeClassName? |
<NavLinkPrefetchProvider> | config: { routes, remotes } |
<RemoteOutlet> | routes, remotes, fallback?, noMatch?, errorBoundary? |
<RemoteApp> | subpath, pages, fallback?, noMatch? |
<ErrorBoundary> | fallback: (err, reset) => ReactNode, onError?: (err) => void |
Nested routes
<NestedRouter routes fallback? notFound? /><Outlet />— child slotuseOutletParams<T>(): TresolveChain(routes, pathname): NestedMatch[]— pure resolver, no React- Per-segment boundaries:
NestedRoute.loading(loading.tsxSuspense fallback) andNestedRoute.errorElement(error.tsx— node or(p: { error, reset }) => ReactNode)
Middleware
type MiddlewareDecision =
| { type: 'next'; headers?: Record<string, string> }
| { type: 'redirect'; to: string; status: 301|302|303|307|308 }
| { type: 'rewrite'; to: string }
| { type: 'respond'; response: Response };
type Middleware = (ctx: {
pathname: string;
searchParams: URLSearchParams;
url?: URL;
request?: Request;
state: Record<string, unknown>;
}) => MiddlewareDecision | void | Promise<MiddlewareDecision | void>;
defineMiddleware(fn: Middleware): Middleware;
next(headers?): MiddlewareDecision;
redirect(to, status = 307): MiddlewareDecision;
rewrite(to): MiddlewareDecision;
respond(response): MiddlewareDecision;
// matcher: '*' = one segment, '**' = any depth; first terminal decision wins
runMiddleware(
chain: Array<Middleware | { matcher?: string | string[]; handler: Middleware }>,
init: { pathname: string; searchParams?; url?; request?; state? },
): Promise<MiddlewareDecision>;Server actions (mutations)
// reads use defineLoader / useLoaderData from @jorvel/ssr; actions are the write side
defineAction<I, O>(fn: (input: I, ctx?: { request?; signal? }) => O | Promise<O>): Action<I, O>;
// pending/error/data state machine; concurrent submits are last-wins
useAction<I, O>(action): { data: O | null; error: unknown; pending: boolean; submit(i: I): Promise<O>; reset(): void };
// binds a FormData action to <form onSubmit>; progressive-enhancement friendly
useFormAction<O>(action): { onSubmit(e); submit(fd: FormData): Promise<O>; data; error; pending; reset };Typed routes
type Validator<T> = { parse(input: unknown): T; safeParse?(input: unknown): { success: true; data: T } | { success: false; error: unknown } };
createRoute<TParams, TSearch>({
path: string; // '/users/:id'
params?: Validator<TParams>;
search?: Validator<TSearch>;
}): TypedRoute<TParams, TSearch>;
defineRoutes<T extends Record<string, TypedRoute<any, any>>>(map: T): T;Hooks
| Hook | Signature |
|---|---|
useSearchParams | () => [URLSearchParams, (next, mode?: 'push' | 'replace') => void] |
useQueryParam | (key: string) => [string | null, (next: string | null) => void] |
useParams | <T>() => T — from the nearest ParamsProvider |
useNavigate | () => (to: string, opts?: { replace?, state? }) => void |
useNavigationEvents | (handler: (e: NavigationEvent) => void) => void |
useRemoteData | <T>({ key, fetcher, ttl? }) => { data, error, loading, refresh } |
Guards
type GuardResult = boolean | { redirect: string };
type RouteGuard = (ctx: { pathname: string; params: Record<string, string> }) => GuardResult | Promise<GuardResult>;
runGuards(resolved, pathname, globalGuards?: RouteGuard[]): Promise<GuardResult>;
createAuthGuard({
isAuthenticated: () => boolean | Promise<boolean>;
loginPath?: string; // default '/login'
captureReturnTo?: boolean; // default true — appends ?next=…
}): RouteGuard;
createRoleGuard({
hasRole: (role: string) => boolean;
roles: string[]; // any-of
fallbackPath?: string;
}): RouteGuard;View Transitions
navigateWithTransition(detail, opts?: { respectReducedMotion? })withViewTransition(update: () => void | Promise<void>, opts?)supportsViewTransitions(): booleanprefersReducedMotion(): boolean
Prefetch
prefetchRoute(pathname: string, config: { routes, remotes }): Promise<void>resetPrefetchCache(): void
Concurrent preload
preloadRemotes(
remotes: Array<{ name: string; entryUrl: string; integrity?: string }>,
opts?: {
concurrency?: number; // default 3
idle?: boolean; // default true — wraps each load in requestIdleCallback
idleBudgetMs?: number; // default 8
onResult?: (r: { remote: string; ok: boolean; durationMs: number; error?: unknown }) => void;
},
): Promise<void>;Service Worker
registerJorvelServiceWorker(opts?: {
url?: string; // default '/jorvel-sw.js'
scope?: string; // default '/'
autoActivate?: boolean; // post SKIP_WAITING on update
onUpdateReady?: () => void;
onActivated?: () => void;
}): Promise<ServiceWorkerRegistration | null>;
unregisterJorvelServiceWorker(): Promise<boolean>;
// Inline source — useful for build-time injection
declare const JORVEL_SERVICE_WORKER_SOURCE: string;Shadow DOM
<ShadowRemote
mode?: 'open' | 'closed'; // default 'open'
css?: string; // inlined into <style>
stylesheets?: string[]; // resolved as <link rel="stylesheet">
>{children}</ShadowRemote>
scopeCss(css: string, scopePrefix: string): string;Islands
<Island
strategy?: 'load' | 'idle' | 'visible' | 'media' | 'interaction';
media?: string; // required when strategy === 'media'
load: () => Promise<{ default: ComponentType<any> }>;
fallback?: ReactNode;
rootMargin?: string; // IntersectionObserver fine-tuning
/>
clientBoundary<T>(component: T): T; // marker for future toolingRemote loader
loadRemoteEntry({
name: string;
entryUrl: string;
integrity?: string; // SRI hash
}, opts?: {
allowedOrigins?: string[]; // verified before fetch
requireIntegrity?: boolean; // fail closed when no SRI hash is present
crossOrigin?: 'anonymous' | 'use-credentials' | 'none';
}): Promise<void>;
loadRemoteModule<T = unknown>(
name: string,
exposedKey: string, // './App'
opts?: { allowedOrigins?: string[]; integrity?: string },
): Promise<T>;
initRemoteContainer(name: string): Promise<void>;Remote registry
getRemoteRegistry(opts?: {
allowedOrigins?: string[];
fetcher?: typeof fetch;
}): RemoteRegistry;
class RemoteRegistry {
register(d: RemoteDescriptor): void;
unregister(name: string): void;
get(name: string): RemoteDescriptor | undefined;
list(): RemoteDescriptor[];
load(manifestUrl: string): Promise<void>;
}Version check
checkVersions({
host: { name: string; version: string };
remote: { name: string; version: string };
singletons?: Record<string, string>;
}): Array<{ pkg: string; expected: string; actual: string }>;Health
createHealthHandler(opts: {
name: string;
version: string;
build?: string;
shared?: Record<string, string>;
probes?: Record<string, () => Promise<{ ok: boolean; detail?: string }>>;
}): (req: Request) => Promise<{ status: number; body: HealthBody }>;
fetchHealth(url: string, opts?: { timeoutMs?: number }): Promise<HealthBody>;Telemetry
onRemoteLoad(cb): () => voidonRuntimeError(cb): () => voidemitRemoteLoad(detail)/emitError(detail)- DOM events:
jorvel:navigate,jorvel:remote-load,jorvel:error
Dev reload
connectJorvelDevReload(url?: string): () => void— opens a WS to the host's reload server; auto-injected whenJORVEL_DEV_RELOAD_URLis set
useRemoteData
Small fetch + cache hook. Dedupes concurrent requests for the same key, TTLs the result, exposes refresh for forced revalidation. Pair with defineLoader on the server for end-to-end hydration.
useRemoteData<T>(opts: {
key: string | unknown[];
fetcher: (signal: AbortSignal) => Promise<T>;
ttl?: number; // ms, default 0 (no cache)
initialData?: T; // from hydration
}): {
data: T | undefined;
error: unknown | undefined;
loading: boolean;
refresh(): Promise<void>;
};Cache tags & revalidation
// tag a read: useRemoteData({ key, fetcher, tags: ['posts', 'post:42'] })
revalidateTag(tag: string): void; // purge every entry with this tag + signal re-render
revalidatePath(path: string): void; // sugar: revalidateTag(path)
invalidateRemoteData(key: string): void; // purge one key
clearRemoteDataCache(): void; // purge everything
prefetchRemoteData<T>(key, fetcher, ttl?, tags?): Promise<T>;
useRevalidationVersion(): number; // subscribe a component to revalidation (re-renders on purge)Query cache (useQuery / useMutation)
class QueryClient {
constructor(opts?: { staleTime?: number; now?: () => number });
fetchQuery<T>(key, queryFn): Promise<T>; // dedupes in-flight
setQueryData<T>(key, updater): void; // optimistic / hydration
invalidate(prefix | (key) => boolean): void;
isStale(key, staleTime?): boolean;
getEntry<T>(key); clear();
}
QueryClientProvider({ client?, children }); useQueryClient(): QueryClient;
useQuery<T>({ queryKey, queryFn, staleTime?, enabled? }):
{ data; error; status; isLoading; isFetching; isStale; refetch };
useMutation<I, O>({ mutationFn, onSuccess?, onError? }):
{ mutate; mutateAsync; data; error; status; isPending; reset };useOptimistic
// React-18-compatible; same shape as React 19's built-in
useOptimistic<State, Action = State>(
state: State,
updateFn: (current: State, optimisticValue: Action) => State,
): [optimisticState: State, addOptimistic: (action: Action) => void];Weighted remotes
Pick among multiple URLs for the same remote, weighted by health and rollout percentage. Useful for canary deploys and CDN failover.
createWeightedRemote(opts: {
candidates: Array<{ entryUrl: string; weight: number; healthUrl?: string }>;
probeIntervalMs?: number; // default 30_000
failoverOnError?: boolean; // default true
}): { resolve(): Promise<string>; dispose(): void };Blue/green
Two-environment cutover with explicit swap(). Use when canary metrics are green and you want a deterministic flip.
createBlueGreen(opts: {
blue: { entryUrl: string; version: string };
green: { entryUrl: string; version: string };
active?: 'blue' | 'green';
}): BlueGreenController;
interface BlueGreenController {
active: 'blue' | 'green';
swap(): void;
resolve(): { entryUrl: string; version: string };
subscribe(listener: (which: 'blue' | 'green') => void): () => void;
}Feature flags
Pluggable provider interface. Bring LaunchDarkly / Statsig / your-own and the runtime exposes a singleton with useFeatureFlag + useFeatureFlags hooks.
interface FeatureFlagProvider {
getBool(key: string, fallback?: boolean): boolean;
getString(key: string, fallback?: string): string;
getNumber(key: string, fallback?: number): number;
subscribe(listener: () => void): () => void;
}
setFeatureFlagProvider(provider: FeatureFlagProvider): void;
useFeatureFlag(key: string, fallback?: boolean): boolean;
useFeatureFlags<T extends Record<string, boolean>>(keys: T): T;Resilience
Retries with jittered backoff, circuit breaker, timeouts. Used internally by loadRemoteEntry; exposed for app-level fetches.
withRetry<T>(fn: () => Promise<T>, opts?: {
attempts?: number; // default 3
baseDelayMs?: number; // default 200
maxDelayMs?: number; // default 5_000
jitter?: number; // 0..1, default 0.3
shouldRetry?: (err: unknown, attempt: number) => boolean;
}): Promise<T>;
createCircuitBreaker(opts: {
threshold: number; // failures before opening
resetMs: number;
}): {
exec<T>(fn: () => Promise<T>): Promise<T>;
state: 'closed' | 'open' | 'half';
};
withTimeout<T>(fn: (signal: AbortSignal) => Promise<T>, ms: number): Promise<T>;Fonts
buildFontPreloadLink(href: string, opts?: { type?: string; crossorigin?: 'anonymous' | 'use-credentials' }): {
rel: 'preload'; as: 'font'; href: string; type: string; crossorigin: string;
};
buildFontFaceCss(faces: Array<{
family: string;
src: string;
weight?: number | string;
style?: 'normal' | 'italic';
display?: 'auto' | 'block' | 'swap' | 'fallback' | 'optional';
unicodeRange?: string;
}>): string;
googleFontsUrl(opts: { families: Array<{ family: string; weights: number[] | { italic: boolean; weight: number }[] }> }): string;
googleFontsPreconnectLinks(): Array<{ rel: 'preconnect'; href: string; crossorigin?: 'anonymous' }>;Image
<Image
src: string;
alt: string;
width: number;
height: number;
widths?: number[];
formats?: Array<'avif' | 'webp' | 'jpg' | 'png'>;
breakpoints?: Array<{ minWidth: number; size: string }>;
fetchPriority?: 'high' | 'low' | 'auto';
loading?: 'lazy' | 'eager';
/>
buildSrcset(template: string, opts: { widths: number[] } | { density: number[] }): string;
buildSizes(opts: { breakpoints: Array<{ minWidth: number; size: string }>; fallback: string }): string;
buildImagePreloadLink(template: string, opts: {
widths?: number[];
sizes?: string;
fetchPriority?: 'high' | 'low' | 'auto';
}): { rel: 'preload'; as: 'image'; imagesrcset: string; imagesizes?: string; fetchpriority?: string };Deprecation helper
Emits a one-time warning per call-site. Used by the runtime to flag removed APIs; usable in app code to retire internal helpers without breaking consumers immediately.
deprecate(message: string, opts?: { since?: string; replacement?: string }): void;
deprecated<T extends (...args: any[]) => any>(fn: T, message: string): T;