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

@maxenve/ren-html

v0.1.14

Published

Static rendering for ren-core: a view rendered once to HTML, after its tasks settle, with ren-dom's exact markup. No hydration.

Readme

ren-html

A view rendered once to HTML, for servers and static pages:

import { $html } from 'ren-html';

const html = await $html(() => <App />, { id: 'home', props: { user }, theme: { accent: 0xff325842 } });
response.end(`<!doctype html><html><head><meta charset="utf-8"></head><body>${html}</body></html>`);

The package is published on npm as @maxenve/ren-html under the MIT license; install it under its import name: npm install ren-html@npm:@maxenve/ren-html.

The page must declare UTF-8 (a <meta charset="utf-8"> or a Content-Type: text/html; charset=utf-8 header): a browser guessing the encoding may read the markup otherwise. ESC is written as &#x1B; (as \u001B in a script, \1b in a style), so no ISO-2022-JP switch reaches it.

There is no hydration. The page is markup and styles; a client that needs the live application renders it fresh with ren-dom.

$html(view, options) starts an application root with ren-dom's $dom on a small document of its own (src/document.js), waits until every task the view started has settled, a nested <Root>'s included (ren-core's $settle: Pending and Recover show their final content; a nested root's failure rejects as the view's own), returns the root element's HTML and disposes the root. The markup is exactly the one ren-dom makes in a browser, the primitives' scoped <style> and the theme's custom properties included (tested against ren-dom on happy-dom). One difference is on purpose: form values and flags a browser keeps as properties (value, checked, selected, a media element's muted, so a static <video autoplay muted> still autoplays) are written as attributes, so the static page shows them; a <select> marks selected the options a browser shows selected (its value's match, an option that came in selected, as a browser's selectedness follows ren-dom's steps; none for the first option a browser picks by itself, and a drop-down select whose value no option has shows its first, where a list box, size 2 and more, shows none: the split with ren-dom is on purpose, since a live drop-down select whose value changes to one no option has shows no option at all (its selectedIndex -1, blank), which markup cannot say: no option marked selected, the static page shows its first (test/select.test.jsx lists the case); its options are the ones Chrome's select counts, in a div too but not in an optgroup's optgroup; a select whose multiple goes keeps its first selected option), a checked radio unchecks the rest of its group (its name and form owner, as a browser's: the last checked, or the last to join the group, is the one written; a radio whose form attribute names a form after it, or one coming in later, joins that form's group when the form comes in, after the form's own radios), an open <details> with a name closes the others of that name in its tree, as a browser's (one coming in open closes itself where another is open), and a <textarea>'s value, once set, is its text in place of its children, as the field a browser shows keeps the value typed over text appended later. A nonce is written too, on purpose: a browser keeps it in an internal slot and writes no attribute, but the page's Content Security Policy reads it from the markup. The other difference is the page's missing base URL: an a's or area's URL parts (hash, host, protocol and the rest) edit an absolute href as a browser does, but on a relative one they write nothing, where a browser resolves it against the page's URL and writes the absolute URL.

| Option | Meaning | | --- | --- | | id | the root's id (page): its data-ren-root, storage keys and element ids | | props, provide | as Root's: what the view reads with $lookup(Props), contexts passed in | | theme, styles, behavior | as ren-dom's (behavior: 'app', the default, or 'page', or { like, …switches }; what the host element and the sheet write) | | tag | the host element's tag (div) | | timeout | milliseconds to wait for the tasks (10,000; 0 waits forever); past it $html rejects |

The markup's structure is safe to send: no text or value makes a tag or an attribute of its own, and it parses back to the elements the view made, but where the view breaks HTML's content model, which the parser repairs by moving elements (a p holding a div, an a inside an a, a div inside a table or directly inside an svg, and a view's own <body> or <html>, whose attributes, a class or a lang, move onto the page's body or html element), or places an SVG tag outside an <svg> (ren-dom makes it an SVG element anywhere; the parser reads it as HTML, so an image becomes a void img, its children moved out after it and its src loaded): ren-dom's DOM holds such a tree, markup cannot. ren-html writes ren-dom's markup for it rather than refuse or rewrite it, since either would part from ren-dom; put SVG content inside an <svg>. An attribute name that would break out of its tag, or a tag name that does not start with an ASCII letter, throws an InvalidCharacterError, as a browser's setAttribute and createElement do. An event handler prop given a string (any name starting on, in any ASCII case, on an HTML, SVG or MathML element: onclick, ONCLICK, an svg's onload) writes nothing, as ren-dom writes none in a browser: a handler is a function, and a function is no markup. An SVG attribute the parser would read back under another name (ONCLICK as onclick, STYLE as style, strokeWidth as strokewidth), or namespaced (xlink:href, xml:lang, xmlns:xlink), is not written: inert in the DOM, it would be live, or unchecked, once parsed. Attribute values are escaped and otherwise written as given, but for what would run script, which ren-dom never writes (its README, "What is never written"), so neither does $html: a javascript: URL (any ASCII case, leading controls and spaces, tabs and line breaks inside, as a browser's URL parser reads it) in any URL attribute (href, src, action, formaction, xlink:href, poster, data, cite, background and the rest), an href a URL part makes one, an SVG animate's or set's javascript: value, and an iframe's srcdoc, a document of markup: each removes its attribute, as null does, and the rest of the view renders. A character reference in a value is text, its & escaped, never a way around it. Document and serialize are a browser's DOM and serializer: code that builds markup with them directly answers for its own values. An inline style keeps only what a browser's CSSOM keeps: a declaration whose name is not a property's, or whose value is not one declaration's (a ;, brace or newline outside a string, anything left open), is dropped, so a value cannot add declarations of its own. Text is escaped, but for a style or script in HTML context, whose text children are joined and escaped once: its own end tag is written <\/style, <\/script, and a script's <!-- is written \u003C!-- (the same text to a script string and to JSON-LD), so no split can close it early or start the parser's double-escaped state. Under an <svg> or a <math>, where a parser reads a style's or a script's text as markup, it is escaped as any text, and so it is under their HTML integration points (the safe direction) and under any element whose content a parser does not read as tags (from the HTML standard: noscript, xmp, iframe, noembed, noframes, textarea, title, plaintext, select, frameset), where its text would be markup again once that element ended; a tree that holds a frameset has no raw text at all, as a page ignores a style's or a script's tag after one too. A textarea's, pre's or listing's leading line break is written twice, as the parser drops the first. No raw HTML is written from an element's content (srcdoc is an attribute's value, above): an innerHTML prop rejects $html with REN html: innerHTML is not rendered by $html (render the elements, or text; innerText sets text).

Document and serialize are exported for code that builds or reads the same markup. Shadow roots are not rendered: attachShadow throws (use $dom), and so does a template's shadowrootmode, a shadow root a page's parser would attach. A template is written as a browser serializes ren-dom's: its content, which only a parser fills, so it is empty.

Its dependencies are ren-core and ren-dom (install rrx-core, ren-core, ren-canv and ren-dom first, in that order; happy-dom and parse5 are dev dependencies, for the parity test and the fuzz). Node 22.15 or later. Tests: npm test runs test/html.test.jsx (parity with ren-dom in a browser document, form values and a select's value, style declarations, names and raw text, failures and time-outs, the theme and the behavior) in both JSX passes, test/select.test.jsx (which options a select shows, against Chrome: test/fixtures/select.json, recorded by scripts/select-oracle.mjs under the Chrome lock, ren-dom's live select on happy-dom and the parse of ren-html's markup; ren-json reads the same file; test/radio.test.jsx, which radios a page checks, the same way: test/fixtures/radio.json, recorded by scripts/radio-oracle.mjs, and a long list's radios grouped in work that grows with it; test/details.test.jsx, which details a page leaves open, the same way: test/fixtures/details.json, recorded by scripts/details-oracle.mjs (ren-json reads a copy); scripts/roles-oracle.mjs records, the same way, the roles Chrome's accessibility tree gives ren-dom's elements, ren-json's test/fixtures/roles.json), test/fuzz.test.js (random svg, math and HTML nesting, styles and scripts also inside noscript, textarea, title, xmp, iframe, noembed, noframes, plaintext, select and frameset, hostile tag names that must be refused, with hostile text split across text children, 5 fixed seeds × 8,000 views, parsed back by parse5 as a fragment and as a whole page, with and without <body>: every element parsed is one the view made, with its tag and namespace under the same parent, every element outside those opaque ones is kept, and its text too, and the page's own html, head and body take no attribute; each element's attributes are the view's under the names a parser reads back, and its style holds only the declarations the view set (read by a CSS tokenizer); a failure names its seed and smallest view), test/state-fuzz.test.js (random hover values, fixed seeds: the state sheet written reads, by a CSS tokenizer, as its own rules and nothing else), test/reflected.test.js (every reflected property of the HTML standard, from the table in test/fixtures/reflected.js: a property on its owners, converted by its IDL type and numeric rule), test/differential.test.jsx (ren-dom on happy-dom against $html, each prop on each tag, but for the differences it lists with their reasons), test/security.test.jsx (what would run script, in every URL attribute and the variants a browser's URL parser reads, a URL part, an animation, srcdoc, on* strings in any case on HTML, SVG and MathML elements: the same markup as ren-dom's, and parse5 reads back none of it), test/forms.test.js (the standard's 18 void elements, and the form value conversions against test/fixtures/forms.js, a table ren-json shares), test/exports.test.js (the export lock), test/timers.test.js (the timers guard) and test/types/html.tsx (the declarations, through tsc).