From 33cbced0ba806fdad7c157b5df0e1353d0b7d031 Mon Sep 17 00:00:00 2001
From: David Sveningsson <ext@sidvind.com>
Date: Sun, 20 Jun 2021 01:25:37 +0200
Subject: [PATCH] feat!: cjs/esm hybrid package

fixes #112

BREAKING CHANGE: the library is now shipped as a hybrid CJS/ESM package. If you
are simply consuming the CLI tool or one of the existing integrations this will
not affect you.

For plugin developers and if you consume the API in any way the biggest change
is that the distributed source is now bundled and you can no longer access
individual files.

Typically something like:

```diff
-import foo from "html-validate/dist/foo";
+import { foo } from "html-validate"
```

Feel free to open an issue if some symbol you need isn't exported.

If your usage includes checking presence of rules use the `ruleExists` helper:

```diff
-try {
-  require("html-validate/dist/rules/attr-case");
-} catch (err) {
-  /* fallback */
-}
+import { ruleExists } from "html-validate";
+if (!ruleExists("attr-case")) {
+  /* fallback */
+}
```
---
 README.md                                     |  16 +
 bin/html-validate.js                          |   2 +-
 docs/dev/running-in-browser.md                | 105 +++++
 .../processors/validate-results.js            |   5 +-
 docs/dgeni/processors/rules.js                |  16 +-
 docs/dgeni/templates/base.template.html       |   1 +
 jest.d.ts                                     |   2 +-
 jest.js                                       |   2 +-
 package-lock.json                             | 392 ++++++++++++++++++
 package.json                                  |  36 +-
 rollup.config.js                              | 153 +++++++
 scripts/pkg                                   |  13 +
 src/browser.ts                                |   9 +-
 src/cli/html-validate.ts                      |   6 +-
 src/config/config.ts                          |   4 +-
 src/config/index.ts                           |   1 +
 src/index.ts                                  |   2 +-
 src/package.ts                                |   6 +
 src/resolve.ts                                |   9 +
 src/rule.ts                                   |  11 +-
 src/rules/unrecognized-char-ref.ts            |   5 +-
 test-utils.d.ts                               |   2 +-
 test-utils.js                                 |   2 +-
 tsconfig.json                                 |  18 +-
 24 files changed, 765 insertions(+), 53 deletions(-)
 create mode 100644 docs/dev/running-in-browser.md
 create mode 100644 rollup.config.js
 create mode 100755 scripts/pkg
 create mode 100644 src/package.ts
 create mode 100644 src/resolve.ts

diff --git a/README.md b/README.md
index 61623117b..18816d1e3 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 8b8075382..ca2b9d022 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 000000000..e70edd620
--- /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 5a2250f0e..d601ac588 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 c0ab6816d..ec1e69248 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 d87609cf7..be139139f 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 a5501a872..c0166699a 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 678839a98..b7699d037 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 18b97e3cc..53755e806 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 83cc8236c..9f91f4f7c 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 000000000..eca9ae70f
--- /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 000000000..10f58816d
--- /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 2bf363118..abf3a6882 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 8cb02aaac..f2cc05ae9 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 9d40c6d95..cc2df4438 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 33c5732b2..94c5153a1 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 873a5ab7e..06b19d98c 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 000000000..68cbc31ed
--- /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 000000000..fadd2b545
--- /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 f03de3235..47e7f1ce6 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 58d3a9919..3d70684b8 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 d800fcb08..bf30b5641 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 2caf80049..e2983c745 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 881b92cef..7392f6b9d 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"]
 }
-- 
GitLab