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

lwn-js

v0.3.1

Published

A high-performance TypeScript UI engine with direct component updates, explicit lifetimes, and server-side rendering.

Readme

LWN

npm version

Please, note - work in progress

SSR IS EXTREMELY EARLY ACCESS AND SHOULD BE USED WITH CAUTION - MUCH OF IT WILL BE FULLY REPLACED IN THE NEXT VERSIONS

LWN is a TypeScript UI engine for building component-based interfaces. Components create logical views and frames that define lifetimes for child components, listeners, and subscriptions. A renderer decides how views are placed. The included DOM renderer supports browser interfaces and server rendering.

Get started

Install the package:

npm install lwn-js

Select the DOM renderer, create a component, and mount its view:

import {
  component,
  destroy,
  domRenderer,
  listen,
  setRenderer,
} from 'lwn-js/core';
import { html } from 'lwn-js/html';

setRenderer(domRenderer);

const Counter = component(() => {
  let count = 0;
  const output = html`<output>0</output>`;
  const increment = html`<button>Increment</button>`;

  listen(increment, 'click', () => {
    count += 1;
    output.textContent = String(count);
  });

  return {
    node: html`<section>
      <h1>Counter</h1>
      ${output}${increment}
    </section>`,
  };
});

const root = document.querySelector('#app');
if (!root) {
  throw new Error('Missing #app');
}

const counter = Counter();
root.append(counter.node);

export function unmount() {
  destroy(counter);
}

The component keeps its private count in a local variable. Its click listener is tied to the component frame and is removed when the component is destroyed.

Components and frames

Create a component with component(). Each call to its factory creates a new component instance and returns a controller for that instance. A controller describes one rendered root with node or several roots with nodes. Pass the controller to destroy() to remove its views, destroy its logical descendants, and run its cleanup hooks.

Component factories called during another component's setup become its logical children. Use attach(parent, child) and detach(child) when a logical relationship needs to change while both components remain alive. Logical attachment determines lifetime and frame lookup. A renderer or an outlet determines where views appear.

Use onAttach() for work that depends on the current logical parent. The hook runs after attachment and its cleanup runs on detach. This is useful when a component can move between parents and needs to resolve a different store after each move.

A frame is active only during synchronous engine callbacks. Event listeners registered with listen() and subscriptions created during setup are re-entered in their component frame automatically. If external asynchronous code needs to create frame-bound resources, capture getFrame() before starting it and use withFrame(frame, callback) in the callback.

State and shared services

Keep state local when only one component uses it. Use createState() for a value shared by multiple parts of the interface, and createEmitter() for events. Subscriptions made during component setup are removed when that component is destroyed.

Use a store to provide a service to logical descendants:

import {
  attachStore,
  component,
  createState,
  createStore,
  useStore,
} from 'lwn-js/core';
import { html } from 'lwn-js/html';

const Theme = createStore(() => createState('slate'));

const Toolbar = component(() => {
  const theme = useStore(Theme);
  return {
    node: html`<button>${theme.get()}</button>`,
  };
});

const App = component(() => {
  attachStore(Theme);
  return { node: html`<main>${Toolbar()}</main>` };
});

A store is found by walking the logical parent chain. Components that may move between parents should resolve parent-provided stores after attachment, for example inside onAttach().

Navigation

Routes match paths and run actions for the active route. An outlet shows one component at a time and destroys the previous view:

import { component } from 'lwn-js/core';
import { html } from 'lwn-js/html';
import { createOutlet, setupRouter } from 'lwn-js/router';

const HomeRoute = { path: '/' };
const SettingsRoute = { path: '/settings' };
const router = setupRouter([HomeRoute, SettingsRoute]);

const Home = component(() => ({
  node: html`<h1>Home</h1>`,
}));

const App = component(() => {
  const slot = html<Comment>`<!---->`;
  const outlet = createOutlet(slot);

  router.route(HomeRoute, () => outlet.show(Home));
  router.route(SettingsRoute, () => outlet.show(() => import('./Settings')));

  return { node: html`<main>${slot}</main>` };
});

Route actions registered during component setup follow that component's lifetime. Lazy imports keep a page out of the initial bundle until it is needed.

Views and renderers

The core works with logical views and a renderer interface. setRenderer() selects the renderer used by outlets and view cleanup. The included domRenderer places DOM views in browser or server documents. Implement the Renderer interface to use another view system.

The lwn-js/html package provides html(), mhtml(), element(), and text(). html() requires exactly one root, while mhtml() returns all roots. Interpolated strings become text, and interpolated views are inserted as views.

Server rendering and hydration

The lwn-js/ssr and lwn-js/server packages provide server data, server rendering, static generation, incremental regeneration, and hydration. Declare typed data with serverToken(), load it in server routes, and read it synchronously during component setup with useServer().

Hydration expects the client to build the same view structure as the server. Keep browser-only values such as local storage, random IDs, and viewport state out of the initial structure. Apply browser-specific state after hydration. The SSR shop example demonstrates the server and client entry points, route data, caching modes, and hydration.

Packages

| Import path | Provides | | ------------- | ---------------------------------------------------------------------------- | | lwn-js/core | Components, frames, lifecycle hooks, state, stores, listeners, and renderers | | lwn-js/router | Routes, history adapters, and outlets | | lwn-js/html | HTML templates and view helpers | | lwn-js/ssr | Hydration and server data helpers | | lwn-js/server | Server rendering, static generation, caching, and the Node adapter |

Core has no runtime dependencies. The server package uses Vite and LinkeDOM as optional peer dependencies.

Examples

  • hello-world: a counter with button and keyboard input.
  • simple-app: persistent todo lists with routes and shared state.
  • complex-app: a peer-to-peer chat with detachable conversations.
  • ssr-app: a shop demonstrating server rendering and hydration.

Development

From the repository root:

npm install
npm run typecheck
npm run lint
npm test
npm run build