Repository: preactjs/preact-render-to-string (js)

Issue:
Void elements have whitespace at closing
Preact: `<img src="image.png" />`
React: `<img src="image.png"/>`

Not a critical issue but would be nice to match react's render to string output and it avoids having to do further minification.

Pull request:
Remove trailing space for void elements
Marked as major in case people rely on this for tests

Fixes #279 

Patch:
diff --git a/.changeset/beige-hounds-enjoy.md b/.changeset/beige-hounds-enjoy.md
new file mode 100644
index 0000000..15dbc9f
--- /dev/null
+++ b/.changeset/beige-hounds-enjoy.md
@@ -0,0 +1,5 @@
+---
+'preact-render-to-string': patch
+---
+
+Change style calculation to use a Set rather than Regex
diff --git a/.changeset/clever-ghosts-grin.md b/.changeset/clever-ghosts-grin.md
new file mode 100644
index 0000000..e468705
--- /dev/null
+++ b/.changeset/clever-ghosts-grin.md
@@ -0,0 +1,5 @@
+---
+'preact-render-to-string': patch
+---
+
+Fix CJS export
diff --git a/.changeset/fifty-hounds-press.md b/.changeset/fifty-hounds-press.md
new file mode 100644
index 0000000..ae5133b
--- /dev/null
+++ b/.changeset/fifty-hounds-press.md
@@ -0,0 +1,5 @@
+---
+"preact-render-to-string": patch
+---
+
+Enumerate draggable attribute, so the output isn't `draggable` but `draggable="true"`
diff --git a/.changeset/fluffy-poets-happen.md b/.changeset/fluffy-poets-happen.md
new file mode 100644
index 0000000..591f6ed
--- /dev/null
+++ b/.changeset/fluffy-poets-happen.md
@@ -0,0 +1,5 @@
+---
+'preact-render-to-string': patch
+---
+
+Follow up fixes to #278
diff --git a/.changeset/gentle-trains-attack.md b/.changeset/gentle-trains-attack.md
new file mode 100644
index 0000000..af5ded7
--- /dev/null
+++ b/.changeset/gentle-trains-attack.md
@@ -0,0 +1,7 @@
+---
+'preact-render-to-string': major
+---
+
+Remove trailing space for void_elements, this could fail some test_assertions as
+`<img />` will become `<img/>`, the other `VOID_ELEMENTS` this will be applied for
+can be found [here](https://github.com/preactjs/preact-render-to-string/blob/remove-trailing-space/src/index.js#L368-L385)
diff --git a/.changeset/quiet-toes-carry.md b/.changeset/quiet-toes-carry.md
new file mode 100644
index 0000000..445e54d
--- /dev/null
+++ b/.changeset/quiet-toes-carry.md
@@ -0,0 +1,5 @@
+---
+'preact-render-to-string': major
+---
+
+Remove the castin to VNode for `preact/debug`, this is fixed in Preact >= 10.13.0
diff --git a/.changeset/soft-badgers-clap.md b/.changeset/soft-badgers-clap.md
new file mode 100644
index 0000000..e6b13d5
--- /dev/null
+++ b/.changeset/soft-badgers-clap.md
@@ -0,0 +1,5 @@
+---
+"preact-render-to-string": patch
+---
+
+improve unmount option hook call performance
diff --git a/README.md b/README.md
index c784e82..e48954d 100644
--- a/README.md
+++ b/README.md
@@ -86,6 +86,62 @@ app.get('/:fox', (req, res) => {
 
 ---
 
+### `Suspense` & `lazy` components with [`preact/compat`](https://www.npmjs.com/package/preact) & [`preact-ssr-prepass`](https://www.npmjs.com/package/preact-ssr-prepass)
+
+```bash
+npm install preact preact-render-to-string preact-ssr-prepass
+```
+
+```jsx
+export default () => {
+  return (
+    <h1>Home page</h1>
+  )
+}
+```
+
+```jsx
+import { Suspense, lazy } from "preact/compat"
+
+// Creation of the lazy component
+const HomePage = lazy(() => import("./pages/home"))
+
+const Main = () => {
+  return (
+    <Suspense fallback={<p>Loading</p>}>
+      <HomePage />
+    </Suspense>
+  )
+}
+```
+
+```jsx
+import { render } from "preact-render-to-string"
+import prepass from "preact-ssr-prepass"
+import { Main } from "./main"
+
+const main = async () => {
+  // Creation of the virtual DOM
+  const vdom = <Main />
+  
+  // Pre-rendering of lazy components
+  await prepass(vdom)
+  
+  // Rendering of components 
+  const html = render(vdom)
+  
+  console.log(html)
+  // <h1>Home page</h1>
+}
+
+// Execution & error handling
+main().catch(error => {
+  console.error(error)
+})
+```
+
+---
+
 ### License
 
 [MIT](http://choosealicense.com/licenses/mit/)
diff --git a/benchmarks/index.js b/benchmarks/index.js
index 8066541..5820775 100644
--- a/benchmarks/index.js
+++ b/benchmarks/index.js
@@ -1,6 +1,6 @@
 import { h } from 'preact';
 import Suite from 'benchmarkjs-pretty';
-import renderToStringBaseline from './lib/render-to-string';
+import renderToStringBaseline from 'baseline-rts';
 // import renderToString from '../src/index';
 import renderToString from '../dist/index.module.js';
 import TextApp from './text';
diff --git a/benchmarks/lib/render-to-string.js b/benchmarks/lib/render-to-string.js
deleted file mode 100644
index de44f9c..0000000
--- a/benchmarks/lib/render-to-string.js
+++ /dev/null
@@ -1,454 +0,0 @@
-/* eslint-disable */
-// preact-render-to-string@5.2.6 (6a0bec2)
-import { Fragment as e, options as t, h as r } from 'preact';
-var n = /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|^--/i,
-	o = /^(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/,
-	i = /[\s\n\\/='"\0<>]/,
-	l = /^xlink:?./,
-	a = /["&<]/;
-function s(e) {
-	if (!1 === a.test((e += ''))) return e;
-	for (var t = 0, r = 0, n = '', o = ''; r < e.length; r++) {
-		switch (e.charCodeAt(r)) {
-			case 34:
-				o = '&quot;';
-				break;
-			case 38:
-				o = '&amp;';
-				break;
-			case 60:
-				o = '&lt;';
-				break;
-			default:
-				continue;
-		}
-		r !== t && (n += e.slice(t, r)), (n += o), (t = r + 1);
-	}
-	return r !== t && (n += e.slice(t, r)), n;
-}
-var f = function (e, t) {
-		return String(e).replace(/(\n+)/g, '$1' + (t || '\t'));
-	},
-	u = function (e, t, r) {
-		return (
-			String(e).length > (t || 40) ||
-			(!r && -1 !== String(e).indexOf('\n')) ||
-			-1 !== String(e).indexOf('<')
-		);
-	},
-	c = {},
-	_ = /([A-Z])/g;
-function p(e) {
-	var t = '';
-	for (var r in e) {
-		var o = e[r];
-		null != o &&
-			'' !== o &&
-			(t && (t += ' '),
-			(t +=
-				'-' == r[0] ? r : c[r] || (c[r] = r.replace(_, '-$1').toLowerCase())),
-			(t =
-				'number' == typeof o && !1 === n.test(r)
-					? t + ': ' + o + 'px;'
-					: t + ': ' + o + ';'));
-	}
-	return t || void 0;
-}
-function d(e, t) {
-	return (
-		Array.isArray(t) ? t.reduce(d, e) : null != t && !1 !== t && e.push(t), e
-	);
-}
-function v() {
-	this.__d = !0;
-}
-function h(e, t) {
-	return {
-		__v: e,
-		context: t,
-		props: e.props,
-		setState: v,
-		forceUpdate: v,
-		__d: !0,
-		__h: []
-	};
-}
-function g(e, t) {
-	var r = e.contextType,
-		n = r && t[r.__c];
-	return null != r ? (n ? n.props.value : r.__) : t;
-}
-var y = [];
-function m(r, n, a, c, _, v) {
-	if (null == r || 'boolean' == typeof r) return '';
-	if ('object' != typeof r) return 'function' == typeof r ? '' : s(r);
-	var b = a.pretty,
-		x = b && 'string' == typeof b ? b : '\t';
-	if (Array.isArray(r)) {
-		for (var k = '', S = 0; S < r.length; S++)
-			b && S > 0 && (k += '\n'), (k += m(r[S], n, a, c, _, v));
-		return k;
-	}
-	if (void 0 !== r.constructor) return '';
-	var w,
-		C = r.type,
-		O = r.props,
-		j = !1;
-	if ('function' == typeof C) {
-		if (((j = !0), !a.shallow || (!c && !1 !== a.renderRootComponent))) {
-			if (C === e) {
-				var A = [];
-				return (
-					d(A, r.props.children), m(A, n, a, !1 !== a.shallowHighOrder, _, v)
-				);
-			}
-			var F,
-				H = (r.__c = h(r, n));
-			t.__b && t.__b(r);
-			var M = t.__r;
-			if (C.prototype && 'function' == typeof C.prototype.render) {
-				var L = g(C, n);
-				((H = r.__c = new C(O, L)).__v = r),
-					(H._dirty = H.__d = !0),
-					(H.props = O),
-					null == H.state && (H.state = {}),
-					null == H._nextState &&
-						null == H.__s &&
-						(H._nextState = H.__s = H.state),
-					(H.context = L),
-					C.getDerivedStateFromProps
-						? (H.state = Object.assign(
-								{},
-								H.state,
-								C.getDerivedStateFromProps(H.props, H.state)
-						  ))
-						: H.componentWillMount &&
-						  (H.componentWillMount(),
-						  (H.state =
-								H._nextState !== H.state
-									? H._nextState
-									: H.__s !== H.state
-									? H.__s
-									: H.state)),
-					M && M(r),
-					(F = H.render(H.props, H.state, H.context));
-			} else
-				for (var T = g(C, n), E = 0; H.__d && E++ < 25; )
-					(H.__d = !1), M && M(r), (F = C.call(r.__c, O, T));
-			return (
-				H.getChildContext && (n = Object.assign({}, n, H.getChildContext())),
-				t.diffed && t.diffed(r),
-				m(F, n, a, !1 !== a.shallowHighOrder, _, v)
-			);
-		}
-		C =
-			(w = C).displayName ||
-			(w !== Function && w.name) ||
-			(function (e) {
-				var t = (Function.prototype.toString
-					.call(e)
-					.match(/^\s*function\s+([^( ]+)/) || '')[1];
-				if (!t) {
-					for (var r = -1, n = y.length; n--; )
-						if (y[n] === e) {
-							r = n;
-							break;
-						}
-					r < 0 && (r = y.push(e) - 1), (t = 'UnnamedComponent' + r);
-				}
-				return t;
-			})(w);
-	}
-	var $,
-		D,
-		N = '<' + C;
-	if (O) {
-		var P = Object.keys(O);
-		a && !0 === a.sortAttributes && P.sort();
-		for (var W = 0; W < P.length; W++) {
-			var I = P[W],
-				R = O[I];
-			if ('children' !== I) {
-				if (
-					!i.test(I) &&
-					((a && a.allAttributes) ||
-						('key' !== I && 'ref' !== I && '__self' !== I && '__source' !== I))
-				) {
-					if ('defaultValue' === I) I = 'value';
-					else if ('defaultChecked' === I) I = 'checked';
-					else if ('defaultSelected' === I) I = 'selected';
-					else if ('className' === I) {
-						if (void 0 !== O.class) continue;
-						I = 'class';
-					} else
-						_ &&
-							l.test(I) &&
-							(I = I.toLowerCase().replace(/^xlink:?/, 'xlink:'));
-					if ('htmlFor' === I) {
-						if (O.for) continue;
-						I = 'for';
-					}
-					'style' === I && R && 'object' == typeof R && (R = p(R)),
-						'a' === I[0] &&
-							'r' === I[1] &&
-							'boolean' == typeof R &&
-							(R = String(R));
-					var U = a.attributeHook && a.attributeHook(I, R, n, a, j);
-					if (U || '' === U) N += U;
-					else if ('dangerouslySetInnerHTML' === I) D = R && R.__html;
-					else if ('textarea' === C && 'value' === I) $ = R;
-					else if ((R || 0 === R || '' === R) && 'function' != typeof R) {
-						if (!((!0 !== R && '' !== R) || ((R = I), a && a.xml))) {
-							N = N + ' ' + I;
-							continue;
-						}
-						if ('value' === I) {
-							if ('select' === C) {
-								v = R;
-								continue;
-							}
-							'option' === C &&
-								v == R &&
-								void 0 === O.selected &&
-								(N += ' selected');
-						}
-						N = N + ' ' + I + '="' + s(R) + '"';
-					}
-				}
-			} else $ = R;
-		}
-	}
-	if (b) {
-		var V = N.replace(/\n\s*/, ' ');
-		V === N || ~V.indexOf('\n')
-			? b && ~N.indexOf('\n') && (N += '\n')
-			: (N = V);
-	}
-	if (((N += '>'), i.test(C)))
-		throw new Error(C + ' is not a valid HTML tag name in ' + N);
-	var q,
-		z = o.test(C) || (a.voidElements && a.voidElements.test(C)),
-		Z = [];
-	if (D) b && u(D) && (D = '\n' + x + f(D, x)), (N += D);
-	else if (null != $ && d((q = []), $).length) {
-		for (var B = b && ~N.indexOf('\n'), G = !1, J = 0; J < q.length; J++) {
-			var K = q[J];
-			if (null != K && !1 !== K) {
-				var Q = m(K, n, a, !0, 'svg' === C || ('foreignObject' !== C && _), v);
-				if ((b && !B && u(Q) && (B = !0), Q))
-					if (b) {
-						var X = Q.length > 0 && '<' != Q[0];
-						G && X ? (Z[Z.length - 1] += Q) : Z.push(Q), (G = X);
-					} else Z.push(Q);
-			}
-		}
-		if (b && B) for (var Y = Z.length; Y--; ) Z[Y] = '\n' + x + f(Z[Y], x);
-	}
-	if (Z.length || D) N += Z.join('');
-	else if (a && a.xml) return N.substring(0, N.length - 1) + ' />';
-	return (
-		!z || q || D
-			? (b && ~N.indexOf('\n') && (N += '\n'), (N = N + '</' + C + '>'))
-			: (N = N.replace(/>$/, ' />')),
-		N
-	);
-}
-var b = { shallow: !0 };
-S.render = S;
-var x = function (e, t) {
-		return S(e, t, b);
-	},
-	k = [];
-function S(n, o, i) {
-	o = o || {};
-	var l = t.__s;
-	t.__s = !0;
-	var a,
-		s = r(e, null);
-	return (
-		(s.__k = [n]),
-		(a =
-			i &&
-			(i.pretty ||
-				i.voidElements ||
-				i.sortAttributes ||
-				i.shallow ||
-				i.allAttributes ||
-				i.xml ||
-				i.attributeHook)
-				? m(n, o, i)
-				: F(n, o, !1, void 0, s)),
-		t.__c && t.__c(n, k),
-		(t.__s = l),
-		(k.length = 0),
-		a
-	);
-}
-function w(e) {
-	return null == e || 'boolean' == typeof e
-		? null
-		: 'string' == typeof e || 'number' == typeof e || 'bigint' == typeof e
-		? r(null, null, e)
-		: e;
-}
-function C(e, t) {
-	return 'className' === e
-		? 'class'
-		: 'htmlFor' === e
-		? 'for'
-		: 'defaultValue' === e
-		? 'value'
-		: 'defaultChecked' === e
-		? 'checked'
-		: 'defaultSelected' === e
-		? 'selected'
-		: t && l.test(e)
-		? e.toLowerCase().replace(/^xlink:?/, 'xlink:')
-		: e;
-}
-function O(e, t) {
-	return 'style' === e && null != t && 'object' == typeof t
-		? p(t)
-		: 'a' === e[0] && 'r' === e[1] && 'boolean' == typeof t
-		? String(t)
-		: t;
-}
-var j = Array.isArray,
-	A = Object.assign;
-function F(r, n, l, a, f) {
-	if (null == r || !0 === r || !1 === r || '' === r) return '';
-	if ('object' != typeof r) return 'function' == typeof r ? '' : s(r);
-	if (j(r)) {
-		var u = '';
-		f.__k = r;
-		for (var c = 0; c < r.length; c++)
-			(u += F(r[c], n, l, a, f)), (r[c] = w(r[c]));
-		return u;
-	}
-	if (void 0 !== r.constructor) return '';
-	(r.__ = f), t.__b && t.__b(r);
-	var _ = r.type,
-		p = r.props;
-	if ('function' == typeof _) {
-		var d;
-		if (_ === e) d = p.children;
-		else {
-			d =
-				_.prototype && 'function' == typeof _.prototype.render
-					? (function (e, r) {
-							var n = e.type,
-								o = g(n, r),
-								i = new n(e.props, o);
-							(e.__c = i),
-								(i.__v = e),
-								(i.__d = !0),
-								(i.props = e.props),
-								null == i.state && (i.state = {}),
-								null == i.__s && (i.__s = i.state),
-								(i.context = o),
-								n.getDerivedStateFromProps
-									? (i.state = A(
-											{},
-											i.state,
-											n.getDerivedStateFromProps(i.props, i.state)
-									  ))
-									: i.componentWillMount &&
-									  (i.componentWillMount(),
-									  (i.state = i.__s !== i.state ? i.__s : i.state));
-							var l = t.__r;
-							return l && l(e), i.render(i.props, i.state, i.context);
-					  })(r, n)
-					: (function (e, r) {
-							var n,
-								o = h(e, r),
-								i = g(e.type, r);
-							e.__c = o;
-							for (var l = t.__r, a = 0; o.__d && a++ < 25; )
-								(o.__d = !1), l && l(e), (n = e.type.call(o, e.props, i));
-							return n;
-					  })(r, n);
-			var v = r.__c;
-			v.getChildContext && (n = A({}, n, v.getChildContext()));
-		}
-		var y = F(
-			(d = null != d && d.type === e && null == d.key ? d.props.children : d),
-			n,
-			l,
-			a,
-			r
-		);
-		return (
-			t.diffed && t.diffed(r), (r.__ = void 0), t.unmount && t.unmount(r), y
-		);
-	}
-	var m,
-		b,
-		x = '<';
-	if (((x += _), p))
-		for (var k in ((m = p.children), p)) {
-			var S = p[k];
-			if (
-				!(
-					'key' === k ||
-					'ref' === k ||
-					'__self' === k ||
-					'__source' === k ||
-					'children' === k ||
-					('className' === k && 'class' in p) ||
-					('htmlFor' === k && 'for' in p) ||
-					i.test(k)
-				)
-			)
-				if (((S = O((k = C(k, l)), S)), 'dangerouslySetInnerHTML' === k))
-					b = S && S.__html;
-				else if ('textarea' === _ && 'value' === k) m = S;
-				else if ((S || 0 === S || '' === S) && 'function' != typeof S) {
-					if (!0 === S || '' === S) {
-						(S = k), (x = x + ' ' + k);
-						continue;
-					}
-					if ('value' === k) {
-						if ('select' === _) {
-							a = S;
-							continue;
-						}
-						'option' !== _ || a != S || 'selected' in p || (x += ' selected');
-					}
-					x = x + ' ' + k + '="' + s(S) + '"';
-				}
-		}
-	var H = x;
-	if (((x += '>'), i.test(_)))
-		throw new Error(_ + ' is not a valid HTML tag name in ' + x);
-	var M = '',
-		L = !1;
-	if (b) (M += b), (L = !0);
-	else if ('string' == typeof m) (M += s(m)), (L = !0);
-	else if (j(m)) {
-		r.__k = m;
-		for (var T = 0; T < m.length; T++) {
-			var E = m[T];
-			if (((m[T] = w(E)), null != E && !1 !== E)) {
-				var $ = F(E, n, 'svg' === _ || ('foreignObject' !== _ && l), a, r);
-				$ && ((M += $), (L = !0));
-			}
-		}
-	} else if (null != m && !1 !== m && !0 !== m) {
-		r.__k = [w(m)];
-		var D = F(m, n, 'svg' === _ || ('foreignObject' !== _ && l), a, r);
-		D && ((M += D), (L = !0));
-	}
-	if ((t.diffed && t.diffed(r), (r.__ = void 0), t.unmount && t.unmount(r), L))
-		x += M;
-	else if (o.test(_)) return H + ' />';
-	return x + '</' + _ + '>';
-}
-S.shallowRender = x;
-export default S;
-export {
-	S as render,
-	S as renderToStaticMarkup,
-	S as renderToString,
-	x as shallowRender
-};
-//# sourceMappingURL=index.module.js.map
diff --git a/package-lock.json b/package-lock.json
index 5bb75f1..5d0d99d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,6 +17,7 @@
 				"@babel/register": "^7.12.10",
 				"@changesets/changelog-github": "^0.4.1",
 				"@changesets/cli": "^2.18.0",
+				"baseline-rts": "npm:preact-render-to-string@latest",
 				"benchmarkjs-pretty": "^2.0.1",
 				"chai": "^4.2.0",
 				"copyfiles": "^2.4.1",
@@ -26,11 +27,11 @@
 				"lint-staged": "^10.5.3",
 				"microbundle": "^0.15.1",
 				"mocha": "^8.2.1",
-				"preact": "^10.11.1",
+				"preact": "^10.13.0",
 				"prettier": "^2.2.1",
 				"sinon": "^9.2.2",
 				"sinon-chai": "^3.5.0",
-				"typescript": "^4.1.3"
+				"typescript": "^5.0.0"
 			},
 			"peerDependencies": {
 				"preact": ">=10"
@@ -2564,6 +2565,19 @@
 			"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
 			"dev": true
 		},
+		"node_modules/baseline-rts": {
+			"name": "preact-render-to-string",
+			"version": "5.2.6",
+			"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz",
+			"integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==",
+			"dev": true,
+			"dependencies": {
+				"pretty-format": "^3.8.0"
+			},
+			"peerDependencies": {
+				"preact": ">=10"
+			}
+		},
 		"node_modules/benchmark": {
 			"version": "2.1.4",
 			"resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz",
@@ -7162,6 +7176,19 @@
 			"integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==",
 			"dev": true
 		},
+		"node_modules/microbundle/node_modules/typescript": {
+			"version": "4.9.5",
+			"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
+			"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
+			"dev": true,
+			"bin": {
+				"tsc": "bin/tsc",
+				"tsserver": "bin/tsserver"
+			},
+			"engines": {
+				"node": ">=4.2.0"
+			}
+		},
 		"node_modules/micromatch": {
 			"version": "4.0.4",
 			"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
@@ -10791,9 +10818,9 @@
 			}
 		},
 		"node_modules/preact": {
-			"version": "10.11.1",
-			"resolved": "https://registry.npmjs.org/preact/-/preact-10.11.1.tgz",
-			"integrity": "sha512-1Wz5PCRm6Fg+6BTXWJHhX4wRK9MZbZBHuwBqfZlOdVm2NqPe8/rjYpufvYCwJSGb9layyzB2jTTXfpCTynLqFQ==",
+			"version": "10.13.1",
+			"resolved": "https://registry.npmjs.org/preact/-/preact-10.13.1.tgz",
+			"integrity": "sha512-KyoXVDU5OqTpG9LXlB3+y639JAGzl8JSBXLn1J9HTSB3gbKcuInga7bZnXLlxmK94ntTs1EFeZp0lrja2AuBYQ==",
 			"dev": true,
 			"funding": {
 				"type": "opencollective",
@@ -13085,16 +13112,16 @@
 			}
 		},
 		"node_modules/typescript": {
-			"version": "4.1.3",
-			"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz",
-			"integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==",
+			"version": "5.0.2",
+			"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.2.tgz",
+			"integrity": "sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw==",
 			"dev": true,
 			"bin": {
 				"tsc": "bin/tsc",
 				"tsserver": "bin/tsserver"
 			},
 			"engines": {
-				"node": ">=4.2.0"
+				"node": ">=12.20"
 			}
 		},
 		"node_modules/unbox-primitive": {
@@ -15911,6 +15938,15 @@
 			"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
 			"dev": true
 		},
+		"baseline-rts": {
+			"version": "npm:preact-render-to-string@5.2.6",
+			"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz",
+			"integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==",
+			"dev": true,
+			"requires": {
+				"pretty-format": "^3.8.0"
+			}
+		},
 		"benchmark": {
 			"version": "2.1.4",
 			"resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz",
@@ -19582,6 +19618,12 @@
 					"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
 					"integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==",
 					"dev": true
+				},
+				"typescript": {
+					"version": "4.9.5",
+					"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
+					"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
+					"dev": true
 				}
 			}
 		},
@@ -22502,9 +22544,9 @@
 			"dev": true
 		},
 		"preact": {
-			"version": "10.11.1",
-			"resolved": "https://registry.npmjs.org/preact/-/preact-10.11.1.tgz",
-			"integrity": "sha512-1Wz5PCRm6Fg+6BTXWJHhX4wRK9MZbZBHuwBqfZlOdVm2NqPe8/rjYpufvYCwJSGb9layyzB2jTTXfpCTynLqFQ==",
+			"version": "10.13.1",
+			"resolved": "https://registry.npmjs.org/preact/-/preact-10.13.1.tgz",
+			"integrity": "sha512-KyoXVDU5OqTpG9LXlB3+y639JAGzl8JSBXLn1J9HTSB3gbKcuInga7bZnXLlxmK94ntTs1EFeZp0lrja2AuBYQ==",
 			"dev": true
 		},
 		"preferred-pm": {
@@ -24327,9 +24369,9 @@
 			"dev": true
 		},
 		"typescript": {
-			"version": "4.1.3",
-			"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz",
-			"integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==",
+			"version": "5.0.2",
+			"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.2.tgz",
+			"integrity": "sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw==",
 			"dev": true
 		},
 		"unbox-primitive": {
diff --git a/package.json b/package.json
index a439330..a73962e 100644
--- a/package.json
+++ b/package.json
@@ -4,21 +4,23 @@
 	"version": "5.2.6",
 	"description": "Render JSX to an HTML string, with support for Preact components.",
 	"main": "dist/index.js",
-	"umd:main": "dist/index.js",
+	"umd:main": "dist/index.umd.js",
 	"module": "dist/index.module.js",
 	"jsnext:main": "dist/index.module.js",
 	"types": "src/index.d.ts",
 	"exports": {
 		".": {
 			"types": "./src/index.d.ts",
-			"import": "./dist/index.mjs",
 			"browser": "./dist/index.module.js",
+			"umd": "./dist/index.umd.js",
+			"import": "./dist/index.mjs",
 			"require": "./dist/index.js"
 		},
 		"./jsx": {
 			"types": "./jsx.d.ts",
-			"import": "./dist/jsx.mjs",
 			"browser": "./dist/jsx.module.js",
+			"umd": "./dist/jsx.umd.js",
+			"import": "./dist/jsx.mjs",
 			"require": "./dist/jsx.js"
 		},
 		"./package.json": "./package.json"
@@ -28,7 +30,7 @@
 		"bench:v8": "BABEL_ENV=test microbundle benchmarks/index.js -f modern --alias benchmarkjs-pretty=benchmarks/lib/benchmark-lite.js --external none --target node --no-compress --no-sourcemap --raw -o benchmarks/.v8.mjs && v8 --module benchmarks/.v8.mjs",
 		"build": "npm run -s transpile && npm run -s transpile:jsx && npm run -s copy-typescript-definition",
 		"postbuild": "node ./config/node-13-exports.js && node ./config/node-commonjs.js",
-		"transpile": "microbundle src/index.js -f es,umd --target web --external preact",
+		"transpile": "microbundle src/index.js -f es,cjs,umd --target web --external preact",
 		"transpile:jsx": "microbundle src/jsx.js -o dist/jsx.js --target web --external preact && microbundle dist/jsx.js -o dist/jsx.js -f cjs --external preact",
 		"copy-typescript-definition": "copyfiles -f src/*.d.ts dist",
 		"test": "eslint src test && tsc && npm run test:mocha && npm run test:mocha:compat && npm run test:mocha:debug && npm run bench",
@@ -121,11 +123,12 @@
 		"lint-staged": "^10.5.3",
 		"microbundle": "^0.15.1",
 		"mocha": "^8.2.1",
-		"preact": "^10.11.1",
+		"baseline-rts": "npm:preact-render-to-string@latest",
+		"preact": "^10.13.0",
 		"prettier": "^2.2.1",
 		"sinon": "^9.2.2",
 		"sinon-chai": "^3.5.0",
-		"typescript": "^4.1.3"
+		"typescript": "^5.0.0"
 	},
 	"dependencies": {
 		"pretty-format": "^3.8.0"
diff --git a/src/index.js b/src/index.js
index db6b6df..7e8f5dc 100644
--- a/src/index.js
+++ b/src/index.js
@@ -21,7 +21,7 @@ const isArray = Array.isArray;
 const assign = Object.assign;
 
 // Global state for the current render pass
-let beforeDiff, afterDiff, renderHook;
+let beforeDiff, afterDiff, renderHook, ummountHook;
 
 /**
  * Render Preact JSX + Components to an HTML string.
@@ -42,6 +42,7 @@ export default function renderToString(vnode, context) {
 	beforeDiff = options[DIFF];
 	afterDiff = options[DIFFED];
 	renderHook = options[RENDER];
+	ummountHook = options.unmount;
 
 	const parent = h(Fragment, null);
 	parent[CHILDREN] = [vnode];
@@ -144,15 +145,6 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) {
 			rendered =
 				rendered +
 				_renderToString(child, context, isSvgMode, selectValue, parent);
-
-			if (
-				typeof child === 'string' ||
-				typeof child === 'number' ||
-				typeof child === 'bigint'
-			) {
-				// @ts-ignore manually constructing a Text vnode
-				vnode[i] = h(null, null, child);
-			}
 		}
 		return rendered;
 	}
@@ -236,7 +228,7 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) {
 		if (afterDiff) afterDiff(vnode);
 		vnode[PARENT] = undefined;
 
-		if (options.unmount) options.unmount(vnode);
+		if (ummountHook) ummountHook(vnode);
 
 		return str;
 	}
@@ -319,8 +311,13 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) {
 					name = name.toLowerCase().replace(XLINK_REPLACE_REGEX, 'xlink:');
 				} else if (UNSAFE_NAME.test(name)) {
 					continue;
-				} else if (name[0] === 'a' && name[1] === 'r' && v != null) {
-					// serialize boolean aria-xyz attribute values as strings
+				} else if (
+					((name[0] === 'a' && name[1] === 'r') || name === 'draggable') &&
+					v != null
+				) {
+					// serialize boolean aria-xyz or draggable attribute values as strings
+					// `draggable` is an enumerated attribute and not Boolean. A value of `true` or `false` is mandatory
+					// https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/draggable
 					v += '';
 				}
 			}
@@ -337,6 +334,8 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) {
 	}
 
 	if (UNSAFE_NAME.test(type)) {
+		// this seems to performs a lot better than throwing
+		// return '<!-- -->';
 		throw new Error(`${type} is not a valid HTML tag name in ${s}>`);
 	}
 
@@ -354,11 +353,11 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) {
 
 	if (afterDiff) afterDiff(vnode);
 	vnode[PARENT] = undefined;
-	if (options.unmount) options.unmount(vnode);
+	if (ummountHook) ummountHook(vnode);
 
 	// Emit self-closing tag for empty void elements:
 	if (!html && SELF_CLOSING.has(type)) {
-		return s + ' />';
+		return s + '/>';
 	}
 
 	return s + '>' + html + '</' + type + '>';
diff --git a/src/util.js b/src/util.js
index bb76198..c3a598e 100644
--- a/src/util.js
+++ b/src/util.js
@@ -3,7 +3,6 @@ export const UNSAFE_NAME = /[\s\n\\/='"\0<>]/;
 export const XLINK = /^xlink:?./;
 
 // DOM properties that should NOT have "px" added when numeric
-const IS_NON_DIMENSIONAL = /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|^--/;
 const ENCODED_ENTITIES = /["&<]/;
 
 /** @param {string} str */
@@ -50,9 +49,45 @@ export let isLargeString = (s, length, ignoreLines) =>
 	String(s).indexOf('<') !== -1;
 
 const JS_TO_CSS = {};
-const SUFFIX_CACHE = {};
 
-const CSS_REGEX = /([A-Z])/g;
+const IS_NON_DIMENSIONAL = new Set([
+	'animation-iteration-count',
+	'border-image-outset',
+	'border-image-slice',
+	'border-image-width',
+	'box-flex',
+	'box-flex-group',
+	'box-ordinal-group',
+	'column-count',
+	'fill-opacity',
+	'flex',
+	'flex-grow',
+	'flex-negative',
+	'flex-order',
+	'flex-positive',
+	'flex-shrink',
+	'flood-opacity',
+	'font-weight',
+	'grid-column',
+	'grid-row',
+	'line-clamp',
+	'line-height',
+	'opacity',
+	'order',
+	'orphans',
+	'stop-opacity',
+	'stroke-dasharray',
+	'stroke-dashoffset',
+	'stroke-miterlimit',
+	'stroke-opacity',
+	'stroke-width',
+	'tab-size',
+	'widows',
+	'z-index',
+	'zoom'
+]);
+
+const CSS_REGEX = /[A-Z]/g;
 // Convert an Object style to a CSSText string
 export function styleObjToCss(s) {
 	let str = '';
@@ -63,17 +98,16 @@ export function styleObjToCss(s) {
 				prop[0] == '-'
 					? prop
 					: JS_TO_CSS[prop] ||
-					  (JS_TO_CSS[prop] = prop.replace(CSS_REGEX, '-$1').toLowerCase());
+					  (JS_TO_CSS[prop] = prop.replace(CSS_REGEX, '-$&').toLowerCase());
 
 			let suffix = ';';
-			if (SUFFIX_CACHE[name]) {
-				suffix = 'px';
-			} else if (
+			if (
 				typeof val === 'number' &&
-				IS_NON_DIMENSIONAL.test(prop.toLowerCase()) === false
+				// Exclude custom-attributes
+				!name.startsWith('--') &&
+				!IS_NON_DIMENSIONAL.has(name)
 			) {
-				SUFFIX_CACHE[name] = true;
-				suffix = 'px';
+				suffix = 'px;';
 			}
 			str = str + name + ':' + val + suffix;
 		}


Test patch:
diff --git a/test/render.test.js b/test/render.test.js
index 17391c3..82e0a73 100644
--- a/test/render.test.js
+++ b/test/render.test.js
@@ -116,7 +116,7 @@ describe('render', () => {
 
 		it('should serialize defaultChecked prop to the checked attribute', () => {
 			let rendered = render(<input type="checkbox" defaultChecked />),
-				expected = `<input type="checkbox" checked />`;
+				expected = `<input type="checkbox" checked/>`;
 
 			expect(rendered).to.equal(expected);
 		});
@@ -135,6 +135,13 @@ describe('render', () => {
 			expect(rendered).to.equal(expected);
 		});
 
+		it('should include boolean draggable attribute', () => {
+			let rendered = render(<div draggable />),
+				expected = `<div draggable="true"></div>`;
+
+			expect(rendered).to.equal(expected);
+		});
+
 		describe('attribute name sanitization', () => {
 			it('should omit attributes with invalid names', () => {
 				let rendered = render(
@@ -240,7 +247,7 @@ describe('render', () => {
 						<wbr />
 					</div>
 				),
-				expected = `<div><input type="text" /><wbr /></div>`;
+				expected = `<div><input type="text"/><wbr/></div>`;
 
 			expect(rendered).to.equal(expected);
 		});
@@ -1313,7 +1320,7 @@ describe('render', () => {
 				);
 			}
 
-			expect(render(<App />)).to.equal('<div><p>P481</p><p>P476951</p></div>');
+			expect(render(<App />)).to.equal('<div><p>P0-0</p><p>P0-1</p></div>');
 		});
 	});
 });