# @modular-frontend/core

> Framework-neutral primitives for modular frontend applications: module descriptors, slots, navigation, validation, journey contracts, and a lightweight store. No UI-framework dependency — components are carried as opaque values.

Latest version **0.6.0** (published 2026-07-20) · 0 weekly downloads

## Install

```sh
npm install @modular-frontend/core
pnpm add @modular-frontend/core
yarn add @modular-frontend/core
bun add @modular-frontend/core
```

## Health

**Score 65/100 (B)** — status: active.

Positive: has types; esm support; no vulnerabilities; has provenance; high maintenance score.

Warnings: low downloads; pre 1.0.

## Facts

| | |
|---|---|
| Version | 0.6.0 |
| Published | 2026-07-20 |
| First published | 2026-07-05 |
| Weekly downloads | 0 |
| TypeScript types | bundled |
| Module format | ESM + CommonJS |
| Dependencies | 1 |
| Unpacked size | 404.4 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Provenance | attested (GitHub Actions) |
| GitHub stars | 7 |
| Maintainers | kibertoad |

## Links

- npm: https://www.npmjs.com/package/@modular-frontend/core
- Repository: https://github.com/kibertoad/modular-react
- npm.io page: https://npm.io/package/@modular-frontend/core

## Dependencies (1)

- [@standard-schema/spec](https://npm.io/package/@standard-schema/spec.md) ^1.0.0

## Recent versions

- 0.6.0 (latest) — 2026-07-20
- 0.5.0 — 2026-07-19
- 0.4.0 — 2026-07-19
- 0.3.0 — 2026-07-19
- 0.2.0 — 2026-07-18
- 0.1.0 — 2026-07-05

## README

# @modular-frontend/core

Framework-neutral primitives for building modular frontend applications. Provides module descriptor types, slot/navigation builders, validation, journey contracts, and a lightweight store, with no UI-framework dependency.

This is the shared foundation the framework bindings build on. The React binding (`@modular-react/core`) re-exports this package; a future `@modular-vue/core` will do the same. Components are carried as opaque values through a small `UiComponent` / `UiNode` type seam, which each binding refines to its own component type.

## Installation

```bash
npm install @modular-frontend/core
```

Most apps depend on a framework binding (`@modular-react/core`, and downstream `@react-router-modules/*` / `@tanstack-react-modules/*`) rather than on this package directly. Use `@modular-frontend/core` when building framework-agnostic tooling or a new framework binding.

## What's included

- **Types**: `ModuleDescriptor`, `AnyModuleDescriptor`, `LazyModuleDescriptor`, `NavigationItem`, `ModuleLifecycle`, `ReactiveService`, `SlotMap`, `SlotMapOf`, `ZoneMap`, `ZoneMapOf`, `Store`, `RegistryConfig`, `NavigationGroup`, `NavigationManifest`, `ModuleEntry`, `DynamicSlotFactory`, `SlotFilter`
- **UI-type seam**: `UiComponent`, `UiNode` — framework-neutral stand-ins refined by each binding
- **Slots**: `buildSlotsManifest`, `collectDynamicSlotFactories`, `evaluateDynamicSlots`
- **Navigation**: `buildNavigationManifest`, `resolveNavHref`
- **Route data**: `mergeRouteStaticData` (router-agnostic merge helper used by `useZones` / `useRouteData` in the runtime packages)
- **Validation**: `validateNoDuplicateIds`, `validateDependencies`, `validateEntryExitShape`
- **Store**: `createStore` (a lightweight zustand-compatible store, no middleware)
- **Detection**: `isStore`, `isStoreApi` (alias), `isReactiveService`, `separateDeps`
- **Helpers**: `defineModule`, `defineSlots`, `buildDepsSnapshot`, `runLifecycleHooks`
- **Subject-keyed panels**: `definePanelGroup`, `resolvePanels`, and the `PanelEntry` / `PanelGroupHandle` types — the pure, framework-neutral engine for a render-**all**, predicate-gated, open-contribution slot projection (the render-all sibling of the component-pairing helpers). The thin per-binding hosts (`usePanels` / `<PanelsOutlet>`) live in `@modular-react/react` and `@modular-vue/vue`. See [docs/subject-panels.md](../../docs/subject-panels.md)
- **Journey contracts**: type-only surfaces describing a journey runtime (implemented in `@modular-react/journeys`)
- **Semver subset**: `satisfies`, `parseRange`, `parseVersion`, `compareVersions`

## The UiComponent seam

The core never renders, calls, or inspects a component — it only carries them as values on descriptors (`component`, `zones`, entry-point `component`, `NavigationItem.icon`). So instead of depending on any UI framework, those positions use two neutral aliases:

```typescript
export type UiComponent<P = any> = ((props: P) => any) | (new (props: P) => any);
export type UiNode = any;
```

`UiComponent<P>` is "callable or constructable with props `P`", which admits React's full `ComponentType` (function components via the call arm, class components via the construct arm) as well as any other function/constructor component, and stays usable as a JSX element type in a React binding without pulling in `@types/react`. The call arm keeps function components props-checked against `ModuleEntryProps`. A Vue binding narrows the same alias to Vue's component type.

`UiNode` stays `any` rather than `unknown` so a binding can pass these values straight into its framework's render slots (e.g. a React `<Suspense fallback>`), which `unknown` would reject.

## Usage

```typescript
import { buildSlotsManifest, createStore } from "@modular-frontend/core";
import type { ModuleDescriptor, Store } from "@modular-frontend/core";
```

## Generic NavigationItem

`NavigationItem` has four optional generics that let hosts opt into stricter typing — typed i18n labels, dynamic-href context, an app-owned `meta` bag, and an app-owned dispatchable `action` union:

```typescript
import type { NavigationItem } from "@modular-frontend/core";
// `ParseKeys` from i18next resolves to your app's translation keys once you
// augment `CustomTypeOptions.resources` (see the i18next TypeScript guide).
import type { ParseKeys } from "i18next";

interface NavContext {
  workspaceId: string;
}

type Permission = "managePortalRequests" | "viewReports";

interface NavMeta {
  permission?: Permission;
  badge?: "beta" | "new";
}

type NavAction =
  | { kind: "open-module"; moduleId: string; entry: string; input?: unknown }
  | { kind: "journey-start"; journeyId: string; buildInput?: (ctx?: unknown) => unknown };

// Alias once in app-shared and use everywhere
export type AppNavItem = NavigationItem<ParseKeys, NavContext, NavMeta, NavAction>;
```

`action` defaults to `never`, so apps that don't need dispatchable nav intents pay no cost — the field is absent from the item surface.

At render time, resolve the href with context:

```typescript
import { resolveNavHref } from "@modular-frontend/core";

const href = resolveNavHref(item, { workspaceId });
```

See [docs/navigation.md](../../docs/navigation.md) for the full guide.

## AnyModuleDescriptor

`ModuleDescriptor` has four type parameters (`TSharedDependencies`, `TSlots`, `TMeta`, `TNavItem`). Internal helpers — navigation builders, lazy-field warnings, test fixtures — often only care about one of them (usually `TNavItem`), and writing `ModuleDescriptor<any, any, any, AppNavItem>` everywhere is noisy.

`AnyModuleDescriptor<TNavItem>` is the shorthand:

```typescript
import type { AnyModuleDescriptor, NavigationItem } from "@modular-frontend/core";

// Accept any module shape, but preserve the nav item narrowing.
function collectNav<TNavItem extends NavigationItem>(
  modules: readonly AnyModuleDescriptor<TNavItem>[],
) {
  return modules.flatMap((m) => m.navigation ?? []);
}
```

Prefer the full `ModuleDescriptor<...>` at user-facing boundaries — the alias is intended for generic plumbing where the extra positional `any`s would be pure filler.

## mergeRouteStaticData

Router-agnostic merge helper used internally by the `useZones` and `useRouteData` hooks in the runtime packages. The two routers diverge on where they park per-route static data (`handle` in React Router, `staticData` in TanStack Router) but agree on the merge rules — so the shared helper takes the merge rules and a getter that plucks the data field.

Semantics: iterates matches in the order given (root → leaf), deeper matches overwrite shallower ones per key, `undefined` values are skipped (so a leaf can't silently clobber an ancestor by omitting the key or setting it to `undefined`). Arrays at the data position are ignored rather than enumerated as index-keyed objects.

## Full documentation

See the [main documentation](https://github.com/kibertoad/modular-react#readme) for the full guide.

---
_Source: https://npm.io/package/@modular-frontend/core · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
