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

@sigx/runtime-core

v0.15.6

Published

Runtime core for SignalX

Downloads

10,527

Readme

@sigx/runtime-core

Runtime core for SignalX. Provides the platform-agnostic component system, JSX runtime, reconciler, lifecycle hooks, dependency injection, and control flow primitives.

📚 Full guides, API reference and live examples → https://sigx.dev/core/packages/runtime-core/overview/

Install

npm install @sigx/runtime-core

Usage

import { component, signal, onMounted, onUnmounted } from '@sigx/runtime-core';

const Timer = component(() => {
  const elapsed = signal(0);
  let interval: number;

  onMounted(() => {
    interval = setInterval(() => elapsed.value++, 1000);
  });

  onUnmounted(() => {
    clearInterval(interval);
  });

  return () => <span>Elapsed: {elapsed.value}s</span>;
});

Rest props — forwarding what you didn't consume

Forwarding a component's leftover props onto its root element is plain JS. Destructure what the component consumes; the rest is what the consumer passed:

const Button = component<ButtonProps>(ctx => () => {
  const { variant, ...rest } = ctx.props;
  return <button class={variant} {...rest} />;
});

The destructure is the declared-props list — there is no helper for it, and the rest object is typed by TypeScript off the props type. Framework-internal keys (key, ref, children, model bindings, client:* directives) never appear in ctx.props or reach the DOM, so the spread is safe.

For the component's props type to accept host attributes in the first place, declare & Define.Attrs:

type ButtonProps = Define.Prop<'variant', 'primary' | 'ghost'> & Define.Attrs;

Declare it only if the component really does forward — a type that compiles and then drops the attribute is the failure mode the opt-in exists to prevent.

mergeProps — when both sides set the same key

A JSX spread is flattened by the compiler into one object literal before the runtime sees it, so <button {...rest} {...bag} /> lets later keys clobber earlier ones. If the consumer and the component both set class, one is lost; same for onClick. mergeProps combines them instead:

const Button = component<ButtonProps>(ctx => {
  const merged = mergeProps(
    () => { const { variant: _v, ...rest } = ctx.props; return rest; },
    () => ({ class: 'btn', onClick: onActivate })
  );
  return () => <button {...merged}>{ctx.slots.default?.()}</button>;
});

| Key | Rule | |---|---| | class / className | concatenated in argument order, emitted as class | | style | merged into an object; string sources are parsed first | | on* handlers | chained in source order, grouped by the event they resolve toonClick and onclick become one entry, so two handlers can never collide in the same DOM listener slot | | ref | chained; every source's ref is fed | | everything else | exact spread semantics — the last source with the key wins, including an explicit undefined |

Two things to know. mergeProps is not a defaults helper: it replaces a spread, so it behaves like one (destructuring with defaults already covers defaults). And call it once in setup, as above — the derived ref and chained handlers are identity-cached, and rebuilding them each render hands the renderer fresh functions that make it tear down and re-apply refs for nothing. Sources may be thunks, so the result stays reactive either way.

Chaining cannot express swallow. A component that gates a consumer handler — dropping onClick while disabled — keeps destructuring it out and calling it itself.

Setup reactions are disposed on unmount

effect(), watch(), and non-detached effectScope() created directly in a component's setup are tied to the component's lifetime — they're stopped automatically when it unmounts (and re-created on HMR reload). You don't need to hold their handles and call .stop() in onUnmounted:

const Search = component(() => {
  const query = signal('');
  // Auto-disposed on unmount — no manual cleanup needed.
  watch(() => query.value, (q) => runSearch(q));
  return () => <input onInput={(e) => (query.value = e.target.value)} />;
});

Only setup itself is scoped: reactions created inside onMounted/onCreated (or async callbacks) are not captured — dispose those via onUnmounted. computed() is lazy and needs no disposal.

Required injectables

defineInjectable(factory) gives an injectable a zero-config fallback: used without a provider, it lazily creates a module-global singleton. That is right for optional services, and wrong for per-app services like a router — on the server, a forgotten provide would silently share one instance across every request (dev builds warn when this happens during SSR).

Declare those services required by passing a name instead of a factory. There is no fallback; using it unprovided throws a structured error (SIGX202) naming the injectable. (In production builds, runtime errors carry the SIGX### code, any runtime detail, and a link to https://sigx.dev/errors/ — the full message and fix suggestion appear in dev builds; error.code is the same in both.)

export const useRouter = defineInjectable<Router>('Router');

// Per app (per request under SSR):
const app = defineApp(<App />);
app.defineProvide(useRouter, () => createRouter(url));

// In any component:
const router = useRouter();

App-level provides are read live: a defineProvide call made after app.mount() is visible to components mounted afterwards. Component-tree provides (defineProvide in setup) always take precedence.

That generated suggestion assumes defineProvide is the remedy. For a pack whose injectable is satisfied by rendering something, pass your own hint — it replaces the suggestion, keeping the SIGX202 code and the name:

export const useScreen = defineInjectable<Screen>('Screen', {
    hint: 'useIsFocused must be called from a component rendered as a route by <Stack>.',
});

The hint is only read in dev builds, but the string literal lives in your module, so it ships in your production bundle regardless — gate it yourself (hint: __DEV__ ? '…' : undefined) if the bytes matter.

The dev SSR warning on the factory form names the injectable after factory.name, which inline arrow factories don't have. Pass { name } to name it — otherwise the warning falls back to the definition site (defined at file:line), captured dev-only:

export const useSessionStore = defineInjectable(() => createSessionStore(), { name: 'sessionStore' });

The warning is skipped on live clients. That check is isLiveClient(), not typeof window, so windowless-but-live runtimes (lynx, terminal, Web Workers) stay quiet — see the declareLiveClient() note in the async docs.

Dependency injection outside components

Use-functions from defineInjectable/defineFactory resolve to app-context instances inside components. Code that runs outside component setup — router navigation guards, socket handlers, entry-scope code — must opt in with app.runWithContext(fn), or it silently gets a separate realm-level fallback instance:

const useAuthStore = defineFactory(() => createAuthStore(), 'scoped');
const app = defineApp(<App />);

router.beforeEach((to) => {
  // Same instance the app's components see — not a realm copy.
  const auth = app.runWithContext(() => useAuthStore());
  if (!auth.isAuthenticated && to.meta.requiresAuth) return '/login';
});

The context applies only to the synchronous portion of the callback — after an await, re-enter with another runWithContext call (dev builds warn once per app when the callback returns a Promise or other thenable):

// ❌ Wrong — after the await, useSession() resolves a realm fallback, not the app's instance
await app.runWithContext(async () => {
  const user = await fetchUser();
  useSession().user = user; // context already restored here
});

// ✅ Right — re-enter for each synchronous section that resolves dependencies
const user = await fetchUser();
app.runWithContext(() => {
  useSession().user = user;
});

Nested calls restore the previous context. Plugins receive the app in install() and can capture it to wrap their own callbacks.

Libraries that run user-authored callbacks in app context (routers invoking navigation guards, schedulers running handlers) can tailor that warning with the asyncAdvice option, so the advice reaches the callback's author instead of pointing at a runWithContext call site they never see:

// A string replaces the remediation sentence of the warning…
app.runWithContext(() => userGuard(to, from), {
  asyncAdvice: '(from my-router) Resolve injectables at the top of the guard, before the first await.'
});

// …false suppresses it for a deliberately sync-only call (the once-per-app
// warning slot is not consumed — later unmarked async callbacks still warn).
app.runWithContext(fn, { asyncAdvice: false });

Dev-only; ignored in production builds. The warning still fires at most once per app across all callers.

Writing plugins

A plugin is a function or an object with install(app, options?), registered with app.use(). Inside install, app._context is the supported surface for wiring app-wide services — pass it to seam provide-helpers (e.g. provideAsyncEngine from sigx/internals) or use app.defineProvide for injectables. The underscore marks it as an advanced surface, not a private one; no cast is needed:

import { defineInjectable, type Plugin } from '@sigx/runtime-core';

class MyService {
  close() { /* release sockets, timers, … */ }
}

export const useMyService = defineInjectable(() => new MyService());

export const myPlugin: Plugin = {
  name: 'my-plugin',
  install(app) {
    const service = app.defineProvide(useMyService);
    app._context.disposables.add(() => service.close());
  }
};

Non-web renderers

This package references no web global unguarded — it runs anywhere. One thing renderer authors must know: useData/useStream only auto-run their sources on a live client, and without a declaration that is detected as "window exists" (which keeps server renders safe). A client runtime with no window (native, terminal) must say so once, from its platform-identity module:

import { declareLiveClient } from '@sigx/runtime-core/internals';

declareLiveClient(); // this runtime is a live client — keyed reads fetch on mount

Never call this from code a server render can evaluate (that would defeat the SSR guard) — it belongs in the module that defines your renderer's platform, the way @sigx/runtime-dom/platform defines the web's.

A declaration also stamps globalThis.__SIGX_LIVE_CLIENT__, which @sigx/server's live-client guard reads (rfc-server rev 2): a server function invoked in a declared live client throws instead of executing its body locally. The window fallback never stamps.

Note: Most users should install sigx instead, which bundles this package with a DOM renderer and the reactivity system.

📚 Documentation

The complete export list (component model, JSX runtime, lifecycle, lazy/Defer, DI, control flow, directives, error handling), guides and live examples → https://sigx.dev/core/packages/runtime-core/overview/

License

MIT