diff --git a/README.md b/README.md
index 61623117b47353b6d9aebe5dfd9ea3dd6ff2e0b2..18816d1e3abd08308c27d9a9f9711d8e245cfabf 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,22 @@ Offline HTML5 validator. Validates either a full document or a smaller
 - Strict and non-forgiving parsing. It will not try to correct any incorrect
   markup or guess what it should do.
 
+## Bundles
+
+The library comes in four flavours:
+
+- CommonJS full (`dist/cjs/main.js`)
+- CommonJS browser (`dist/cjs/browser.js`)
+- ESM full (`dist/esm/main.js`)
+- ESM browser (`dist/esm/browser.js`)
+
+The browser versions contains a slimmed version without CLI dependencies.
+Your tooling will probably use the correct version but if needed you can import the files directly.
+
+Do note that to run in a browser you still need to polyfill the `fs` nodejs library.
+
+Browsers and bundlers are currently not 100% supported but is possible with some tricks, see [running in browser](https://html-validate.org/dev/running-in-browser.html) for more details.
+
 ## Usage
 
     npm install -g html-validate
diff --git a/bin/html-validate.js b/bin/html-validate.js
index 8b8075382782844f4b980215fea59ad2aed3d069..ca2b9d0226b0cd9ac94f0c909f8cb3c3d0d1bccc 100755
--- a/bin/html-validate.js
+++ b/bin/html-validate.js
@@ -1,4 +1,4 @@
 #!/usr/bin/env node
 "use strict";
 
-require("../dist/cli/html-validate");
+require("../dist/cjs/html-validate");
diff --git a/docs/dev/running-in-browser.md b/docs/dev/running-in-browser.md
new file mode 100644
index 0000000000000000000000000000000000000000..e70edd620a75c24f750263b52574950aaea5a6b8
--- /dev/null
+++ b/docs/dev/running-in-browser.md
@@ -0,0 +1,105 @@
+---
+docType: content
+title: Running in a browser
+---
+
+# Running in a browser
+
+While primarly developed as a NodeJS CLI/backend tool it is possible to run `html-validate` in a browser as well.
+
+<div class="alert alert-info">
+	<i class="fa fa-info-circle" aria-hidden="true"></i>
+	<strong>Note</strong>
+	<p>While it is possible to get <code>html-validate</code> running in a browser it is currently not supported and requires a few workarounds.</p>
+</div>
+
+Improvements are welcome!
+
+## Example
+
+There is an example project [try-online-repo] running at [try-online-url] showing that it can be done and the workarounds required.
+
+[try-online-repo]: https://gitlab.com/html-validate/try-online
+[try-online-url]: https://online.html-validate.org/
+
+## Configuration loading
+
+By default `html-validate` will traverse the filesystem looking for configuration files (e.g. `.htmlvalidate.json`).
+This is true even when using `validateString(..)`.
+
+This will manifest itself with errors such as:
+
+- `Cannot find module 'fs'`
+- `Cannot read property 'existsSync' of undefined`
+- `fs_1.default.existsSync is not a function`
+
+### Workaround 1: prevent loader from trying to access filesystem
+
+By far the easiest method is to pass a config to the [[HtmlValidate]] constructor with the `root` property to `true`:
+
+```ts
+import { HtmlValidate } from "html-validate";
+
+const htmlvalidate = new HtmlValidate({
+  root: true,
+  extends: ["html-validate:recommended"],
+});
+```
+
+Do note that no default configuration will be loaded either so you must explicitly enable rules or extend a preset.
+
+### Workaround 2:
+
+If you are emulating or providing virtual access to a filesystem you can ensure the `fs` module is implemented.
+There is no exhaustive list of functions which must be added.
+
+If you are using webpack you can use `resolve.alias` to implement this:
+
+```js
+module.exports = {
+  resolve: {
+    alias: {
+      fs$: path.resolve(__dirname, "src/my-fs.js"),
+    },
+  },
+};
+```
+
+## Bundled files
+
+The `html-validate` NPM package contains a few data files such as `elements/html.json`.
+These files are dynamically imported and will most likely not be picked up by your bundler.
+Either you need to ensure the bundler picks up the files or the configuration loader does not need to import thme.
+
+- `elements/*.json`
+
+This will manifest itself with errors such as:
+
+- `Error: Failed to load elements from "html5": Cannot find module 'html5'`
+
+This is typically archived by passing an object instead of a string when configuring `html-validate`:
+
+```diff
+ import { HtmlValidate } from "html-validate";
+
++// check your loader! it must return a plain object (not `default: { ... }`, a path/url, etc)
++import html5 from "html-validate/elements/html5.json";
+
+ const htmlvalidate = new HtmlValidate({
+   root: true,
+   extends: ["html-validate:recommended"],
+-  elements: ["html5"],
++  elements: [html5]
+});
+```
+
+## Webpack
+
+Internally there are many dynamic imports and `fs` access.
+
+You will see warnings such as:
+
+    WARNING in ./node_modules/html-validate/dist/config/config.js 81:25-50
+    Critical dependency: the request of a dependency is an expression
+
+In many cases there is no way to avoid the warning per se but the workaround above are implemented the code paths triggering these issues are not hit.
diff --git a/docs/dgeni/inline-validate/processors/validate-results.js b/docs/dgeni/inline-validate/processors/validate-results.js
index 5a2250f0edcd3de1fafc27693eac4c3961351ef5..d601ac5881d99e8e763750dd1752f02bfbf4be8f 100644
--- a/docs/dgeni/inline-validate/processors/validate-results.js
+++ b/docs/dgeni/inline-validate/processors/validate-results.js
@@ -1,6 +1,7 @@
 const kleur = require("kleur");
-const HtmlValidate = require("../../../../dist/htmlvalidate").default;
-const codeframe = require("../../../../dist/formatters/codeframe").codeframe;
+const { HtmlValidate, formatterFactory } = require("../../../../dist/cjs");
+
+const codeframe = formatterFactory("codeframe");
 
 const formatterOptions = {
 	showLink: false,
diff --git a/docs/dgeni/processors/rules.js b/docs/dgeni/processors/rules.js
index c0ab6816da646bf4394cab0cd1340857927f7a49..ec1e69248e52532cbc3cd8e107cadf0e867012c3 100644
--- a/docs/dgeni/processors/rules.js
+++ b/docs/dgeni/processors/rules.js
@@ -1,19 +1,8 @@
-const { default: a17y } = require("../../../dist/config/presets/a17y");
-const { default: document } = require("../../../dist/config/presets/document");
-const { default: recommended } = require("../../../dist/config/presets/recommended");
-const { default: standard } = require("../../../dist/config/presets/standard");
+const { configPresets } = require("../../../dist/cjs");
 
 /* sort order */
 const availablePresets = ["recommended", "standard", "a17y", "document"];
 
-/* preset configuration */
-const presets = {
-	a17y,
-	document,
-	recommended,
-	standard,
-};
-
 function compareName(a, b) {
 	if (a.name < b.name) {
 		return -1;
@@ -59,7 +48,8 @@ module.exports = function rulesProcessor(renderDocsProcessor) {
 				category: doc.category,
 				summary: doc.summary,
 				presets: availablePresets.reduce((result, presetName) => {
-					const config = presets[presetName];
+					const key = `html-validate:${presetName}`;
+					const config = configPresets[key];
 					if (config && config.rules) {
 						result[presetName] = Boolean(config.rules[doc.name]);
 					}
diff --git a/docs/dgeni/templates/base.template.html b/docs/dgeni/templates/base.template.html
index d87609cf71ff30924394b205e7a1b9287e3af89c..be139139ffcba77c689438236ad6e6012b0cfd62 100644
--- a/docs/dgeni/templates/base.template.html
+++ b/docs/dgeni/templates/base.template.html
@@ -74,6 +74,7 @@
 									<li><a href="{{ pkg.bugs.url }}">File issue</a></li>
 									<li role="separator" class="divider"></li>
 									<li>{@link dev/using-api Using API}</li>
+									<li>{@link dev/running-in-browser Running in a browser}</li>
 									<li>{@link dev/writing-rules Writing rules}</li>
 									<li>{@link dev/writing-plugins Writing plugins}</li>
 									<li>{@link dev/transformers Writing transformers}</li>
diff --git a/jest.d.ts b/jest.d.ts
index a5501a872eb0e1a6d6759e420bb62da5dcf99b9b..c0166699aed886fb54dc4e3dbd7a7feb3564a460 100644
--- a/jest.d.ts
+++ b/jest.d.ts
@@ -1,2 +1,2 @@
 /* eslint-disable-next-line import/export, import/no-unresolved */
-export * from "./dist/matchers";
+export * from "./dist/cjs/matchers";
diff --git a/jest.js b/jest.js
index 678839a98237aa3f2bd1e02ae254e7b21b09ebb9..b7699d037c16a481369b76c30c834fa88eecba7e 100644
--- a/jest.js
+++ b/jest.js
@@ -1 +1 @@
-module.exports = require("./dist/matchers");
+module.exports = require("./dist/cjs/matchers");
diff --git a/package-lock.json b/package-lock.json
index 18b97e3cca44f425ebf1e6ae71717546dacab307..53755e80676b8daf5fd58839200f6124d21deeeb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -37,6 +37,10 @@
         "@html-validate/prettier-config": "1.1.0",
         "@html-validate/semantic-release-config": "2.0.0",
         "@lodder/grunt-postcss": "3.0.1",
+        "@rollup/plugin-json": "4.1.0",
+        "@rollup/plugin-replace": "2.4.2",
+        "@rollup/plugin-typescript": "8.2.1",
+        "@rollup/plugin-virtual": "2.0.3",
         "@types/babar": "0.2.0",
         "@types/babel__code-frame": "7.0.2",
         "@types/estree": "0.0.47",
@@ -77,6 +81,9 @@
         "npm-pkg-lint": "1.4.0",
         "postcss": "8.3.5",
         "prettier": "2.3.1",
+        "rollup": "2.51.2",
+        "rollup-plugin-copy": "3.4.0",
+        "rollup-plugin-dts": "3.0.2",
         "sass": "1.35.1",
         "semantic-release": "17.4.4",
         "serve-static": "1.14.1",
@@ -3055,6 +3062,81 @@
         "@octokit/openapi-types": "^5.2.0"
       }
     },
+    "node_modules/@rollup/plugin-json": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-4.1.0.tgz",
+      "integrity": "sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw==",
+      "dev": true,
+      "dependencies": {
+        "@rollup/pluginutils": "^3.0.8"
+      },
+      "peerDependencies": {
+        "rollup": "^1.20.0 || ^2.0.0"
+      }
+    },
+    "node_modules/@rollup/plugin-replace": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz",
+      "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==",
+      "dev": true,
+      "dependencies": {
+        "@rollup/pluginutils": "^3.1.0",
+        "magic-string": "^0.25.7"
+      },
+      "peerDependencies": {
+        "rollup": "^1.20.0 || ^2.0.0"
+      }
+    },
+    "node_modules/@rollup/plugin-typescript": {
+      "version": "8.2.1",
+      "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-8.2.1.tgz",
+      "integrity": "sha512-Qd2E1pleDR4bwyFxqbjt4eJf+wB0UKVMLc7/BAFDGVdAXQMCsD4DUv5/7/ww47BZCYxWtJqe1Lo0KVNswBJlRw==",
+      "dev": true,
+      "dependencies": {
+        "@rollup/pluginutils": "^3.1.0",
+        "resolve": "^1.17.0"
+      },
+      "engines": {
+        "node": ">=8.0.0"
+      },
+      "peerDependencies": {
+        "rollup": "^2.14.0",
+        "tslib": "*",
+        "typescript": ">=3.7.0"
+      }
+    },
+    "node_modules/@rollup/plugin-virtual": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/@rollup/plugin-virtual/-/plugin-virtual-2.0.3.tgz",
+      "integrity": "sha512-pw6ziJcyjZtntQ//bkad9qXaBx665SgEL8C8KI5wO8G5iU5MPxvdWrQyVaAvjojGm9tJoS8M9Z/EEepbqieYmw==",
+      "dev": true,
+      "peerDependencies": {
+        "rollup": "^1.20.0||^2.0.0"
+      }
+    },
+    "node_modules/@rollup/pluginutils": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz",
+      "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==",
+      "dev": true,
+      "dependencies": {
+        "@types/estree": "0.0.39",
+        "estree-walker": "^1.0.1",
+        "picomatch": "^2.2.2"
+      },
+      "engines": {
+        "node": ">= 8.0.0"
+      },
+      "peerDependencies": {
+        "rollup": "^1.20.0||^2.0.0"
+      }
+    },
+    "node_modules/@rollup/pluginutils/node_modules/@types/estree": {
+      "version": "0.0.39",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
+      "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==",
+      "dev": true
+    },
     "node_modules/@semantic-release/changelog": {
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/@semantic-release/changelog/-/changelog-5.0.1.tgz",
@@ -3793,6 +3875,15 @@
       "integrity": "sha512-c5ciR06jK8u9BstrmJyO97m+klJrrhCf9u3rLu3DEAJBirxRqSCvDQoYKmxuYwQI5SZChAWu+tq9oVlGRuzPAg==",
       "dev": true
     },
+    "node_modules/@types/fs-extra": {
+      "version": "8.1.1",
+      "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.1.tgz",
+      "integrity": "sha512-TcUlBem321DFQzBNuz8p0CLLKp0VvF/XH9E4KHNmgwyp4E3AfgI5cjiIVZWlbfThBop2qxFIh4+LeY6hVWWZ2w==",
+      "dev": true,
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
     "node_modules/@types/glob": {
       "version": "7.1.3",
       "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz",
@@ -8507,6 +8598,12 @@
         "node": ">=4.0"
       }
     },
+    "node_modules/estree-walker": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz",
+      "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==",
+      "dev": true
+    },
     "node_modules/esutils": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
@@ -12585,6 +12682,15 @@
         "node": ">=10"
       }
     },
+    "node_modules/magic-string": {
+      "version": "0.25.7",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
+      "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==",
+      "dev": true,
+      "dependencies": {
+        "sourcemap-codec": "^1.4.4"
+      }
+    },
     "node_modules/make-dir": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
@@ -18548,6 +18654,119 @@
         "inherits": "^2.0.1"
       }
     },
+    "node_modules/rollup": {
+      "version": "2.51.2",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.51.2.tgz",
+      "integrity": "sha512-ReV2eGEadA7hmXSzjxdDKs10neqH2QURf2RxJ6ayAlq93ugy6qIvXMmbc5cWMGCDh1h5T4thuWO1e2VNbMq8FA==",
+      "dev": true,
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.1"
+      }
+    },
+    "node_modules/rollup-plugin-copy": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/rollup-plugin-copy/-/rollup-plugin-copy-3.4.0.tgz",
+      "integrity": "sha512-rGUmYYsYsceRJRqLVlE9FivJMxJ7X6jDlP79fmFkL8sJs7VVMSVyA2yfyL+PGyO/vJs4A87hwhgVfz61njI+uQ==",
+      "dev": true,
+      "dependencies": {
+        "@types/fs-extra": "^8.0.1",
+        "colorette": "^1.1.0",
+        "fs-extra": "^8.1.0",
+        "globby": "10.0.1",
+        "is-plain-object": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8.3"
+      }
+    },
+    "node_modules/rollup-plugin-copy/node_modules/fs-extra": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
+      "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
+      "dev": true,
+      "dependencies": {
+        "graceful-fs": "^4.2.0",
+        "jsonfile": "^4.0.0",
+        "universalify": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=6 <7 || >=8"
+      }
+    },
+    "node_modules/rollup-plugin-copy/node_modules/globby": {
+      "version": "10.0.1",
+      "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.1.tgz",
+      "integrity": "sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==",
+      "dev": true,
+      "dependencies": {
+        "@types/glob": "^7.1.1",
+        "array-union": "^2.1.0",
+        "dir-glob": "^3.0.1",
+        "fast-glob": "^3.0.3",
+        "glob": "^7.1.3",
+        "ignore": "^5.1.1",
+        "merge2": "^1.2.3",
+        "slash": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/rollup-plugin-copy/node_modules/is-plain-object": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz",
+      "integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/rollup-plugin-copy/node_modules/jsonfile": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+      "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
+      "dev": true,
+      "optionalDependencies": {
+        "graceful-fs": "^4.1.6"
+      }
+    },
+    "node_modules/rollup-plugin-copy/node_modules/universalify": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+      "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
+      "dev": true,
+      "engines": {
+        "node": ">= 4.0.0"
+      }
+    },
+    "node_modules/rollup-plugin-dts": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-3.0.2.tgz",
+      "integrity": "sha512-hswlsdWu/x7k5pXzaLP6OvKRKcx8Bzprksz9i9mUe72zvt8LvqAb/AZpzs6FkLgmyRaN8B6rUQOVtzA3yEt9Yw==",
+      "dev": true,
+      "dependencies": {
+        "magic-string": "^0.25.7"
+      },
+      "engines": {
+        "node": ">=v12.22.1"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/Swatinem"
+      },
+      "optionalDependencies": {
+        "@babel/code-frame": "^7.12.13"
+      },
+      "peerDependencies": {
+        "rollup": "^2.48.0",
+        "typescript": "^4.2.4"
+      }
+    },
     "node_modules/run-parallel": {
       "version": "1.1.9",
       "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz",
@@ -19205,6 +19424,12 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/sourcemap-codec": {
+      "version": "1.4.8",
+      "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
+      "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
+      "dev": true
+    },
     "node_modules/space-separated-tokens": {
       "version": "1.1.5",
       "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz",
@@ -23112,6 +23337,61 @@
         "@octokit/openapi-types": "^5.2.0"
       }
     },
+    "@rollup/plugin-json": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-4.1.0.tgz",
+      "integrity": "sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw==",
+      "dev": true,
+      "requires": {
+        "@rollup/pluginutils": "^3.0.8"
+      }
+    },
+    "@rollup/plugin-replace": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz",
+      "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==",
+      "dev": true,
+      "requires": {
+        "@rollup/pluginutils": "^3.1.0",
+        "magic-string": "^0.25.7"
+      }
+    },
+    "@rollup/plugin-typescript": {
+      "version": "8.2.1",
+      "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-8.2.1.tgz",
+      "integrity": "sha512-Qd2E1pleDR4bwyFxqbjt4eJf+wB0UKVMLc7/BAFDGVdAXQMCsD4DUv5/7/ww47BZCYxWtJqe1Lo0KVNswBJlRw==",
+      "dev": true,
+      "requires": {
+        "@rollup/pluginutils": "^3.1.0",
+        "resolve": "^1.17.0"
+      }
+    },
+    "@rollup/plugin-virtual": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/@rollup/plugin-virtual/-/plugin-virtual-2.0.3.tgz",
+      "integrity": "sha512-pw6ziJcyjZtntQ//bkad9qXaBx665SgEL8C8KI5wO8G5iU5MPxvdWrQyVaAvjojGm9tJoS8M9Z/EEepbqieYmw==",
+      "dev": true,
+      "requires": {}
+    },
+    "@rollup/pluginutils": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz",
+      "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==",
+      "dev": true,
+      "requires": {
+        "@types/estree": "0.0.39",
+        "estree-walker": "^1.0.1",
+        "picomatch": "^2.2.2"
+      },
+      "dependencies": {
+        "@types/estree": {
+          "version": "0.0.39",
+          "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
+          "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==",
+          "dev": true
+        }
+      }
+    },
     "@semantic-release/changelog": {
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/@semantic-release/changelog/-/changelog-5.0.1.tgz",
@@ -23665,6 +23945,15 @@
       "integrity": "sha512-c5ciR06jK8u9BstrmJyO97m+klJrrhCf9u3rLu3DEAJBirxRqSCvDQoYKmxuYwQI5SZChAWu+tq9oVlGRuzPAg==",
       "dev": true
     },
+    "@types/fs-extra": {
+      "version": "8.1.1",
+      "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.1.tgz",
+      "integrity": "sha512-TcUlBem321DFQzBNuz8p0CLLKp0VvF/XH9E4KHNmgwyp4E3AfgI5cjiIVZWlbfThBop2qxFIh4+LeY6hVWWZ2w==",
+      "dev": true,
+      "requires": {
+        "@types/node": "*"
+      }
+    },
     "@types/glob": {
       "version": "7.1.3",
       "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz",
@@ -27398,6 +27687,12 @@
       "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
       "dev": true
     },
+    "estree-walker": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz",
+      "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==",
+      "dev": true
+    },
     "esutils": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
@@ -30578,6 +30873,15 @@
         "yallist": "^4.0.0"
       }
     },
+    "magic-string": {
+      "version": "0.25.7",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
+      "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==",
+      "dev": true,
+      "requires": {
+        "sourcemap-codec": "^1.4.4"
+      }
+    },
     "make-dir": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
@@ -34821,6 +35125,88 @@
         "inherits": "^2.0.1"
       }
     },
+    "rollup": {
+      "version": "2.51.2",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.51.2.tgz",
+      "integrity": "sha512-ReV2eGEadA7hmXSzjxdDKs10neqH2QURf2RxJ6ayAlq93ugy6qIvXMmbc5cWMGCDh1h5T4thuWO1e2VNbMq8FA==",
+      "dev": true,
+      "requires": {
+        "fsevents": "~2.3.1"
+      }
+    },
+    "rollup-plugin-copy": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/rollup-plugin-copy/-/rollup-plugin-copy-3.4.0.tgz",
+      "integrity": "sha512-rGUmYYsYsceRJRqLVlE9FivJMxJ7X6jDlP79fmFkL8sJs7VVMSVyA2yfyL+PGyO/vJs4A87hwhgVfz61njI+uQ==",
+      "dev": true,
+      "requires": {
+        "@types/fs-extra": "^8.0.1",
+        "colorette": "^1.1.0",
+        "fs-extra": "^8.1.0",
+        "globby": "10.0.1",
+        "is-plain-object": "^3.0.0"
+      },
+      "dependencies": {
+        "fs-extra": {
+          "version": "8.1.0",
+          "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
+          "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
+          "dev": true,
+          "requires": {
+            "graceful-fs": "^4.2.0",
+            "jsonfile": "^4.0.0",
+            "universalify": "^0.1.0"
+          }
+        },
+        "globby": {
+          "version": "10.0.1",
+          "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.1.tgz",
+          "integrity": "sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==",
+          "dev": true,
+          "requires": {
+            "@types/glob": "^7.1.1",
+            "array-union": "^2.1.0",
+            "dir-glob": "^3.0.1",
+            "fast-glob": "^3.0.3",
+            "glob": "^7.1.3",
+            "ignore": "^5.1.1",
+            "merge2": "^1.2.3",
+            "slash": "^3.0.0"
+          }
+        },
+        "is-plain-object": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz",
+          "integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==",
+          "dev": true
+        },
+        "jsonfile": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+          "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
+          "dev": true,
+          "requires": {
+            "graceful-fs": "^4.1.6"
+          }
+        },
+        "universalify": {
+          "version": "0.1.2",
+          "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+          "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
+          "dev": true
+        }
+      }
+    },
+    "rollup-plugin-dts": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-3.0.2.tgz",
+      "integrity": "sha512-hswlsdWu/x7k5pXzaLP6OvKRKcx8Bzprksz9i9mUe72zvt8LvqAb/AZpzs6FkLgmyRaN8B6rUQOVtzA3yEt9Yw==",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "^7.12.13",
+        "magic-string": "^0.25.7"
+      }
+    },
     "run-parallel": {
       "version": "1.1.9",
       "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz",
@@ -35338,6 +35724,12 @@
         }
       }
     },
+    "sourcemap-codec": {
+      "version": "1.4.8",
+      "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
+      "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
+      "dev": true
+    },
     "space-separated-tokens": {
       "version": "1.1.5",
       "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz",
diff --git a/package.json b/package.json
index 83cc8236c352ae25bff911687f06256c6656fa9b..9f91f4f7cc5e12c44fd5a474bf1098eb095cd49d 100644
--- a/package.json
+++ b/package.json
@@ -19,8 +19,20 @@
   "license": "MIT",
   "author": "David Sveningsson <ext@sidvind.com>",
   "sideEffects": false,
-  "main": "dist/index.js",
-  "browser": "dist/browser.js",
+  "type": "commonjs",
+  "exports": {
+    ".": {
+      "require": "./dist/cjs/index.js",
+      "import": "./dist/es/index.js"
+    },
+    "./elements/*": "./elements/*",
+    "./jest.js": "./jest.js",
+    "./package.json": "./package.json",
+    "./test-utils.js": "./test-utils.js"
+  },
+  "main": "dist/cjs/index.js",
+  "module": "dist/es/index.js",
+  "browser": "dist/cjs/browser.js",
   "bin": {
     "html-validate": "bin/html-validate.js"
   },
@@ -30,17 +42,14 @@
     "elements",
     "jest.{js,d.ts}",
     "test-utils.{js,d.ts}",
-    "!**/*.map",
+    "!dist/types/**",
     "!**/*.snap",
-    "!**/*.spec.d.ts",
-    "!**/*.spec.js",
-    "!**/*.spec.ts",
-    "!**/__fixtures__",
-    "!**/__mocks__",
-    "!dist/rules/**/*.d.ts"
+    "!**/*.spec.{js,ts,d.ts}"
   ],
   "scripts": {
-    "build": "tsc",
+    "prebuild": "tsc",
+    "build": "rollup --config rollup.config.js",
+    "postbuild": "scripts/pkg",
     "build:docs": "grunt docs",
     "clean": "rm -rf dist public",
     "compatibility": "scripts/compatibility.sh",
@@ -141,6 +150,10 @@
     "@html-validate/prettier-config": "1.1.0",
     "@html-validate/semantic-release-config": "2.0.0",
     "@lodder/grunt-postcss": "3.0.1",
+    "@rollup/plugin-json": "4.1.0",
+    "@rollup/plugin-replace": "2.4.2",
+    "@rollup/plugin-typescript": "8.2.1",
+    "@rollup/plugin-virtual": "2.0.3",
     "@types/babar": "0.2.0",
     "@types/babel__code-frame": "7.0.2",
     "@types/estree": "0.0.47",
@@ -181,6 +194,9 @@
     "npm-pkg-lint": "1.4.0",
     "postcss": "8.3.5",
     "prettier": "2.3.1",
+    "rollup": "2.51.2",
+    "rollup-plugin-copy": "3.4.0",
+    "rollup-plugin-dts": "3.0.2",
     "sass": "1.35.1",
     "semantic-release": "17.4.4",
     "serve-static": "1.14.1",
diff --git a/rollup.config.js b/rollup.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..eca9ae70f6715a8e2b0f5382c01a9ac6964c3a77
--- /dev/null
+++ b/rollup.config.js
@@ -0,0 +1,153 @@
+import fs from "fs";
+import path from "path";
+import json from "@rollup/plugin-json";
+import replace from "@rollup/plugin-replace";
+import virtual from "@rollup/plugin-virtual";
+import copy from "rollup-plugin-copy";
+import dts from "rollup-plugin-dts";
+import typescript from "@rollup/plugin-typescript";
+
+/**
+ * @typedef {import('rollup').RollupOptions} RollupOptions
+ */
+
+/** @type {string[]} */
+const entrypoints = [
+	"src/index.ts",
+	"src/browser.ts",
+	"src/cli/html-validate.ts",
+	"src/matchers.ts",
+	"src/transform/test-utils.ts",
+];
+
+/** @type {string[]} */
+const types = [
+	"dist/types/index.d.ts",
+	"dist/types/browser.d.ts",
+	"dist/types/matchers.d.ts",
+	"dist/types/transform/test-utils.d.ts",
+];
+
+/** @type {string[]} */
+const inputs = [...entrypoints, ...types];
+
+/** @type {string[]} */
+const external = [
+	/* nodejs */
+	"fs",
+	"path",
+
+	/* dependencies */
+	"@babel/code-frame",
+	"@html-validate/stylish",
+	"@sidvind/better-ajv-errors",
+	"ajv",
+	"deepmerge",
+	"glob",
+	"ignore",
+	"jest-diff",
+	"json-merge-patch",
+	"kleur",
+	"minimist",
+	"prompts",
+];
+
+const packageJson = fs.readFileSync(path.join(__dirname, "package.json"), "utf-8");
+
+/**
+ * @param {string} id
+ * @returns {string|undefined}
+ */
+function manualChunks(id) {
+	/** @type {string} */
+	const base = path.relative(__dirname, id);
+	if (inputs.includes(base)) {
+		return undefined;
+	}
+
+	/** @type {string} */
+	const rel = base.startsWith("src/")
+		? path.relative(path.join(__dirname, "src"), id)
+		: path.relative(path.join(__dirname, "dist/types"), id);
+
+	if (rel.startsWith("cli/")) {
+		return "cli";
+	}
+
+	return "core";
+}
+
+/**
+ * @param {string} format
+ * @returns {RollupOptions[]}
+ */
+function build(format) {
+	const resolved = `
+		import path from "path";
+		export const projectRoot = path.resolve(__dirname, "../../");
+		export const distFolder = path.resolve(projectRoot, "dist/${format}");
+	`;
+	return [
+		{
+			input: entrypoints,
+			output: {
+				dir: `dist/${format}`,
+				format,
+				sourcemap: true,
+				manualChunks,
+				chunkFileNames: "[name].js",
+			},
+			external,
+			plugins: [
+				virtual({
+					"package.json": packageJson,
+					"src/resolve": resolved,
+				}),
+				typescript({
+					outDir: `dist/${format}`,
+					declaration: false,
+					declarationDir: undefined,
+				}),
+				json(),
+				replace({
+					preventAssignment: true,
+					delimiters: ["", ""],
+					values: {
+						/**
+						 * Fix the path from src/package.ts
+						 */
+						'"../package.json"': '"../../package.json"',
+						/**
+						 * Replace __filename global with source filename relative to dist folder
+						 *
+						 * @param {string} filename
+						 */
+						__filename: (filename) => {
+							const relative = path.relative(path.join(__dirname, "src"), filename);
+							return `"@/${relative}"`;
+						},
+					},
+				}),
+			],
+		},
+		{
+			input: types,
+			output: {
+				dir: `dist/${format}`,
+				format,
+				manualChunks,
+				chunkFileNames: "[name].d.ts",
+			},
+			plugins: [
+				dts(),
+				copy({
+					verbose: true,
+					targets: [{ src: "src/schema/*.json", dest: "dist/schema" }],
+				}),
+			],
+		},
+	];
+}
+
+/** @type {RollupOptions[]} */
+export default [...build("cjs"), ...build("es")];
diff --git a/scripts/pkg b/scripts/pkg
new file mode 100755
index 0000000000000000000000000000000000000000..10f58816d2470691b71eda5feb59fcc678963ef4
--- /dev/null
+++ b/scripts/pkg
@@ -0,0 +1,13 @@
+#!/bin/sh
+
+cat > dist/cjs/package.json <<EOF
+{
+    "type": "commonjs"
+}
+EOF
+
+cat > dist/es/package.json <<EOF
+{
+    "type": "module"
+}
+EOF
diff --git a/src/browser.ts b/src/browser.ts
index 2bf3631180277df9571400efc13d57f1f2014a38..abf3a68823565dd0b98e7cd3f0edbaddfd2a8529 100644
--- a/src/browser.ts
+++ b/src/browser.ts
@@ -2,12 +2,12 @@
 
 export { default as HtmlValidate } from "./htmlvalidate";
 export { AttributeData } from "./parser";
-export { Config, ConfigData, ConfigError, ConfigLoader, Severity } from "./config";
+export { Config, ConfigData, ConfigError, ConfigLoader, Severity, configPresets } from "./config";
 export { DynamicValue, HtmlElement, NodeClosed, TextNode } from "./dom";
 export { EventDump, TokenDump } from "./engine";
 export { UserError, SchemaValidationError } from "./error";
 export * from "./event";
-export { MetaData, MetaElement, MetaTable } from "./meta";
+export { MetaData, MetaElement, MetaTable, MetaCopyableProperty } from "./meta";
 export { Rule, RuleDocumentation } from "./rule";
 export { Source, Location, ProcessElementContext } from "./context";
 export { Report, Reporter, Message, Result } from "./reporter";
@@ -15,7 +15,4 @@ export { Transformer, TemplateExtractor } from "./transform";
 export { Plugin } from "./plugin";
 export { Parser } from "./parser";
 export { ruleExists } from "./utils";
-
-const pkg = require("../package.json");
-
-export const version = pkg.version;
+export { version } from "./package";
diff --git a/src/cli/html-validate.ts b/src/cli/html-validate.ts
index 8cb02aaac4fbb307dd9911253e7f0fdbabb585d2..f2cc05ae982a6a9525f0606073ba7e564f027545 100644
--- a/src/cli/html-validate.ts
+++ b/src/cli/html-validate.ts
@@ -3,12 +3,10 @@ import path from "path";
 import kleur from "kleur";
 import minimist from "minimist";
 import { TokenDump, SchemaValidationError, UserError, Report, Reporter, Result } from "..";
+import * as pkg from "../package";
 import { eventFormatter } from "./json";
-
 import { CLI } from "./cli";
 
-const pkg = require("../../package.json");
-
 enum Mode {
 	LINT,
 	INIT,
@@ -125,7 +123,7 @@ function handleUnknownError(err: Error): void {
 		console.error(err);
 	}
 	console.groupEnd();
-	const bugUrl = `${pkg.bugs.url}?issuable_template=Bug`;
+	const bugUrl = `${pkg.bugs}?issuable_template=Bug`;
 	console.error(kleur.red(`This is a bug in ${pkg.name}-${pkg.version}.`));
 	console.error(
 		kleur.red(
diff --git a/src/config/config.ts b/src/config/config.ts
index 9d40c6d9537fb00739fe543616293dcc9e5f3cfc..cc2df4438c6b227eca8aacd034532449011b1d23 100644
--- a/src/config/config.ts
+++ b/src/config/config.ts
@@ -10,6 +10,7 @@ import { Plugin } from "../plugin";
 import schema from "../schema/config.json";
 import { TransformContext, Transformer, TRANSFORMER_API } from "../transform";
 import { requireUncached } from "../utils";
+import { projectRoot } from "../resolve";
 import bundledRules from "../rules";
 import { Rule } from "../rule";
 import { ConfigData, RuleConfig, RuleOptions, TransformMap } from "./config-data";
@@ -251,7 +252,6 @@ export class Config {
 
 		const metaTable = new MetaTable();
 		const source = this.config.elements || ["html5"];
-		const root = path.resolve(__dirname, "..", "..");
 
 		/* extend validation schema from plugins */
 		for (const plugin of this.getPlugins()) {
@@ -271,7 +271,7 @@ export class Config {
 			let filename: string;
 
 			/* try searching builtin metadata */
-			filename = path.join(root, "elements", `${entry}.json`);
+			filename = path.join(projectRoot, "elements", `${entry}.json`);
 			if (fs.existsSync(filename)) {
 				metaTable.loadFromFile(filename);
 				continue;
diff --git a/src/config/index.ts b/src/config/index.ts
index 33c5732b28d7f442fd5bc2947a34b950aa8e67b5..94c5153a1074bd5b69a76c110ad1b1039635d5fd 100644
--- a/src/config/index.ts
+++ b/src/config/index.ts
@@ -2,5 +2,6 @@ export { Config } from "./config";
 export { ConfigData, RuleConfig, RuleOptions } from "./config-data";
 export { ConfigLoader } from "./config-loader";
 export { ConfigError } from "./error";
+export { default as configPresets } from "./presets";
 export { ResolvedConfig } from "./resolved-config";
 export { Severity } from "./severity";
diff --git a/src/index.ts b/src/index.ts
index 873a5ab7e001b1c93f5aafea33363452c0a496e2..06b19d98c232e95fb6ccf9be723ec44486b070b2 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,5 +1,5 @@
 /* used when calling require('htmlvalidate'); */
 
 export * from "./browser";
-export { CLI } from "./cli/cli";
 export { Formatter, getFormatter as formatterFactory } from "./formatters";
+export { CLI } from "./cli/cli";
diff --git a/src/package.ts b/src/package.ts
new file mode 100644
index 0000000000000000000000000000000000000000..68cbc31ed9fce4f395e19ef34e13b0e495e5fdd6
--- /dev/null
+++ b/src/package.ts
@@ -0,0 +1,6 @@
+const pkg = require("../package.json");
+
+export const name: string = pkg.name;
+export const version: string = pkg.version;
+export const homepage: string = pkg.homepage;
+export const bugs: string = pkg.bugs.url;
diff --git a/src/resolve.ts b/src/resolve.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fadd2b545ccce86b3e68860ac9e0063aa66db609
--- /dev/null
+++ b/src/resolve.ts
@@ -0,0 +1,9 @@
+/**
+ * This module will be overwritten by bundler during compilation. Make sure that
+ * any changes to this file is present in the virtual module.
+ */
+
+import path from "path";
+
+export const projectRoot = path.resolve(__dirname, "..");
+export const distFolder = path.join(projectRoot, "src");
diff --git a/src/rule.ts b/src/rule.ts
index f03de32353dfde4b2fcb760993f1ab04dcb967b9..47e7f1ce66eccf41b489154a63b24bb8210f6e4a 100644
--- a/src/rule.ts
+++ b/src/rule.ts
@@ -8,11 +8,11 @@ import { Parser } from "./parser";
 import { Reporter } from "./reporter";
 import { MetaTable, MetaLookupableProperty } from "./meta";
 import { SchemaValidationError } from "./error";
+import { distFolder } from "./resolve";
+import { homepage } from "./package";
 
 export { SchemaObject } from "ajv";
 
-const homepage = require("../package.json").homepage;
-
 const remapEvents: Record<string, string> = {
 	"tag:open": "tag:start",
 	"tag:close": "tag:end",
@@ -355,7 +355,12 @@ export abstract class Rule<ContextType = void, OptionsType = void> {
 }
 
 export function ruleDocumentationUrl(filename: string): string {
+	/* during bundling all __filename's are converted to paths relative to the src
+	 * folder and with the @/ prefix, by replacing the @ with the dist folder we
+	 * can resolve the path properly */
+	filename = filename.replace("@", distFolder);
 	const p = path.parse(filename);
-	const rel = path.relative(path.join(__dirname, "rules"), path.join(p.dir, p.name));
+	const root = path.join(distFolder, "rules");
+	const rel = path.relative(root, path.join(p.dir, p.name));
 	return `${homepage}/rules/${rel}.html`;
 }
diff --git a/src/rules/unrecognized-char-ref.ts b/src/rules/unrecognized-char-ref.ts
index 58d3a99190a8dc858f1eabae5c1bea2050638633..3d70684b81d7cc86e9e9ce6fcdcc9c9dceb44c8f 100644
--- a/src/rules/unrecognized-char-ref.ts
+++ b/src/rules/unrecognized-char-ref.ts
@@ -1,9 +1,12 @@
+import path from "path";
 import { Location, sliceLocation } from "../context";
 import { NodeType } from "../dom";
 import { AttributeEvent, ElementReadyEvent } from "../event";
+import { projectRoot } from "../resolve";
 import { Rule, RuleDocumentation, ruleDocumentationUrl } from "../rule";
 
-const entities = require("../../elements/entities.json");
+/* eslint-disable-next-line import/no-dynamic-require */
+const entities = require(path.join(projectRoot, "elements/entities.json"));
 
 const regexp = /&([a-z0-9]+|#x?[0-9a-f]+);/gi;
 
diff --git a/test-utils.d.ts b/test-utils.d.ts
index d800fcb08d97e1b8807750b0289cf92e48f956b8..bf30b564175a1ea7a19665b929704afcfc2d743c 100644
--- a/test-utils.d.ts
+++ b/test-utils.d.ts
@@ -1,2 +1,2 @@
 /* eslint-disable-next-line import/export, import/no-unresolved */
-export * from "./dist/transform/test-utils";
+export * from "./dist/cjs/test-utils";
diff --git a/test-utils.js b/test-utils.js
index 2caf80049b9108dc45a50cccb68c83dbd795ef0f..e2983c745c374331c64d00fc92e01587547ba5cd 100644
--- a/test-utils.js
+++ b/test-utils.js
@@ -1 +1 @@
-module.exports = require("./dist/transform/test-utils");
+module.exports = require("./dist/cjs/test-utils");
diff --git a/tsconfig.json b/tsconfig.json
index 881b92cef2450e8960b5900dd1f0d4754ccf46dd..7392f6b9d786606de9bab03149d2f011e3628ecb 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,18 +1,24 @@
 {
   "compilerOptions": {
-    "alwaysStrict": true,
     "rootDir": "./src",
+    "outDir": "dist",
+
+    "emitDeclarationOnly": true,
     "declaration": true,
-    "esModuleInterop": true,
+    "declarationDir": "dist/types",
+
+    "target": "es2017",
     "lib": ["es2017"],
-    "module": "commonjs",
+    "module": "esnext",
+    "moduleResolution": "node",
+
+    "alwaysStrict": true,
+    "esModuleInterop": true,
     "noImplicitAny": true,
-    "outDir": "dist",
     "resolveJsonModule": true,
     "sourceMap": true,
-    "target": "es2017",
     "strict": true
   },
   "include": ["src/**/*.ts"],
-  "exclude": ["node_modules"]
+  "exclude": ["node_modules", "**/*.spec.ts"]
 }