JORVEL

@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

Every function below is fully typed. Pass generics where shown to thread types from your own route registry through to hooks.

Router

SymbolSignature
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
ts
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

ComponentProps
<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 slot
  • useOutletParams<T>(): T
  • resolveChain(routes, pathname): NestedMatch[] — pure resolver, no React
  • Per-segment boundaries: NestedRoute.loading (loading.tsx Suspense fallback) and NestedRoute.errorElement (error.tsx node or (p: { error, reset }) => ReactNode)

Middleware

ts
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)

ts
// 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

ts
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

HookSignature
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

ts
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(): boolean
  • prefersReducedMotion(): boolean

Prefetch

  • prefetchRoute(pathname: string, config: { routes, remotes }): Promise<void>
  • resetPrefetchCache(): void

Concurrent preload

ts
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

ts
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

ts
<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

ts
<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 tooling

Remote loader

ts
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

ts
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

ts
checkVersions({
  host: { name: string; version: string };
  remote: { name: string; version: string };
  singletons?: Record<string, string>;
}): Array<{ pkg: string; expected: string; actual: string }>;

Health

ts
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): () => void
  • onRuntimeError(cb): () => void
  • emitRemoteLoad(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 when JORVEL_DEV_RELOAD_URL is 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.

ts
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

ts
// 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)

ts
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

ts
// 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.

ts
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.

ts
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.

ts
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.

ts
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

ts
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

ts
<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.

ts
deprecate(message: string, opts?: { since?: string; replacement?: string }): void;
deprecated<T extends (...args: any[]) => any>(fn: T, message: string): T;