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

Issue:
Object children are rendered as `<undefined>`
```js
import render from "https://esm.sh/preact-render-to-string@5.2.4?deps=preact@10.11.0";
import { h } from "https://esm.sh/preact@10.11.0";
console.log(render(h("div", null, { hello: "world" })));
```

```shell
$ deno run test.ts
<div><undefined></undefined></div>
```

Pull request description:
Ignore non-VNode objects during rendering
This fixes #245.

Patch:
diff --git a/.changeset/curly-bananas-do.md b/.changeset/curly-bananas-do.md
new file mode 100644
index 0000000..4df000d
--- /dev/null
+++ b/.changeset/curly-bananas-do.md
@@ -0,0 +1,5 @@
+---
+'preact-render-to-string': patch
+---
+
+Fix object and function children being rendered as `undefined`
diff --git a/src/index.js b/src/index.js
index 9892d21..4f5e283 100644
--- a/src/index.js
+++ b/src/index.js
@@ -195,6 +195,7 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) {
 
 	// Text VNodes: escape as HTML
 	if (typeof vnode !== 'object') {
+		if (typeof vnode === 'function') return '';
 		return encodeEntities(vnode);
 	}
 
@@ -210,6 +211,9 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) {
 		return rendered;
 	}
 
+	// VNodes have {constructor:undefined} to prevent JSON injection:
+	if (vnode.constructor !== undefined) return '';
+
 	vnode[PARENT] = parent;
 	if (options[DIFF]) options[DIFF](vnode);
 
diff --git a/src/pretty.js b/src/pretty.js
index 4d85cbc..1c97798 100644
--- a/src/pretty.js
+++ b/src/pretty.js
@@ -29,6 +29,7 @@ export function _renderToStringPretty(
 
 	// #text nodes
 	if (typeof vnode !== 'object') {
+		if (typeof vnode === 'function') return '';
 		return encodeEntities(vnode);
 	}
 
@@ -53,6 +54,9 @@ export function _renderToStringPretty(
 		return rendered;
 	}
 
+	// VNodes have {constructor:undefined} to prevent JSON injection:
+	if (vnode.constructor !== undefined) return '';
+
 	let nodeName = vnode.type,
 		props = vnode.props,
 		isComponent = false;


Test patch:
diff --git a/test/jsx.test.js b/test/jsx.test.js
index 733ebaf..403b888 100644
--- a/test/jsx.test.js
+++ b/test/jsx.test.js
@@ -163,4 +163,12 @@ describe('jsx', () => {
 			<meta charset="utf-8" />
 		`);
 	});
+
+	it('should prevent JSON injection', () => {
+		expect(renderJsx(<div>{{ hello: 'world' }}</div>)).to.equal('<div></div>');
+	});
+
+	it('should not render function children', () => {
+		expect(renderJsx(<div>{() => {}}</div>)).to.equal('<div></div>');
+	});
 });
diff --git a/test/pretty.test.js b/test/pretty.test.js
index 5c6b441..4ee901e 100644
--- a/test/pretty.test.js
+++ b/test/pretty.test.js
@@ -222,4 +222,14 @@ describe('pretty', () => {
 			</p>
 		`);
 	});
+
+	it('should prevent JSON injection', () => {
+		expect(prettyRender(<div>{{ hello: 'world' }}</div>)).to.equal(
+			'<div></div>'
+		);
+	});
+
+	it('should not render function children', () => {
+		expect(prettyRender(<div>{() => {}}</div>)).to.equal('<div></div>');
+	});
 });
diff --git a/test/render.test.js b/test/render.test.js
index 3783569..8f23482 100644
--- a/test/render.test.js
+++ b/test/render.test.js
@@ -1260,4 +1260,12 @@ describe('render', () => {
 			'<select><option selected value="2">2</option></select>'
 		);
 	});
+
+	it('should prevent JSON injection', () => {
+		expect(render(<div>{{ hello: 'world' }}</div>)).to.equal('<div></div>');
+	});
+
+	it('should not render function children', () => {
+		expect(render(<div>{() => {}}</div>)).to.equal('<div></div>');
+	});
 });