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

@wcstack/server

v4.20.0

Published

Server-side rendering for wcstack Web Components.

Readme

@wcstack/server

🤖 AI coding agents: This README is a package-level reference, not the primary entry point for building a wcstack application. If you have not already done so, first read the repository README and AGENTS.md, then use the wcstack-app skill.

What if Web Components rendered on the server?

Imagine a future where your <wcs-state> templates are fully rendered before they reach the browser — data is fetched, bindings are resolved, lists are expanded, conditionals are evaluated. The user sees content instantly, and the client picks up exactly where the server left off.

That's what @wcstack/server explores. It runs your existing @wcstack/state templates through happy-dom, produces fully-rendered HTML with embedded hydration data, and lets the client resume reactivity with zero flicker. No special template syntax, no server-specific markup — just the same HTML you already write.

Features

Basic Features

  • Full Template Rendering: Runs @wcstack/state bindings server-side — text, attributes, for loops, if/elseif/else conditionals, filters, and mustache {{ }} syntax.
  • Automatic Hydration Data: Generates <wcs-ssr> elements containing the state's data and the page-level templates, and marks rows, branches and text bindings with comments, for seamless client-side hydration.
  • Async Data Fetching: Supports $connectedCallback with fetch() — the server waits for $connectedCallback and the elements that report readiness before it serializes, and — since 4.3 — for a request a binding started (see Server Communication with wcs-fetch).
  • RenderCore: A headless, event-driven rendering class that follows the wc-bindable protocol for observable html / loading / error state.
  • Zero Browser Dependencies: Runs in Node.js with happy-dom as the only runtime dependency.

Unique Features

  • Drop-in SSR: No changes to your client-side templates. Add enable-ssr to <wcs-state> and render with renderToString().
  • Template Fragment Preservation: Page-level for/if templates are kept in <wcs-ssr> under the ids their anchor comments name, so the client can put them back and keep rendering from them.
  • Form Values in the Markup: An input's value / checked, the options a select has selected and a textarea's text are written into the HTML, so the page shows them before the client binds it. Other DOM properties (e.g., innerHTML) are in the rendered HTML as they are, and the client applies every binding to the nodes it adopts.
  • wc-bindable Protocol: RenderCore exposes rendering state via the standard protocol, enabling the same bind() pattern on both server and client.

Installation

npm install @wcstack/server

Quick Start

renderToString() — One-shot rendering

import { renderToString } from "@wcstack/server";

const html = await renderToString(`
  <wcs-state json='{"items":["Apple","Banana","Cherry"]}' enable-ssr>
  </wcs-state>
  <ul>
    <template data-wcs="for: items">
      <li data-wcs="textContent: items.*"></li>
    </template>
  </ul>
`);

console.log(html);
// Fully rendered HTML with <wcs-ssr> hydration data

The same call doubles as a snapshot test for a page: expect(await renderToString(html)).toMatchSnapshot() in vitest pins the rendered markup without a browser. For headless DOM tests that also exercise writes and handlers, installGlobals() (below) is the globals swap to reuse — see Testing Your Page in the state README.

RenderCore — Observable rendering with caching

import { RenderCore } from "@wcstack/server";

const renderer = new RenderCore();

// Listen to state changes via wc-bindable protocol
renderer.addEventListener("wcs-render:loading-changed", (e) => {
  console.log("loading:", e.detail);
});

renderer.addEventListener("wcs-render:html-changed", (e) => {
  console.log("rendered:", e.detail.length, "bytes");
});

// Render and cache
await renderer.render(templateHtml);

// Subsequent reads use the cached result
console.log(renderer.html);

API Reference

renderToString(html: string, options?: RenderOptions): Promise<string>

Renders an HTML string containing @wcstack/state templates. Returns fully-rendered HTML with hydration data for any <wcs-state enable-ssr> elements.

Options:

| Option | Description | |--------|-------------| | url | Full URL of the request (e.g. "http://localhost:3000/products/1"). Reflected in window.location / document.baseURI. Required for server-rendering components that route on the URL. | | baseHref | Value for the <base href> injected into <head>. Defaults to "/" when url is given. Set it explicitly for sub-path deployments. | | baseUrl | Base URL for resolving relative fetch URLs. Defaults to the origin of url. | | bootstraps | Array of bootstrap functions (defaults to bootstrapState). Async loaders are allowed — packages whose classes extend HTMLElement at module scope cannot be imported top-level in plain Node, so pass e.g. async () => (await import('@wcstack/fetch')).bootstrapFetch(); the loader runs after DOM globals are installed. | | timeoutMs | How long to wait for the page to become ready, in milliseconds. Default DEFAULT_RENDER_TIMEOUT_MS (30 000). Exceeding it rejects with a message naming the cause. Pass 0 to disable the limit. Values that cannot work as a limit are treated as "no limit" too: NaN, anything ≤ 0, and anything above MAX_RENDER_TIMEOUT_MS (2 147 483 647) including Infinity — Node's setTimeout rounds a delay past 2³¹−1 down to 1 ms, so "raising" the limit that way would otherwise invert into an immediate timeout. | | resolveModule | Since 4.3. (url: URL) => string \| URL \| null — the local file a <wcs-state src> module is read from on the server: it gets the URL the page names (resolved against baseUrl / url) and returns a path or a file: URL, e.g. (url) => join(publicDir, url.pathname). Node's module loader reads file: and data: URLs only. Without it, or when it returns nothing, the state fails to initialize with a message saying so. The module is imported anew for each render, as an inline state is, so no state object is shared between requests. A .json src is fetched, as before. |

Why there is a limit at all. renderToString swaps globalThis, so renders are serialised through one mutex, released in a finally. Before the limit, a page that never became ready never reached that finally — one bad page left every later render on the process hanging forever. The limit guarantees the mutex comes back.

The budget is shared between waiting for readiness and the cleanup drain in finally; the drain gets whatever is left, but never less than CLEANUP_MIN_TIMEOUT_MS (1 000 ms), since a zero budget would mean "no limit" again. Worst-case wall time is therefore timeoutMs + 1 000 ms. With timeoutMs: 0 the cleanup is unbounded as well — that is what disabling the limit means.

Long-lived processes. The last cleanup step asks the state provider to drop what it kept for that render (reset() on the ssr-snapshot builder). @wcstack/state 4.0 restarts its template ids there, so each render numbers its templates from wcs-t0 again instead of counting up across renders. It runs after the window is closed, and it is optional: a provider without it is simply not asked. A snapshot is always scoped to its own document either way.

Rendering pipeline:

  1. Creates a happy-dom window and installs browser globals
  2. Parses HTML and triggers connectedCallback on all <wcs-state> elements
  3. Awaits all $connectedCallback promises (including fetch() calls)
  4. Waits for buildBindings to complete
  5. Calls the snapshot builder of @wcstack/state, which writes form values into the markup and generates <wcs-ssr> elements for states with enable-ssr
  6. Restores globals and returns the rendered HTML

RenderCore

Headless rendering class extending EventTarget. Implements the wc-bindable protocol.

| Property | Type | Description | |----------|------|-------------| | html | string \| null | Rendered HTML (cached after render()) | | loading | boolean | true while rendering is in progress | | error | Error \| null | Error from the last render() call, if any |

| Method | Returns | Description | |--------|---------|-------------| | render(html) | Promise<string \| null> | Renders the template and caches the result. Returns null on error. |

| Event | Detail | Description | |-------|--------|-------------| | wcs-render:html-changed | string | Fired when rendering completes successfully | | wcs-render:loading-changed | boolean | Fired when loading state changes | | wcs-render:error | Error | Fired when rendering fails |

wc-bindable declaration:

static wcBindable = {
  protocol: "wc-bindable",
  version: 1,
  properties: [
    { name: "html", event: "wcs-render:html-changed" },
    { name: "loading", event: "wcs-render:loading-changed" },
    { name: "error", event: "wcs-render:error" },
  ],
};

Helper Functions

| Function | Description | |----------|-------------| | installGlobals(window) | Installs happy-dom globals on globalThis. Returns a restore function. | | waitForReady(root, { maxIterations? }) | Awaits every custom element under root (a document or a ShadowRoot) that follows the readiness protocols — static hasConnectedCallbackPromise elements' connectedCallbackPromise (re-scanning while new elements appear, e.g. <wcs-router>'s initial route), then static getBindingsReady(root) (<wcs-state>'s binding construction). This is the wait renderToString performs before serializing; @wcstack/testing's mount() reuses it. Rejects if binding initialization fails. |

Constants

| Name | Description | |------|-------------| | GLOBALS_KEYS | Array of browser global keys installed during SSR (document, HTMLElement, Node, etc.) | | VERSION | Package version string from package.json | | DEFAULT_RENDER_TIMEOUT_MS | Default of RenderOptions.timeoutMs (30 000) | | MAX_RENDER_TIMEOUT_MS | Largest delay Node's setTimeout honours (2 147 483 647); anything above is treated as "no limit" | | CLEANUP_MIN_TIMEOUT_MS | Floor for the cleanup drain's share of the budget (1 000) |

SSR Output Structure

When a <wcs-state> has the enable-ssr attribute, renderToString() inserts a <wcs-ssr> element immediately before it, holding the version of @wcstack/state that rendered the page, the state's data and the page-level templates. The rendered content stays where it is, marked with comments:

<!-- Generated by renderToString() -->
<wcs-ssr version="4.0.0">

  <!-- State snapshot -->
  <script type="application/json">{"items":["Apple","Banana","Cherry"]}</script>

  <!-- Page-level templates, by the id their anchor comment names -->
  <template id="wcs-t0" data-wcs="for: items">
    <li data-wcs="textContent: items.*"></li>
  </template>

</wcs-ssr>

<wcs-state json='...' enable-ssr></wcs-state>

<!-- Rendered output (visible immediately) -->
<ul>
  <!--wcs-p:wcs-t0--><!--wcs-[--><!--wcs-|--><li>Apple</li><!--wcs-|--><li>Banana</li><!--wcs-|--><li>Cherry</li><!--wcs-]-->
</ul>

| Marker | Meaning | |--------|---------| | <!--wcs-p:ID--> | Where a page-level for: / if: / elseif: / else: template was. The template is in <wcs-ssr> as <template id="ID"> (wcs-t0, wcs-t1, … in each render) | | <!--wcs-[--> … <!--wcs-]--> | A list's rows, right after its anchor; each row starts with <!--wcs-\|--> | | <!--wcs-[:i--> … <!--wcs-]--> | The branch an if: / elseif: / else: chain rendered (i is its position in the chain, from 0), right after the chain's last anchor. No region when no branch is rendered | | <!--wcs-t:EXPR-->value<!--wcs-/t--> | A text binding at page level ({{ }} or <!--@@: -->). EXPR is its whole expression, filters included, URI-encoded | | <!--wcs-s--> / <!--wcs-e--> | Separates two adjacent text nodes / stands for an empty one, so that parsing the HTML gives back the same text nodes | | data-wcs-raw="…" | On a text-only element (<textarea>, <title>) with {{ }} in it: its content as written, which the client puts back |

Inside a row or a branch, a nested for: / if: keeps its own anchor comment, and its region follows it in the same way.

  • Keep the comments in the output. Removing them in a later minification step (removeComments and the like) breaks hydration. Their format is not an API either: do not post-process the output based on it.
  • There is no property value table: what a binding rendered (innerHTML included) is in the HTML as it is, and the client applies every binding to the nodes it adopts. 3.x's data-wcs-ssr-id attributes and <script type="application/json" data-wcs-ssr-props> are gone.
  • Form values go into the markup where they differ from it, on any page with a <wcs-state> (with or without enable-ssr): an input's value and checked attributes, selected on the options a select has selected, a textarea's text. A password's value is left out (the client's binding fills it in), and so is a textarea value that contains {{.
  • What a reader types before the client binds the page is kept — the HTML is usable at once, the bundles may take seconds. Since 4.3, a form control whose value is not the one its markup gives when the client starts (a browser restoring a form on reload counts too) gets that value back once the page is bound, with an input and a change, as the reader's own edit would: a two-way binding (value:, checked:, radio:) writes it to the state. Before, binding the page wrote the state's value over it without a word. A control in a row the client renders anew (a snapshot it discarded) is not kept.
  • <wcs-ssr> is generated for a root <wcs-state enable-ssr> only, not for a volume (mount="…") or a component's <wcs-state bind-component>. A volume's data is in its root's snapshot.

The client-side @wcstack/state reads the <wcs-ssr> element during hydration, restores state and templates, and resumes reactivity without re-rendering.

Server and client versions

<wcs-ssr version> is the version of @wcstack/state that rendered the page, and the client hydrates only a snapshot of its own major.minor. Deploy @wcstack/server and the client together, of the same major.minor.

  • The server renders with the @wcstack/state of its own version. The engine that renders the page — the version <wcs-ssr version> names — is the @wcstack/state npm installs. Since 4.3, @wcstack/server depends on it at exactly its own version, so pinning the server pins the engine. Up to 4.2 it depended on it with a caret (^4.2.0), and a fresh install after a release rendered with the new minor: pin @wcstack/state next to the server there. Take the client's version from <wcs-ssr version> in the output (as examples/ssr does), so the client cannot drift from the engine.
  • Never serve a client older than the server's minor. It discards the snapshot, and a binding written for the newer minor (a filter argument it does not know, for example) fails its initialization. Since 4.4 the client plans the page before it takes the server's rows and branches away, so the page stays as the server rendered it — readable, but nothing in it responds. A client up to 4.3 fails after it took them away: the page is left without them. A client of a newer minor discards the snapshot too and renders the page again, so everything $connectedCallback loads is fetched again in the browser.
  • A 4.0 client discards the output of a 3.x server. It warns <wcs-ssr version="3.5.0"> does not match 4.0.0: its snapshot is discarded, and the page renders on the client from its own state. and renders the page on the client as if there were no server: the state loads from its own source, $connectedCallback runs on the client, and the server's rows, branches and markers give way to the templates they were rendered from. With the output of a 3.x server up to 3.5.2, a text binding outside templates also loses its filters (the warning says so). Purge HTML cached from a 3.x server when you switch.
  • A 4.0 server with a 3.x client is not supported: the 3.x client does not read the 4.0 markers.

The details are in the migration guide §3.6.

Server Integration Example

import { createServer } from "node:http";
import { RenderCore } from "@wcstack/server";

const renderer = new RenderCore();

const template = `
  <wcs-state enable-ssr>
    <script type="module">
      export default {
        async $connectedCallback() {
          const res = await fetch("http://localhost:3000/api/data");
          this.items = await res.json();
        },
        items: []
      };
    </script>
  </wcs-state>
  <ul>
    <template data-wcs="for: items">
      <li data-wcs="textContent: items.*"></li>
    </template>
  </ul>
`;

createServer(async (req, res) => {
  if (!renderer.html) {
    await renderer.render(template);
  }
  res.writeHead(200, { "Content-Type": "text/html" });
  res.end(renderer.html);
}).listen(3000);

Input HTML Rules

  • Pass only the contents of <body> — do not include <html>, <head>, or <body> tags.
  • <script> / <link> external resource loading is not executed. → Provide required packages via options.bootstraps. Since 4.11 this holds for stylesheets too: the server runs on happy-dom 21, which loads no CSS file and no iframe page. Up to 4.10, on happy-dom 20, a <link rel="stylesheet"> in the HTML was fetched by the server — a request to any URL the HTML names, when it renders HTML it does not trust (GHSA-c8p4-j97h-3vg8).

What SSR Can Do

State Initialization & Data Fetching

  • Load <wcs-state> from a json attribute, an inline <script type="module">, or a src attribute — the form the documents prefer under a strict CSP. For src, since 4.3, pass resolveModule to map the URL the page names to a local file (Node's module loader reads only file: and data: URLs). Up to 4.2 a src state could not be server-rendered, and the render failed with ERR_UNSUPPORTED_ESM_URL_SCHEME
  • Execute $connectedCallback for server-side fetch (API calls, etc.)
<!-- Direct JSON -->
<wcs-state enable-ssr json='{"title":"Hello"}'></wcs-state>

<!-- Fetch data from API in $connectedCallback -->
<!-- $connectedCallback is defined as a method on the state object; `this` is the state proxy -->
<wcs-state enable-ssr>
  <script type="module">
    export default {
      async $connectedCallback() {
        const res = await fetch('/api/users');
        this.users = await res.json();
      }
    };
  </script>
</wcs-state>

Server Communication with wcs-fetch

  • <wcs-fetch> auto-fetch (without manual) also executes on the server. A url written in the markup is waited for; a url that a binding sets from state (data-wcs="url: articleUrl") starts its request during the render. Since 4.3, the render waits for it too, within timeoutMs: its value is in the page and in the snapshot, and so is what it renders — a <wcs-fetch> in those rows is waited for in turn. A request that has not finished when the limit runs out is left out with a warning ([wcs/ssr-pending-timeout]), and loads on the client; the render does not fail for it. Up to 4.2 the render did not wait for it, and the page was served without its value.
  • When the render is done, the server takes the page's elements out of the document before it puts the globals back, so that each element's own cleanup (a <wcs-fetch> aborting its request, a <wcs-timer> stopping) runs inside the render. A request or a timer that outlived the render used to throw into the process (fixed in 4.3).
  • On the client, a <wcs-fetch> sends its request when it connects or when hydration writes its url: it does not know what the snapshot holds, so data the server loaded is fetched again. To load it once, give the element manual and send it with trigger when the page needs it, or return null from its url getter while the state already holds the data for that url (keep the id the data belongs to).
  • Use manual + $connectedCallback for explicit control:
<wcs-fetch id="api" url="/api/users" manual></wcs-fetch>
<wcs-state enable-ssr>
  <script type="module">
    export default {
      async $connectedCallback() {
        const el = document.getElementById('api');
        this.users = await el.fetch();
      }
    };
  </script>
</wcs-state>

Note: bootstrapFetch must be included in the bootstraps option.

Bindings & Structural Rendering

  • data-wcs binding application (text, attribute, class, style, property)
  • <template data-wcs="for:"> / if: / elseif: / else: structural rendering
<ul>
  <template data-wcs="for: users">
    <li data-wcs="textContent: .name"></li>
  </template>
</ul>
<template data-wcs="if: isAdmin">
  <div class="admin-panel">...</div>
</template>

Hydration

  • Automatic <wcs-ssr> metadata generation for <wcs-state enable-ssr>
  • Client-side hydration restores bindings without re-rendering: the server's rows and branches, nested ones included, are adopted where they are
  • A <wcs-state> without enable-ssr is still rendered on the server, but it gets no <wcs-ssr> and is not hydrated: on the client only its data-wcs attribute bindings outside templates follow the state, while its {{ }} text, rows and branches stay as the server rendered them
  • The client of a hydrated page does not run $connectedCallback (the server did). What only the browser can read — localStorage, the viewport, a media query — is not read there on the client, and a guarded read in it on the server finds nothing: read it through an element (<wcs-storage>, <wcs-media-query>), whose value reaches the state on the client, or in a $watch handler, which runs on the client only.
  • date, time and datetime format in the time zone of the runtime: the server's on the server and the reader's on the client, so the text can change at hydration. For one text, format in a getter with an explicit timeZone (new Intl.DateTimeFormat(locale, { timeZone })).

Custom Element Waiting

  • Automatically awaits all custom elements with static hasConnectedCallbackPromise = true
  • Uses a stabilization loop: after awaiting, re-scans the DOM for newly added custom elements (up to 10 iterations), ensuring elements dynamically inserted during $connectedCallback are also awaited
  • Then waits for the work an element started after it connected, since 4.3: an element whose class has static hasPendingPromise = true reports it as its pendingPromise (null when there is none; <wcs-fetch> reports its request, settled whether it succeeds or fails). This is waited for within what is left of timeoutMs, and what it rendered is waited for in turn. When the limit runs out, the page is rendered without it, with a warning. waitForReady() (and @wcstack/testing's mount()) does not take this step

Router SSR

Put enable-ssr on <wcs-router> and pass the request url (design: docs/ssr-router-design.md (ja)):

const body = await renderToString(template, {
  url: `http://localhost:3000${req.url}`,
  bootstraps: [
    // classes extending HTMLElement cannot be imported top-level in plain
    // Node — the async loader runs after DOM globals are installed
    async () => (await import('@wcstack/state')).bootstrapState(),
    async () => (await import('@wcstack/router')).bootstrapRouter(),
  ],
});
  • The initial route of the request URL is rendered on the server — typed params, nested routes, and structural templates (for: / if:) inside route content included.
  • The client-side router adopts the server-rendered DOM instead of re-rendering it; state bindings hydrated on those nodes stay live. If anything about the server output doesn't verify (URL mismatch, edited templates), the client silently falls back to normal client-side rendering.
  • <wcs-ssr> snapshots are built as a server-orchestrated final pass (the ssr-snapshot protocol), so hydration data always includes route content regardless of load order.
  • <wcs-link> anchors are server-rendered (with active / aria-current) and adopted on the client — in for: rows and if: branches too.
  • Sub-path deployments: pass baseHref and serve a matching <base href> to the client.

What SSR Cannot Do

  • Execute <script src="..."> or <link> in <head>
  • Access browser-specific APIs (localStorage, sessionStorage, navigator, etc.)
  • Render Shadow DOM (Declarative Shadow DOM not supported)
  • Register event handlers (restored via client-side hydration)
  • Load components dynamically via <wcs-autoloader>
  • Apply outerHTML: / outerText: (they are applied on the client: the server output contains the element as written, so the value is not in the HTML that search engines or no-JS views see)
  • Render the children that a Light DOM custom element bound at page level renders from a value (the client renders them; children you wrote stay in the output)
  • Run $watch handlers or $stream sources (on the server, streams keep their initial value; both start on the client)
  • Server-render guarded routes (by design — the guard is an authorization point that runs client-side; the outlet is left empty)
  • Adopt <wcs-layout> routes: the server renders them, and the client renders them again — the server's nodes are replaced, so what a reader typed into them before the client loaded is lost
  • Put <wcs-head> contents in the output's <head> (the output is the body only): they stay in the route's markup, where the client applies them to document.head when it adopts the route. Write the <title> and description a crawler should see in the HTML you wrap the output in

HTML Splitting Pattern

renderToString receives only the <body> contents. Wrap the result with <head> and <script> tags on the outside:

// server.js
const ssrBody = await renderToString(template, {
  baseUrl: 'http://localhost:3001',
});
const page = `<!DOCTYPE html>
<html lang="en">
<head>
  <script type="module" src="/packages/state/dist/auto.min.js"></script>
</head>
<body>${ssrBody}</body>
</html>`;

Using Multiple Packages

const ssrBody = await renderToString(template, {
  baseUrl: 'http://localhost:3001',
  // async loaders: these packages cannot be imported top-level in plain Node
  bootstraps: [
    async () => (await import('@wcstack/state')).bootstrapState(),
    async () => (await import('@wcstack/fetch')).bootstrapFetch(),
  ],
});

How It Works

Rendering Pipeline

  1. Global Setup: Creates a happy-dom Window and temporarily installs browser globals (document, HTMLElement, MutationObserver, etc.) on globalThis. Disables URL.createObjectURL to force the base64 data URL fallback for inline scripts.

  2. SSR Mode: Sets data-wcs-server attribute on the <html> element. @wcstack/state detects this attribute to enable SSR behavior.

  3. Bootstrap: Calls user-provided bootstrap functions (defaults to bootstrapState() if omitted).

  4. HTML Parse & Callback: Sets document.body.innerHTML, which triggers happy-dom's element lifecycle. Each <wcs-state> loads its data source and runs $connectedCallback. Awaits all custom elements with hasConnectedCallbackPromise via a stabilization loop — after each await, re-scans the DOM for newly added elements (up to 10 iterations).

  5. Ready: Awaits getBindingsReady() of every custom element class that has it (<wcs-state>'s binding construction, see waitForReady) — text interpolation, attribute mapping, list expansion, conditional evaluation.

  6. SSR Metadata: Calls the snapshot builder that @wcstack/state installed (the ssr-snapshot protocol) as a final pass. It writes form values into the markup, moves each list's rows and the rendered branch into a marked region right after its anchor, and inserts a <wcs-ssr> element before each root <wcs-state enable-ssr>.

  7. Cleanup: Restores original globals and closes the happy-dom window.

Client-Side Hydration

The client-side @wcstack/state detects <wcs-ssr> elements and, when the version has the client's major.minor:

  1. Loads the state from its own source and takes the snapshot's data over it (getters, setters and methods stay), and skips $connectedCallback, which the server ran
  2. Puts the templates back at their anchors (<!--wcs-p:ID-->) and turns the text markers back into bindings
  3. Adopts the server's rows and branches where they are and applies every binding to them — a custom element in them is connected once and never disconnected. A server row that no longer fits its template is rendered again in its place; rows and branches the client does not render go as soon as the page is bound
  4. Resumes normal reactive binding

The rendered DOM is visible immediately — hydration only wires up interactivity. A snapshot of another major.minor is discarded and the page renders on the client (Server and client versions).

License

MIT