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

@streetui/renderer

v2.2.0

Published

StreetUI renderer — framework-owned DOM renderer, reconciliation, patching

Readme

@streetui/renderer

StreetUI's own DOM renderer. It mounts a compiled semantic graph into real DOM, patches individual nodes when signals change, reconciles keyed lists, renders the same graph to an HTML string on the server, and hydrates server HTML into a live app in the browser. There is no virtual DOM and no third-party SSR or hydration framework — browser and server share one pipeline (DSL → Compiler → Graph → Runtime → Renderer).

Browser mount

import { createRenderer } from '@streetui/renderer';

const renderer = createRenderer();                 // BrowserDOMAdapter by default
const handle = renderer.mount(compile(app), document.getElementById('app')!);
// …later
handle.unmount();                                  // disposes subscriptions + listeners

mount creates the full DOM tree, wires events, and opens signal subscriptions. When a signal changes, its subscriber patches only the affected node (setTextContent / setAttribute / setProperty) — no re-render, no diffing.

Server rendering — renderToString

import { renderToString } from '@streetui/renderer';

const html = renderToString(compile(app));         // "<h1>…</h1><section>…"
// or with an explicit adapter:
renderToString(compile(app), { domAdapter: new ServerDOMAdapter() });

renderToString runs the exact same mountGraph pipeline used in the browser, but against a ServerDOMAdapter — a lightweight in-memory node model that serializes to HTML. It emits only the application's own elements (the synthetic container is not serialized), so you embed the result wherever the client will mount, e.g. <div id="app">${html}</div>.

Void elements are emitted HTML5-style — <img src="…"> and <input …> with no self-closing slash and no closing tag. Text and attribute values are escaped, so markup is XSS-safe by construction.

Lifecycle (no leaked subscriptions). The synchronous mount opens signal subscriptions; on the server those would live forever. renderToString therefore disposes the root instance immediately after serializing — SSR has a render lifecycle only. Mutating a signal after renderToString returns is a safe no-op; the live runtime lifecycle is established later on the client by hydrate.

State transfer — serializeState / readState

Server-resolved state (e.g. a fetched list) crosses to the client as an inert JSON island rather than being re-fetched:

import { serializeState, readState } from '@streetui/renderer';
import { BrowserDOMAdapter } from '@streetui/dom';

// server:
const island = serializeState({ products: data });  // <script type="application/json" data-streetui-state>…</script>
// ship `html + island`

// client:
const state = readState(new BrowserDOMAdapter(), document);
const products = state['products'];

serializeState returns '' for an empty map, and escapes <, >, &, U+2028, and U+2029 as \uXXXX code points so the payload can never break out of the <script> (a literal </script> in the data is neutralized). readState is routed through a DOMAdapter, so it never assumes a global document and returns {} when the island is absent or unparseable — hydration then proceeds as a cold client render.

Hydration — hydrate

const renderer = createRenderer();
const handle = renderer.hydrate(compile(app), document.getElementById('app')!);

hydrate walks the graph top-down against the server DOM, matching each graph node positionally to one element by tag. Matching subtrees are adopted in place — the same element objects are reused, never deleted and recreated. A local mismatch (e.g. server emitted <h1> where the client graph expects <p>) repairs only that subtree via a fresh mount at that position; the rest of the app is untouched. Surplus server DOM beyond the graph is removed.

After hydration the app is fully live and behaves exactly like a browser mount:

  • Events are wired once (single listener per node — no duplicates).
  • Reactive text/attributes patch the same server node on signal change.
  • when() toggles its branch in place, even when the branch was absent from the server HTML.
  • Keyed lists (listOf) reorder by reusing the existing server <li> nodes rather than rebuilding them.
  • Controlled inputs adopt their server value and are two-way bound — typing updates the signal, and signal changes update the input.

runtime.hydrate uses this._renderer.hydrate ?? this._renderer.mount, so a renderer without hydration support degrades gracefully to a cold mount.

Router & resource hydration

mountRouter(router, { hydrate: true, container, shell }) hydrates the persistent shell and the initial route from the server HTML, resolving dynamic params and query the same way on both sides, then takes over client-side navigation (subsequent route changes mount fresh; the shell persists). See packages/router/README.md.

resource() accepts initialData / initialError / initialStatus so a server-resolved fetch seeds the client and is not repeated on hydration (pass immediate: true only if you deliberately want a client refetch). See packages/state/README.md.

Server environment & error handling

renderToString runs under plain Node.js with no window/document/location /localStorage assumptions (all DOM access goes through the adapter), so it is framework-neutral and drops into any Node HTTP layer (including StreetJS). Errors thrown while building the graph surface normally out of renderToString; the renderer does not swallow them. In the app itself, failures stay observable and contained via the errorBoundary DSL block (see the root README), which works identically after hydration.

Streaming (future)

Streaming SSR (flushing HTML in chunks / progressive hydration) is intentionally not implemented in v0.4. The current model renders a complete document and hydrates it in one pass. The ServerDOMAdapter + mountGraph split leaves room to add a streaming serializer later without a second renderer; it is tracked as future work.

Public API

renderToString, RenderToStringOptions, serializeState, readState, STATE_MARKER_ATTR, createRenderer (with mount / hydrate), and the RenderHandle returned by both. All rendering paths consume a CompiledApplication from @streetui/compiler.