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/server-renderer

v0.15.5

Published

Server-side rendering and client hydration for SigX

Readme

@sigx/server-renderer

Server-side rendering and client hydration for SignalX. Supports streaming and string-based rendering, a plugin-driven architecture, and head management.

The SSR platform's boundary model lives here: plugins decide a component's flush (inline / stream / skip) and hydrate (load / idle / visible / media / interaction / never) axes through the pre-setup resolveBoundary hook, core records them in the per-request __SIGX_BOUNDARIES__ table, and the built-in boundary hydrator schedules each one client-side — selective hydration without a framework switch. @sigx/ssr-islands is the first-party pack on these seams.

Packs install on the APP — app.use(pack()) is the one install shape (#413). A pack's install(app) registers its server render hooks through the provideSSRPlugin seam, and every render method that receives the App (createSSR().render(app), the request/fetch handlers' app option) picks them up. createSSR({ plugins }) remains as the instance-level channel for engine internals, tests, and custom engines; there is no SSRInstance.use().

📚 Full guides, API reference and live examples → https://sigx.dev/server/

Install

npm install @sigx/server-renderer

A taste

import { renderToStream } from '@sigx/server-renderer/server';
import App from './App';

// Streaming (recommended)
const stream = renderToStream(<App />);
// Client hydration
import { defineApp } from 'sigx';
import { ssrClientPlugin } from '@sigx/server-renderer/client';
import App from './App';

defineApp(<App />).use(ssrClientPlugin).hydrate('#root');

The request handler

Production servers are static assets plus one handler over the public document API — crawlers get blocking documents, everyone else shell-first streaming, with useResponse's status/headers/redirect written before the first byte (the dev twin lives in @sigx/vite/ssr):

import { createRequestHandler } from '@sigx/server-renderer/node';

app.use(createRequestHandler({
    template,
    app: (url) => createApp(url),   // fresh app per request
    document: { assets }            // manifest preloads (collectAssets)
}));

The fetch handler (edge runtimes)

createFetchHandler is the WinterCG sibling (rfc-deploy §2) — the same dispatch decisions expressed as (Request) => Promise<Response>, the shape Cloudflare Workers, Deno Deploy, Bun, Vercel Edge, and Netlify all consume natively. It lives on the WinterCG-clean ./server entry (and the root .), so it runs wherever the render path runs:

import { createFetchHandler } from '@sigx/server-renderer/server';

const handler = createFetchHandler<{ env: Env; ctx: ExecutionContext }>({
    template,
    app: (url) => createApp(url),   // same frozen entry contract
    document: { assets }
});

export default {
    // static assets → server functions → document render: the composition
    // stays in YOUR entry — no sigx handler serves files or mounts the
    // server-fn endpoint for you.
    fetch: (request: Request, env: Env, ctx: ExecutionContext) =>
        handler(request, { env, ctx })
};

The second argument is the platform context — opaque to sigx, threaded verbatim into the template/app/document callbacks. It is optional under the default TPlatform = unknown (Deno/Bun entries pass nothing); instantiating the generic with typed bindings makes it required, so forgetting Cloudflare's { env, ctx } is a compile error rather than an undefined at render time. A redirect from useResponse returns a bodyless Response and releases the render; cancelling the Response body (client disconnect) does the same. A shell failure yields a minimal 500 — there is no next() in the fetch world; a custom error page is a try/catch wrapper around the returned handler.

Shared with the Node handler: defaultIsBot (the crawler regex behind the bot → blocking dispatch) and chunksToBytes (the pull-based string-chunks → UTF-8 ReadableStream<Uint8Array> encoder under both the fetch handler and renderDocumentToWebStream) — both exported from ./server for hand-written servers.

Runtime portability & request isolation

The . and ./server entries are WinterCG-clean — no Node builtins on the string / Web-stream / document paths, verified in CI by an edge smoke test that forbids every node: import while streaming a document through the production dist. Node-only Readable shapes (renderToNodeStream, renderDocumentToNodeStream, the toNodeStream adapter) live in @sigx/server-renderer/node.

Request isolation is a contract, not a runtime feature: the per-request SSRContext is the isolation mechanism — AsyncLocalStorage is never required. Everything a request collects (head configs, response state, async results, the boundary table) lives on its own context, created per render call; concurrent renders share nothing. There is no AsyncLocalStorage anywhere on the render path and never has been — reading getCurrentInstance() from an async continuation (after the first await of an async setup, or from a fetcher) is unsupported on the server exactly as it is in the browser: the current instance is one module-level slot, live only for the synchronous span of setup(). Resolve everything instance-dependent synchronously in setup and close over the result.

Request-state registration (packs)

useData/useStream values reach the client automatically through the __SIGX_ASYNC__ blob. A pack that owns request-scoped state of its own — @sigx/store's ssrState is the canonical case — enters the same blob through the public writer on the per-request context (#407):

// inside a component setup, during a server render
const ssrCtx = getCurrentInstance()?.ssr?._ctx;   // typed on SSRHelper
ssrCtx?.registerSerializedState('store:cart', {
    toJSON: () => snapshot(state)   // encoded at EMIT time, so state
});                                 // mutated later serializes final

Emission is handled by stateSerializationPlugin (on by default under renderDocument): shell-walk registrations ship with the shell blob; stream-phase registrations (a store first created below a streamed boundary) ship with the next flush; the onStreamEnd plugin hook — the request's last emission point — is a final drain that guarantees delivery. Keys share the useAsync/useStream namespace, so prefix them (store:cart). Re-registering an emitted key ships a patch; the client merge is last-write-wins. The client half is peekRestored / invalidateRestored / reviveFromServer from @sigx/runtime-core/internals — the blob's single accessor (docs/seams.md). reviveFromServer — the boundary codec's revive half — is also a public export of ./client, for pack code that decodes serialized boundary state itself.

The pack contract (SSRContext accessors)

Strategy packs — first-party (@sigx/ssr-islands, @sigx/resume) and third-party alike — build on the same typed public surface; nothing they need lives behind underscore fields or internals imports (#416):

  • ctx.currentComponentId() — the id of the component currently rendering. resolveBoundary and transformComponentContext run after the component's id is pushed, so this is "the component this hook call is about".
  • ctx.boundaries() — the per-request boundary table as a ReadonlyMap<number, SSRBoundaryRecord> (live, not a snapshot), for whole-table scans: islands' "any schedulable island?" preload check, resume's refresh-envelope encoding. Mutate individual records through ctx.getBoundary(id); the table's shape belongs to core.
  • createSSRContext({ appContext }) — seed a self-created context with an app's DI (type handlers, provides). A boundary refresh re-rendering in a fresh context passes the app context here instead of writing a private field.
  • SSRPack — the factory return type for packs installed with app.use(pack): the one object that is both the SSRPlugin (render and hydration hooks) and the app plugin (install(app) registers it via provideSSRPlugin(app._context, pack)).

The eager scheduler vs the lazy hydration core

Client-side selective hydration is split in two, so deferred pages execute zero runtime JS at load:

  • @sigx/server-renderer/client/scheduler — the eager half: reads the boundary table, wires load/idle/visible/media/interaction triggers, listens for streamed boundaries. It imports nothing from the sigx family (~2 kB, size-limit-guarded). The table surface is an accessor pair: reads (getBoundaryTable/getBoundaryRecord) and writes (installBoundaryRecords/removeBoundaryRecord — how a single-flight refresh patch enters the table and a swapped-out boundary retires, rfc-server §6.3).
  • The hydration core — the executor (hydrateComponent, the renderer, mount/hydrate primitives) lives in a separate chunk that loadHydrationCore() dynamically imports on the first strategy that actually fires. The component chunk and the executor fetch in parallel.

registerClientPlugin (part of the scheduler surface) accepts either a plugin object or a lazy source — { name, load: () => import('...') } — resolved together with the hydration core, so a pack's client hooks ride the same lazily-fetched chunk as the renderer. Registrations dedupe by name, first-wins. Packs that hydrate through their own wake-up (rather than the boundary scheduler) should register plugin objects, or await resolveClientPlugins() before hydrating.

Server plugins can contribute <link rel="modulepreload"> URLs to the document shell via the optional assets(ctx) hook — the intended pairing is to preload the lazily-imported runtime chunk whenever the request recorded schedulable boundaries, keeping the fetch off the critical path while execution still waits for the first trigger.

The ./client barrel re-exports the full surface (scheduler + executor + ssrClientPlugin) for app-rooted hydration, where the runtime is loaded by definition.

📚 Documentation

Streaming and string rendering, the plugin system, hydration, head management — full guides, the complete API reference and live examples → https://sigx.dev/server/

License

MIT