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

aeico-ssr

v0.1.4

Published

Server-side rendering for Aeico — renderHtml() and renderToString()

Readme

aeico-ssr

Server-side rendering for Aeico — serialize html() templates and Aeico components to HTML strings without any DOM dependency.

Safe to use in Node.js, Edge Runtime (Cloudflare Workers, Vercel Edge Functions), and at build time (SSG).


Installation

npm install aeico-ssr

aeico-ssr is a standalone optional package. The core aeico-view package has zero dependency on it — install it only when you need server-side or build-time HTML generation.

Peer dependencies

| Package | Required | When | |---|---|---| | aeico-view | Yes | always | | aeico-element | optional | only for renderToString |


API

renderHtml(result)

Serializes a RenderResult (produced by html()) to an HTML string.

import { html } from 'aeico-view';
import { renderHtml } from 'aeico-ssr';

const result = html(({ div, span }) => {
  div({ className: 'card' }, () => {
    span({ text: 'Hello SSR' });
  });
});

renderHtml(result);
// '<div class="card"><span>Hello SSR</span></div>'

Serialization rules

| Prop | Behaviour | |---|---| | text / textContent | Emitted as HTML-escaped text content | | className / class (string) | Emitted as class="…" | | className / class (object) | Truthy keys are joined: { active: true, hidden: false }class="active" | | style (object) | Serialized as inline style: { color: 'red' }style="color:red" — camelCase keys are converted to kebab-case; CSS custom properties (--foo) are kept as-is | | key | Emitted as data-key="…" | | disabled: true / boolean true | Presence-only attribute (e.g. disabled) | | disabled: false / null / undefined | Attribute omitted entirely | | @click / any @… event handler | Stripped — events are client-only | | Complex object props (arrays, DOM refs) | Skipped — cannot be serialized as HTML attributes | | Void elements (br, hr, input, …) | No closing tag | | camelCase tag name (e.g. myCounter) | Converted to kebab-case: <my-counter> |


renderToString(ComponentClass, props?, slotContent?)

Serializes an Aeico component class to a complete HTML string including the host element tag, reflected attributes, and inner markup — without instantiating the element or touching the DOM.

import { renderToString } from 'aeico-ssr';
import { MyCounter } from './my-counter';

renderToString(MyCounter, { count: 5 });
// '<my-counter count="5"><template shadowrootmode="open"><div>5</div></template></my-counter>'

Shadow DOM (default)

When static useShadowDOM = true (the default for AeicoElement), the inner markup is wrapped in a Declarative Shadow Root template:

<my-counter count="5">
  <template shadowrootmode="open">
    <div>5</div>
  </template>
</my-counter>

The browser attaches the shadow root declaratively before the custom element upgrades. AeicoElement and AeicoBase guard attachShadow so they do not overwrite a DSR shadow root that the browser has already created, meaning hydration requires zero extra configuration — the Reconciler reuses the existing DSR nodes on first render.

If static styles is declared on the component, a <style> tag is injected inside the template immediately before the inner markup.

Light DOM

When static useShadowDOM = false, the inner markup is emitted directly inside the host element tag:

renderToString(MyLight, { label: 'hello' });
// '<my-light label="hello"><p>hello</p></my-light>'

Prop coercion

Props passed to renderToString are coerced to the types declared in static props:

static props = {
  count: { type: Number, reflect: true },
};

Passing { count: '5' } (string) coerces to 5 (number) before the component's render() method is called.

Accepted types: Number, Boolean, String, Array, Object.

Computed properties

If the component declares static computed, those properties are wired up as lazy getters on the render context so render() can access them normally.

Tag name resolution

renderToString derives the tag name from (in priority order):

  1. static tagName — explicitly set on the class
  2. ComponentClass.name converted to kebab-case (e.g. MyCountermy-counter)

A tag name without a hyphen (invalid custom element name) causes renderToString to throw.

Slot content

Pass an html() RenderResult as the optional third argument to inject light DOM children into the host element. These children are distributed into the component's <slot> elements by the browser at parse time.

import { html } from 'aeico-view';
import { renderToString } from 'aeico-ssr';
import { AeNavbar } from './ae-navbar';

renderToString(AeNavbar, { siteTitle: 'Docs' }, html(({ a }) => {
  a({ slot: 'brand', href: '/', text: 'Docs' });
  a({ slot: 'start', href: '/guide', text: 'Guide' });
}));
// '<ae-navbar site-title="Docs">
//   <template shadowrootmode="open">...</template>
//   <a slot="brand" href="/">Docs</a>
//   <a slot="start" href="/guide">Guide</a>
// </ae-navbar>'

Because the third argument is a plain RenderResult, it can be defined once and reused across many renderToString calls:

const navSlot = html(({ a }) => {
  a({ slot: 'brand', href: '/', text: siteTitle });
  for (const item of navItems) {
    a({ slot: 'start', href: item.href, text: item.label });
  }
});

for (const page of pages) {
  renderToString(PageLayout, page, navSlot);
}

Named slots use the slot="name" attribute on the top-level element, matching the browser's native slot-assignment convention. Content without a slot attribute fills the unnamed default <slot>.


HtmlSerializer

Low-level building block used internally by renderHtml and renderToString.

HtmlSerializer mirrors the Reconciler API via a Proxy: arbitrary camelCase property accesses are intercepted and dispatched as tag calls, exactly as on the client.

import { HtmlSerializer } from 'aeico-ssr';

const s = new HtmlSerializer();
(s as any).div({ className: 'box' }, () => {
  (s as any).span({ text: 'hello' });
});
s.toString(); // '<div class="box"><span>hello</span></div>'

Because HtmlSerializer is typed separately from Reconciler (to avoid DOM dependencies), use as any or as unknown as Reconciler when passing it where a Reconciler is expected.

Methods

| Method | Description | |---|---| | toString() | Returns the accumulated HTML string | | reset() | Clears the buffer so the instance can be reused | | text(content) | Appends an HTML-escaped text node | | fragment(cb) | Runs cb inline without wrapping in a tag | | detached(fn) | Executes fn directly (no-op context switch — SSR has no stateful render context) | | node(_existingNode) | No-op — client-only operation; node will be rendered on upgrade | | el(tagName, props?, cb?) | Explicit dynamic-tag API (same as client-side Reconciler.el) |


SSR constraints

renderToString constructs a plain render context via Object.create(ComponentClass.prototype) and assigns props directly — it never calls new ComponentClass(). As a result, lifecycle methods do not run during SSR. The table below shows what this means in practice:

| Lifecycle | Runs in SSR? | Notes | |---|---|---| | render() | Yes | Always called — this is what generates the HTML | | static computed | Yes | Installed as lazy getters on the render context | | onPrepare() | No | See below | | onMounted() | No | Correct — browser-only (DOM queries, event listeners, animations) | | onUpdated() | No | Correct — reactive update cycle is browser-only | | connectedCallback() | No | Correct — browser-only |

onPrepare and state derived from props

The most common pitfall is using onPrepare to compute values that render() then reads:

// SSR-unfriendly: render() depends on state set in onPrepare
class ProductCard extends AeicoBase {
  static props = { rawPrice: { type: Number } };
  rawPrice?: number;
  formattedPrice?: string; // not a prop — invisible to renderToString

  protected onPrepare() {
    this.formattedPrice = `¥${((this.rawPrice ?? 0) / 100).toFixed(2)}`;
  }

  protected render() {
    return html(({ span }) => span({ text: this.formattedPrice })); // undefined in SSR
  }
}

The fix is to make render() a pure function of props. Two idiomatic approaches:

Option A — derive inline in render():

protected render() {
  const formatted = `¥${((this.rawPrice ?? 0) / 100).toFixed(2)}`;
  return html(({ span }) => span({ text: formatted }));
}

Option B — declare as static computed:

static computed = {
  formattedPrice: {
    deps: ['rawPrice'],
    compute: (ctx: ProductCard) => `¥${((ctx.rawPrice ?? 0) / 100).toFixed(2)}`,
  },
};
declare formattedPrice: string;

protected render() {
  return html(({ span }) => span({ text: this.formattedPrice })); // works in both SSR and browser
}

Option B (decorator syntax) — use @computed:

import { computed } from 'aeico-element/decorators';

@computed('rawPrice')
get formattedPrice() {
  return `¥${((this.rawPrice ?? 0) / 100).toFixed(2)}`;
}

protected render() {
  return html(({ span }) => span({ text: this.formattedPrice })); // works in both SSR and browser
}

Both forms of static computed / @computed are wired up as lazy getters in createRenderContext, so they evaluate correctly in SSR.

Async data

Lifecycle-driven data fetching (fetch inside onMounted) cannot run during SSR. The recommended pattern is to fetch data at the route/framework level and pass it as props:

// server route
const user = await fetchUser(req.params.id);
const html = renderToString(UserCard, { name: user.name, avatar: user.avatar });

This is the same constraint as React Server Components, Nuxt asyncData, and SvelteKit load — SSR-friendly components receive all initial data through their public interface rather than fetching it internally.

Summary rule

If the initial visual output of a component depends solely on its props (and computed properties derived from props), it is SSR-compatible with zero changes. Lifecycle methods that perform DOM operations, set up event listeners, or trigger side effects are browser-only by design and do not need to be SSR-aware.


Hydration

No special hydration step is required.

On the client, AeicoElement / AeicoBase guard attachShadow so they do not overwrite a DSR shadow root that the browser has already created. The first render() call runs a normal reconciliation pass against the existing nodes, adopting them in-place with minimal DOM mutations.


Security

All text content and attribute values are HTML-escaped before being emitted:

  • &&amp;
  • <&lt; (text only)
  • >&gt; (text only)
  • "&quot; (attributes only)

Event handlers (@… props) are stripped entirely and never appear in the output.


Comparison with renderHtml

| | renderHtml | renderToString | |---|---|---| | Input | RenderResult (from html()) | Component class + props | | Output | HTML fragment | Complete host element HTML | | Host tag | not included | Yes included | | Reflected attrs | No | Yes | | Shadow / Light DOM | No | Yes | | static styles | No | Yes | | Slot content | No | Yes (3rd arg) | | Requires aeico-element | No | Yes (peer dep) |


Building

npm run build        # rollup build + tsc declaration emit
npm run dev          # rollup --watch
npm run type-check   # tsc --noEmit
npm test             # build then node --test