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

@web4.0/adm

v1.0.1

Published

Portable declarative UI composition engine with a React adapter.

Readme

@web4.0/adm

ADM (Administrator) is a declarative UI composition engine. You provide the component implementations and their definitions; ADM validates a serializable configuration, composes the React tree, and offers scoped instance lookup and event communication. It does not provide UI components, styling, state management, or business logic.

Install

npm install @web4.0/adm
npm install your-ui-component-package

React 18 or newer is a peer dependency. The core can also be imported separately without loading the React adapter:

import { ADMCore } from "@web4.0/adm/core";

Define and register components

The registry pairs a component implementation with the properties, events, and child behavior supported by that component.

import React, { useEffect, useState } from "react";
import ADM from "@web4.0/adm";

function ProductList({ adm, products }) {
  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>
          <button onClick={() => adm.emit("onSelect", { product })}>
            {product.name}
          </button>
        </li>
      ))}
    </ul>
  );
}

function ProductDetails({ adm, source }) {
  const [selected, setSelected] = useState(null);

  useEffect(() => {
    const list = adm.query("ProductList", source);
    if (!list) return;
    return list.listen("onSelect", ({ product }) => setSelected(product));
  }, [adm, source]);

  return <article>{selected?.name ?? "Select a product"}</article>;
}

function Page({ children }) {
  return <main>{children}</main>;
}

const components = {
  Page: {
    component: Page,
    children: true
  },
  ProductList: {
    component: ProductList,
    props: {
      products: { type: "array", required: true }
    },
    events: {
      onSelect: { payload: "object" }
    }
  },
  ProductDetails: {
    component: ProductDetails,
    props: {
      source: { type: "string", required: true }
    }
  }
};

ADM.registerComponent(components);

An omitted children declaration means children are unsupported. Property types are string, number, boolean, object, array, and null. Defaults are applied when a property is omitted, while explicit null remains distinct from an omitted property. Unknown properties are rejected. adm is reserved for the runtime and cannot be defined in the registry or application properties.

Compose and render an application

Configurations contain only serializable values and component names. Event handlers and functions belong in component implementations, not in the configuration.

const appConfig = {
  name: "Page",
  selector: { id: "home" },
  children: [
    {
      name: "ProductList",
      selector: { id: "products", className: "catalog" },
      props: {
        products: [
          { id: "keyboard", name: "Keyboard" },
          { id: "mouse", name: "Mouse" }
        ]
      }
    },
    {
      name: "ProductDetails",
      props: { source: "products" }
    }
  ]
};

A component that renders nested ADM components must declare children: true. ADM recursively passes their React elements in props.children. Selectors are runtime identities; className does not apply styling unless it is separately declared as a component property.

Configuration registry and linked configs

Each ADM instance owns a config registry, parallel to its component registry. Page configs remain normal component trees. A component prop can reuse a registered value with type: "linked_config" and a value naming the registry entry. Links are prop values, not page roots or child nodes. Registry entries are resolved lazily when a page is rendered, so component configs may refer to other entries registered later.

import { createADM } from "@web4.0/adm";

const adm = createADM();
adm.registerComponent(components);
adm.registerConfig({
  "layout.options": {
    type: "data_config",
    value: {
      type: "object",
      value: { theme: "light", compact: false }
    }
  },
  "layout.header": {
    type: "component_config",
    value: { name: "Header", props: { title: "Dashboard" } }
  }
});

const pageConfig = {
  name: "PageFrame",
  props: {
    options: {
      type: "linked_config",
      value: "layout.options",
      override: { compact: true }
    },
    header: { type: "linked_config", value: "layout.header" }
  },
  children: [{ name: "ProductList", props: { products: [] } }]
};

Declare options as an object prop and header as a component prop on PageFrame. Data configs use a typed { type, value } declaration; a linked data config resolves to its serializable value. A linked component config is compiled by ADM and passed to the receiver as a React element, using the registered component implementations. Overrides merge recursively by object path; arrays are replaced as a whole and unknown override paths are rejected. Missing targets, type mismatches, and circular links fail explicitly.

Interface routes and reusable configurations

Resolve a page within its interface's page list, then pass the normalized route context and interface-level reusable configurations to render. Route values remain strings, repeated query keys become arrays, and declared query defaults are applied before the page is rendered.

import { createADM, resolvePageRoute } from "@web4.0/adm";

const adm = createADM();
adm.registerComponent(components);

const pages = [
  {
    name: "Profile",
    route: "/users/:userId",
    queries: { tab: { default: "overview" } },
    config: {
      name: "ProfilePage",
      props: {
        userId: { $ref: "route.params.userId" },
        activeTab: { $ref: "route.query.tab" }
      },
      children: [{ $config: "MainFooter" }]
    }
  }
];

const { page, context } = resolvePageRoute(pages, window.location.href);
const interfaceConfigs = {
  MainFooter: {
    name: "Footer",
    props: { copyright: "Example" }
  }
};

adm.render(page.config, {
  route: context,
  reusableConfigs: interfaceConfigs
});

$ref supports route.path, route.params.<name>, and route.query.<name>. A reference may provide a serializable default; missing values without defaults fail with a path-specific error. $config references resolve by name within the active render context, may be nested, and reject unknown or circular references. Reusable configurations are passed to each render rather than added to a shared global registry. Component implementations can inspect the same normalized route through adm.getRouteContext(), which returns a copy so a component cannot mutate the runtime's route state.

resolvePageRoute matches only the supplied interface page list, prefers more specific static routes over parameter routes, and rejects equally specific ambiguous matches. Query values are not coerced; URL-derived values remain strings unless repeated, in which case they are arrays of strings.

Portable interface artifacts

createInterfaceArtifact assembles an external Identity service reference, interface, page, reusable-config, and selected UI-family records into a JSON-serializable application/vnd.web4.adm-interface+json document. It fails if an interface selects a family version that is missing from the supplied registry records. The artifact contains package references and manifests only; it does not install or execute packages.

import { createInterfaceArtifact } from "@web4.0/adm";

const artifact = createInterfaceArtifact({
  service: {
    service_uri: "identity://services/service-123"
  },
  interfaceDoc,
  pages,
  configs,
  families
});

Shorthand and typed values

Use ordinary JSON values throughout page configs; ADM validates component props against the registry's property schema. Explicit { type, value } wrappers remain available when a value needs an explicit ADM type.

{
  name: "Workspace",
  props: {
    workspace: "workspace",
    settings: { year: 2025 }
  }
}

ADM only interprets an exact { type, value } envelope when type is one of its supported value types (string, number, boolean, object, array, or null). Application objects with other type values—including objects that also have a value field—remain ordinary data.

API-backed component properties

A declared component property can use a serializable API source instead of a static value. ADM starts the request when its component tree mounts, validates the response against the component property's declared type, and updates that prop in the live component instance. API configs declare the backend domain, API version, call path, and optional request/auth/response settings. For path-based calls ADM constructs {domain}/api/{version}/{path} (version defaults to v1) and sends the version in x-api-version; calls default to JSON POST. An explicit url can be used when an API does not follow that route pattern.

adm.registerAuthProvider("session", async ({ signal }) => {
  const token = await getCurrentAccessToken({ signal });
  return { Authorization: `Bearer ${token}` };
});

adm.registerConfig({
  "accounts.services": {
    type: "api_config",
    value: {
      path: "/get_services",
      domain: "https://api.example.com",
      version: "v1",
      auth: { provider: "session" },
      initial: []
    }
  }
});

const accountsPage = {
  name: "AccountsDashboard",
  props: {
    copy: { title: "Accounts", description: "Manage services." },
    services: {
      type: "api",
      target: "accounts.services"
    }
  }
};

const removeListener = adm.listen("dataError", ({ component, property, message }) => {
  console.error(`Could not load ${component}.${property}: ${message}`);
});

await adm.requestApi("accounts.services", { body: { page: 1 } });
await adm.refreshDataSource("AccountsDashboard", "accounts-dashboard", "services");

registerAuthProvider keeps credentials in the host application's runtime; configs only reference the provider by name. responsePath is optional and defaults to data, matching the { ok, message, status_code, data } API envelope; set it to a dotted path such as data.services, or to the empty string to select the whole response. url remains supported for existing configs. API descriptors also support method, credentials, public string headers, JSON body, auth, and an initial value. acceptNonOk: true returns unsuccessful envelopes as { status_code, message, data } instead of throwing; otherwise a failed HTTP response or ok: false envelope rejects with the backend status_code exposed as error.code and error.status_code. Registered API configs can be called with requestApi(name, { body }), and API-backed properties can be reloaded with refreshDataSource(...). Authorization and cookie headers cannot be embedded in config; cookie-based auth can use credentials: "include" when appropriate. ADM emits dataLoading, dataLoaded, and dataError events with the component, selector, and property. Errors are also logged; a failed request leaves the initial/current prop value intact. Requests are aborted when their source changes or their rendered scope unmounts. Only the React runtime performs requests; serializing or validating a config does not make a network call.

Sandboxed page previews

The browser preview helpers are exported from @web4.0/adm: createPagePreviewDocument, getPreviewApiSources, and createPreviewRequestHandler. The document loads the selected UI-family release in a sandboxed iframe. API configs inside the iframe contain only an opaque proxy URL and source ID; the host handles matching requests with its ADM runtime so credentials and real API URLs stay outside the frame. The host must attach the request handler to its message event and provide the /api/adm-preview-runtime and /api/ui-family-package routes. Unlisted API requests and network connections remain blocked.

Next.js App Router

Runtime selectors and event subscriptions are client-side behavior. Call ADM.render from a Client Component and keep the component registry and configuration in that client-side module:

"use client";

import ADM from "@web4.0/adm";
import components from "./components";
import appConfig from "./app.config";

ADM.registerComponent(components);

export default function Application() {
  return ADM.render(appConfig);
}

Only the composed tree needs to be a client component; surrounding Next.js layouts and pages can remain Server Components. Component implementations may use React hooks normally. Component event listeners should be subscribed from an effect and return ADM's unsubscribe function so cleanup follows React's lifecycle.

Selectors and events

The adm property is injected into every registered component. Its query and getInstance methods return an instance handle or null if there is no accessible match. A string selector matches selector.id; an object selector matches its supplied id and/or className. Queries are limited to the current rendered application scope. A query matching multiple instances throws an ambiguity error; use unique selectors when a component must be addressed. Separate rendered application scopes and ADM instances do not share instances or event listeners.

const list = adm.query("ProductList", "products");
const unsubscribe = list?.listen("onSelect", payload => {
  console.log(payload.product);
});

// From the ProductList implementation:
adm.emit("onSelect", { product });

An instance handle also exposes listen, unlisten, and emit. Events must be declared in the publisher's registry definition. If an event declares a payload type, ADM validates each emitted payload before notifying listeners. listen returns an idempotent unsubscribe function. ADM removes instance listeners when their components unmount.

UI-family registries and component manifests

When an interface uses more than one UI family, register each family under a stable namespace and reference components by namespace::ComponentName. This keeps component resolution deterministic even when packages export the same local component name.

adm.registerComponent(webRegistry, { namespace: "web" });
adm.registerComponent(chartsRegistry, { namespace: "charts" });

const page = {
  name: "web::Page",
  children: [
    { name: "charts::Chart", props: { data: [] } }
  ]
};

createComponentManifest(registry) creates a technical manifest from the registry. UI-family packages may enrich it with descriptions, usage guidance, categories, and examples. validateComponentManifest(registry, manifest) rejects documented components, props, defaults, events, or child support that do not match the executable registry.

Independent ADM instances

Use the default export for a single application. Create separate instances when applications need independent registries and runtime scopes:

import { createADM } from "@web4.0/adm";

const admin = createADM();
admin.registerComponent(components);

registerComponent(registry, { replace: true }) explicitly replaces existing definitions. Registration is validated atomically, and duplicate names are otherwise rejected.

Package development

npm install
npm test
npm run build

The package ships native ES modules and TypeScript declarations. The React adapter is exported from the main entry point; @web4.0/adm/core exposes the React-independent registry and runtime primitives.