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

Issue:
Empty class attribute duplicated with preact/compat and SSR
**Describe the bug**
When an element is created in preact/compat with className="", rendering it to a string causes two class attributes to be output.

This can happen when using conditional classes returned from the npm `classnames` function, for example.

**To Reproduce**
```
const { createElement } = require("preact/compat");
const render = require("preact-render-to-string");
console.log(render(createElement("a", { className: "" })));
```
Tested with preact v10.6.4 and preact-render-to-string v5.1.19

**Observed behavior**
`<a class class></a>`

**Expected behavior**
`<a class></a>` or `<a class=""></a>`


Pull request:
fix: On empty className w/ compat, class attr will no longer be duplicated
Fixes #208

Empty `props.class` wouldn't be truthy, resulting in `class` being added to the element twice as outlined in the attached issue.

The added test might be a bit overkill, but it seems Mocha has no support for test isolation. Importing `preact/compat` (even in other suites) would throw off one of the existing tests, so I just run the compat suite subsequently. Let me know if there's a better way to go about this.

Patch:
diff --git a/.changeset/fifty-rings-jog.md b/.changeset/fifty-rings-jog.md
new file mode 100644
index 0000000..ad3f569
--- /dev/null
+++ b/.changeset/fifty-rings-jog.md
@@ -0,0 +1,5 @@
+---
+'preact-render-to-string': patch
+---
+
+On empty className w/ compat, class attribute will no longer be duplicated
diff --git a/package.json b/package.json
index a0ee9cb..9b1df53 100644
--- a/package.json
+++ b/package.json
@@ -30,7 +30,8 @@
 		"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 bench",
-		"test:mocha": "BABEL_ENV=test mocha -r @babel/register -r test/setup.js test/**/*.test.js",
+		"test:mocha": "BABEL_ENV=test mocha -r @babel/register -r test/setup.js test/**/[!compat]*.test.js && npm run test:mocha:compat",
+		"test:mocha:compat": "BABEL_ENV=test mocha -r @babel/register -r test/setup.js test/compat.test.js",
 		"format": "prettier src/**/*.{d.ts,js} test/**/*.js --write",
 		"prepublishOnly": "npm run build",
 		"release": "npm run build && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish"
diff --git a/src/index.js b/src/index.js
index fa973a0..7714f36 100644
--- a/src/index.js
+++ b/src/index.js
@@ -15,7 +15,8 @@ const SHALLOW = { shallow: true };
 // components without names, kept as a hash for later comparison to return consistent UnnamedComponentXX names.
 const UNNAMED = [];
 
-const VOID_ELEMENTS = /^(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/;
+const VOID_ELEMENTS =
+	/^(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/;
 
 const UNSAFE_NAME = /[\s\n\\/='"\0<>]/;
 
@@ -247,7 +248,7 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) {
 			if (name === 'defaultValue') {
 				name = 'value';
 			} else if (name === 'className') {
-				if (props.class) continue;
+				if (typeof props.class !== 'undefined') continue;
 				name = 'class';
 			} else if (isSvgMode && name.match(/^xlink:?./)) {
 				name = name.toLowerCase().replace(/^xlink:?/, 'xlink:');


Test patch:
diff --git a/test/compat.test.js b/test/compat.test.js
new file mode 100644
index 0000000..e6fc6b9
--- /dev/null
+++ b/test/compat.test.js
@@ -0,0 +1,12 @@
+import { render } from '../src';
+import { createElement } from 'preact/compat';
+import { expect } from 'chai';
+
+describe('compat', () => {
+	it('should not duplicate class attribute when className is empty', async () => {
+		let rendered = render(createElement('div', { className: '' }));
+		let expected = `<div class></div>`;
+
+		expect(rendered).to.equal(expected);
+	});
+});