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

@homerunner-next/widget-core

v0.6.5

Published

Shared types, URL helpers, plugin schema, runtime mount, and Vite tooling for HomeRunner widget plugins and host packages.

Downloads

1,270

Readme

@homerunner-next/widget-core

Shared SDK for HomeRunner widget plugins and host packages — types, URL helpers, plugin schema, runtime mount, and Vite tooling. The same primitives that system widgets use are exposed here so third-party plugins behave identically to first-party ones.

Install

npm install @homerunner-next/widget-core

Peer deps (all optional, declared via peerDependenciesMeta):

react ^19.2.0
react-dom ^19.2.0
@tanstack/react-query ^5
zod ^3
vite ^5 || ^6 || ^7

Install the peers your plugin actually uses. A pure urls consumer needs none; a runtime-mounting plugin needs React + react-dom + react-query.

Pin to the canonical runtime versions

The renderer's vm sandbox and the runtime IIFE on assets.homerunner.io ship a single shared React build. Plugin client umds externalize react, react-dom, react-dom/client, react-dom/server, react/jsx-runtime, and @tanstack/react-query against that runtime — meaning a plugin's bundled React APIs must match the runtime's exactly, not just by major version. Even a patch drift can trip the Incompatible React versions runtime check inside react-dom.

Canonical versions are tracked in lavender/pnpm-workspace.yaml's catalog: block. As of the current release:

| Package | Pin (exact) | | --- | --- | | react | 19.2.5 | | react-dom | 19.2.5 | | @tanstack/react-query | 5.95.2 |

Pin these exact values in your plugin's package.json (no caret). create-hr-plugin scaffolds with these pins by default — keep them aligned when the catalog is bumped.

Browser-only side effects in useEffect

Plugin SSR umds bundle into a single file (Vite's inlineDynamicImports), so dynamic import("...") calls are hoisted to module-evaluation time — they run when the renderer loads the umd in its vm sandbox, not when the useEffect callback fires. Guard browser-only side effects (DOM measurement, IntersectionObserver, localStorage, third-party loaders like webfontloader) with an explicit early-return check:

useEffect(() => {
  if (typeof window === "undefined") return;
  import("webfontloader").then(({ load }) => load(opts));
}, [opts]);

The SSR build's typeof-tree-shaker rewrites typeof window to "undefined" so the literal "undefined" === "undefined" collapses, esbuild DCEs everything after the unconditional return, and the dynamic import (plus the entire side-effect dep) is excluded from the SSR umd. At runtime in the browser the guard is free.

Subpath exports

| Subpath | Surface | | --- | --- | | @homerunner-next/widget-core | Top-level barrel (re-exports types). Prefer subpaths for tree-shaking. | | @homerunner-next/widget-core/manifest | PluginManifest type | | @homerunner-next/widget-core/urls | resolvePluginAssetUrl, extractAssetFilename, getPluginAssetUrl (asset proxy moved to assets.homerunner.io/p/<slug>/<file>, single-hop sibling of /w/) | | @homerunner-next/widget-core/globals | HRWidgetRuntime / HRPlugins types, getRuntime(), registerPlugin() | | @homerunner-next/widget-core/plugin-schema | zodToManifestSchema, JSONSchema / PluginUiSchema types | | @homerunner-next/widget-core/schema | zod widgetSchema + WidgetSchemaType (the base every widget extends) | | @homerunner-next/widget-core/contracts | WidgetProps, LayoutWidgetProps, Feed, Widget<T>, PageSchema, SlotBinding | | @homerunner-next/widget-core/runtime | mount, ClientWrapperForWidget, useFetchWidget, fetchFeed, fetchWidget, getFinalSettings, setupShadowDOM, usePortalContainer, useShadowHost, useTopLevelPortal, getSharedQueryClient | | @homerunner-next/widget-core/utils | WidgetProvider, useWidget, useTranslation, useLocaleDetection, getProxiedImageUrl, getFeedQuery, getWidgetQuery, locale helpers | | @homerunner-next/widget-core/vite | viteHomerunnerWidget({ slug }) build helper |

Every subpath ships ESM + CJS + .d.ts. Source is included for IDE go-to-definition.

Quick start (plugin author)

The fastest path is the scaffolder:

npx create-hr-plugin my-widget

That generates a working plugin (vm.Script SSR + IIFE client) wired against this SDK. The bits below show what the scaffold does under the hood.

vite.config.ts

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import tailwindcss from "@tailwindcss/vite";
import { viteHomerunnerWidget } from "@homerunner-next/widget-core/vite";

export default defineConfig(({ command }) => {
  const hr = viteHomerunnerWidget({ slug: "my-widget", command });
  return {
    plugins: [react(), tailwindcss(), ...hr.plugins],
    define: hr.define,
    build: hr.build,
    server: { ...hr.server, port: 3001 },
  };
});

viteHomerunnerWidget wires the rollup externals (React / RD / RQ resolved against window.HRWidgetRuntime), dev-mode shim aliases, manifest generation with hashed filenames, and the /w/ plugin-asset proxy.

src/config.ts

import { z } from "zod";
import { widgetSchema } from "@homerunner-next/widget-core/schema";
import {
  zodToManifestSchema,
  type PluginUiSchema,
} from "@homerunner-next/widget-core/plugin-schema";

export const configZod = widgetSchema.extend({
  title: z.string().default("My Widget"),
});

export const uiSchema: PluginUiSchema = {
  title: { component: "TextInput", label: "Title" },
};

export const configSchema = zodToManifestSchema(configZod);
export type PluginConfig = z.infer<typeof configZod>;

widgetSchema is the base every system widget extends — width, colorScheme, lightModeColors, darkModeColors, font, language, section, customCss, filter. The dashboard form renderer pairs these inherited fields with its built-in commonWidgetUiSchema, so your uiSchema only declares plugin-specific fields.

src/index.tsx

import { mount } from "@homerunner-next/widget-core/runtime";
import { registerPlugin } from "@homerunner-next/widget-core/globals";
import MyWidget from "./widget";

registerPlugin("my-widget", MyWidget);

function doMount() {
  document.querySelectorAll<HTMLElement>("[data-hr-widget-container]").forEach((c) => {
    if (c.querySelector(`[data-hr-widget="plugin:my-widget"]`)) {
      mount(c, { widget: MyWidget, widgetType: "plugin:my-widget" });
    }
  });
}

if (document.readyState === "loading") {
  document.addEventListener("DOMContentLoaded", doMount);
} else {
  doMount();
}

mount is bundled locally with the plugin so mount upgrades version with the plugin (instead of being pinned to whatever runtime the page loaded).

SSR module

import { fetchProperties } from "./data";
import type { PluginConfig } from "./config";

export async function dehydrateState(qc: any, ctx: { options: PluginConfig; feedId: number; widgetId: string }) {
  // Prefetch data into the shared React Query client. Cache hydrates
  // through the SSR HTML — no second fetch on the client.
}

export async function getStaticAssets(_options: PluginConfig) {
  return {
    css: ["dist/my-widget.css"],
    js: ["dist/my-widget.iife.js"],
  };
}

The renderer fetches your built SSR bundle, evaluates it in a vm.Script sandbox with React / RD / RQ provided, and calls dehydrateState + getStaticAssets.

Plugin runtime contract

A plugin ships:

  1. Manifest JSONid, version, widgetType, ssr.url, assets.{js,css}, runtime.react, plus a configSchema (JSON Schema, generated via zodToManifestSchema) and optional uiSchema. Path-only asset URLs are resolved against the registered manifest origin by the renderer + dashboard asset proxy, so the same build works on any host.
  2. SSR UMD bundle — exports default (component), getInitialData, dehydrateState, getStaticAssets. Loaded server-side via vm.Script.
  3. Client IIFE bundle — registers the component via registerPlugin() and auto-mounts via mount() on every [data-hr-widget="plugin:<slug>"] it finds inside [data-hr-widget-container]. React / RD / RQ are externalized against window.HRWidgetRuntime.

The host page loads the runtime IIFE once. Every plugin IIFE on the page externalizes against it, so React isn't bundled per-plugin.

Versioning

0.x — surface may change between minor versions. We aim for stability across patch versions.

License

MIT