npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@modular-frontend/core

v0.6.0

Published

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.

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

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
  • 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:

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

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:

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:

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

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

See 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:

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 anys 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 for the full guide.