Repository: macbre/analyze-css (js)

Issue:
Add .d.ts files with types declaration
* [x] prepare `*.d.ts` files - https://www.typescriptlang.org/docs/handbook/declaration-files/templates/module-d-ts.html
* [ ] ~use [`check-dts` for testing `*.d.ts` files](https://github.com/ai/check-dts)~

<img width="796" alt="Screenshot 2021-08-04 at 14 59 00" src="https://user-images.githubusercontent.com/1929317/128194122-74f779ca-a60b-468f-93b8-fabdf8457ef5.png">


Pull request description:
Provide types declaration
Resolves #354.

This PR introduces a braking change regarding how `analyze-css` is imported:

```js
const analyze = require('analyze-css').analyze;
```

----

While this package is not written in TypeScript, we can still take an advantage of type hinting and help ourselves with auto-completion in IDE:

<img width="796" alt="Screenshot 2021-08-04 at 14 59 00" src="https://user-images.githubusercontent.com/1929317/128194122-74f779ca-a60b-468f-93b8-fabdf8457ef5.png">
 
Plus add [inline JSDoc hints](https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html) in core files.

Patch:
diff --git a/.eslintrc.yml b/.eslintrc.yml
index 34695aa..3830685 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -13,3 +13,4 @@ rules:
   - allowEmptyCatch: true
   no-prototype-builtins: 'off'
   "node/no-extraneous-require": 'off'
+  "node/shebang": 'off'
diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml
index 68df6ed..db9bcc7 100644
--- a/.github/workflows/linter.yml
+++ b/.github/workflows/linter.yml
@@ -21,5 +21,8 @@ jobs:
     - name: Run prettier
       run: npx prettier --check .
 
+    - name: Run check-dts
+      run: npx check-dts
+
     - name: Run eslint
       run: npm run lint
diff --git a/README.md b/README.md
index a1adb1d..dc30965 100644
--- a/README.md
+++ b/README.md
@@ -54,7 +54,7 @@ npm i --save analyze-css
 ```
 
 ```js
-const analyze = require('analyze-css');
+const analyze = require('analyze-css').analyze;
 
 (async() => {
   const results = await analyze('.foo {margin: 0 !important}');
diff --git a/bin/analyze-css.js b/bin/analyze-css.js
index f7b99cc..7fd5370 100755
--- a/bin/analyze-css.js
+++ b/bin/analyze-css.js
@@ -9,7 +9,7 @@
 
 const { program } = require("commander");
 
-var analyzer = require("./../lib/index"),
+var analyzer = require("./../lib/index").analyze,
   debug = require("debug")("analyze-css:bin"),
   runner = require("./../lib/runner"),
   runnerOpts = {};
diff --git a/examples/example.js b/examples/example.js
new file mode 100755
index 0000000..77312a9
--- /dev/null
+++ b/examples/example.js
@@ -0,0 +1,22 @@
+#!/usr/bin/env node
+const analyze = require('../').analyze;
+
+(async() => {
+  const results = await analyze('.foo {margin: 0 !important}');
+
+  console.log(results.generator);
+  console.dir(results.metrics);
+  console.dir(results.offenders);
+
+  // list of all available metrics
+  console.log(
+      'All available metrics (for type hinting):\n',
+      'type MetricsNames = ' +
+      Object.keys(results.metrics)
+        .sort()
+        .map(metric => {
+            return `"${metric}"`;
+        })
+        .join(' |\n\t') + ';'
+  )
+})();
diff --git a/lib/collection.js b/lib/collection.js
index 9fefe39..ff6c9ee 100644
--- a/lib/collection.js
+++ b/lib/collection.js
@@ -3,11 +3,20 @@
  */
 "use strict";
 
-function collection() {
+/**
+ * @class
+ */
+function Collection() {
   this.items = {};
 }
 
-collection.prototype = {
+Collection.prototype = {
+  /**
+   * Pushes a given item to the collection and counts each occurrence
+   *
+   * @param {string} item
+   * @return {void}
+   */
   push: function (item) {
     if (typeof this.items[item] === "undefined") {
       this.items[item] = {
@@ -18,6 +27,11 @@ collection.prototype = {
     }
   },
 
+  /**
+   * Sorts collected items in desending order by their occurrences
+   *
+   * @return {Collection}
+   */
   sort: function () {
     var newItems = {},
       sortedKeys;
@@ -38,6 +52,14 @@ collection.prototype = {
     return this;
   },
 
+  /**
+   * Runs provided callback for each item in the collection.
+   *
+   * Item and the count is provided to the callback.
+   *
+   * @param {forEachCallback} callback
+   *
+   */
   forEach: function (callback) {
     Object.keys(this.items).forEach(function (key) {
       callback(key, this.items[key].cnt);
@@ -45,4 +67,10 @@ collection.prototype = {
   },
 };
 
-module.exports = collection;
+/**
+ * @callback forEachCallback
+ * @param {string} item
+ * @param {number} count
+ */
+
+module.exports = Collection;
diff --git a/lib/index.d.ts b/lib/index.d.ts
new file mode 100644
index 0000000..e723655
--- /dev/null
+++ b/lib/index.d.ts
@@ -0,0 +1,91 @@
+/**
+ * This file brings type hinting for index.js file.
+ *
+ * While this package is not written in TypeScript, this file will allow your IDE to provide you with auto-completion.
+ */
+
+declare type MetricsNames =
+  | "base64Length"
+  | "colors"
+  | "comments"
+  | "commentsLength"
+  | "complexSelectors"
+  | "declarations"
+  | "duplicatedProperties"
+  | "duplicatedSelectors"
+  | "emptyRules"
+  | "expressions"
+  | "importants"
+  | "imports"
+  | "length"
+  | "mediaQueries"
+  | "multiClassesSelectors"
+  | "notMinified"
+  | "oldIEFixes"
+  | "oldPropertyPrefixes"
+  | "parsingErrors"
+  | "propertyResets"
+  | "qualifiedSelectors"
+  | "redundantBodySelectors"
+  | "redundantChildNodesSelectors"
+  | "rules"
+  | "selectorLengthAvg"
+  | "selectors"
+  | "selectorsByAttribute"
+  | "selectorsByClass"
+  | "selectorsById"
+  | "selectorsByPseudo"
+  | "selectorsByTag"
+  | "specificityClassAvg"
+  | "specificityClassTotal"
+  | "specificityIdAvg"
+  | "specificityIdTotal"
+  | "specificityTagAvg"
+  | "specificityTagTotal";
+
+/**
+ * Encapsulates a set of metrics
+ */
+declare type Metrics = { [metric in MetricsNames]: number };
+
+/**
+ * Encapsulates a set of offenders
+ */
+declare type Offenders = { [metric in MetricsNames]: Array<Object> };
+
+/**
+ * Encapsulates results from analyze()
+ */
+declare interface Results {
+  generator: string;
+  metrics: Metrics;
+  offenders: Offenders;
+}
+
+/**
+ * analyzer instance passed to the rules code
+ *
+ * See https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html
+ */
+export class CSSAnalyzer {
+  public setMetric(
+    name: MetricsNames,
+    value: number | undefined /** = 0 */
+  ): void;
+  public incrMetric(
+    name: MetricsNames,
+    incr: number | undefined /** = 1 */
+  ): void;
+  public addOffender(
+    metricName: MetricsNames,
+    msg: string,
+    position: any | undefined
+  ): void;
+  public setCurrentPosition(position: any): void;
+  public on(ev: string, fn: any): void;
+}
+
+/**
+ * The main entry-point to analyze a given css
+ */
+export function analyze(css: string, options?: object): Promise<Results>;
diff --git a/lib/index.js b/lib/index.js
index fe3eaf5..023eceb 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -19,7 +19,7 @@ function error(msg, code) {
 }
 
 // Promise-based public endpoint
-function analyzer(css, options) {
+function analyze(css, options) {
   // options can be omitted
   options = options || {};
 
@@ -30,7 +30,7 @@ function analyzer(css, options) {
       reject(
         error(
           "css parameter passed is not a string!",
-          analyzer.EXIT_CSS_PASSED_IS_NOT_STRING
+          analyze.EXIT_CSS_PASSED_IS_NOT_STRING
         )
       );
       return;
@@ -77,19 +77,19 @@ function analyzer(css, options) {
   });
 }
 
-analyzer.version = VERSION;
+analyze.version = VERSION;
 
 // @see https://github.com/macbre/phantomas/issues/664
-analyzer.path = path.normalize(__dirname + "/..");
-analyzer.pathBin = analyzer.path + "/bin/analyze-css.js";
+analyze.path = path.normalize(__dirname + "/..");
+analyze.pathBin = analyze.path + "/bin/analyze-css.js";
 
 // exit codes
-analyzer.EXIT_NEED_OPTIONS = 2;
-analyzer.EXIT_PARSING_FAILED = 251;
-analyzer.EXIT_EMPTY_CSS = 252;
-analyzer.EXIT_CSS_PASSED_IS_NOT_STRING = 253;
-analyzer.EXIT_URL_LOADING_FAILED = 254;
-analyzer.EXIT_FILE_LOADING_FAILED = 255;
+analyze.EXIT_NEED_OPTIONS = 2;
+analyze.EXIT_PARSING_FAILED = 251;
+analyze.EXIT_EMPTY_CSS = 252;
+analyze.EXIT_CSS_PASSED_IS_NOT_STRING = 253;
+analyze.EXIT_URL_LOADING_FAILED = 254;
+analyze.EXIT_FILE_LOADING_FAILED = 255;
 
 class CSSAnalyzer {
   constructor(options) {
@@ -186,7 +186,7 @@ class CSSAnalyzer {
     debug("Going to parse %s kB of CSS", (css.length / 1024).toFixed(2));
 
     if (css.trim() === "") {
-      return error("Empty CSS was provided", analyzer.EXIT_EMPTY_CSS);
+      return error("Empty CSS was provided", analyze.EXIT_EMPTY_CSS);
     }
 
     css = this.fixCss(css);
@@ -266,7 +266,7 @@ class CSSAnalyzer {
                 rule.position.end.column;
               throw error(
                 'Unable to parse "' + selector + '" selector. ' + positionDump,
-                analyzer.EXIT_PARSING_FAILED
+                analyze.EXIT_PARSING_FAILED
               );
             }
 
@@ -381,4 +381,7 @@ class CSSAnalyzer {
   }
 }
 
-module.exports = analyzer;
+module.exports = {
+  analyze,
+  CSSAnalyzer,
+};
diff --git a/lib/runner.js b/lib/runner.js
index e770ca9..42cb120 100644
--- a/lib/runner.js
+++ b/lib/runner.js
@@ -9,7 +9,7 @@ var cli = require("cli"),
   debug = require("debug")("analyze-css:runner"),
   fs = require("fs"),
   resolve = require("path").resolve,
-  analyzer = require("./index"),
+  analyzer = require("./index").analyze,
   preprocessors = new (require("./preprocessors"))();
 
 /**
diff --git a/package-lock.json b/package-lock.json
index 56f143a..b014242 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -718,6 +718,32 @@
         "chalk": "^4.0.0"
       }
     },
+    "@nodelib/fs.scandir": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+      "dev": true,
+      "requires": {
+        "@nodelib/fs.stat": "2.0.5",
+        "run-parallel": "^1.1.9"
+      }
+    },
+    "@nodelib/fs.stat": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+      "dev": true
+    },
+    "@nodelib/fs.walk": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+      "dev": true,
+      "requires": {
+        "@nodelib/fs.scandir": "2.1.5",
+        "fastq": "^1.6.0"
+      }
+    },
     "@sinonjs/commons": {
       "version": "1.8.3",
       "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz",
@@ -839,6 +865,12 @@
       "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==",
       "dev": true
     },
+    "@types/unist": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz",
+      "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==",
+      "dev": true
+    },
     "@types/yargs": {
       "version": "16.0.3",
       "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.3.tgz",
@@ -1006,6 +1038,12 @@
         "sprintf-js": "~1.0.2"
       }
     },
+    "array-union": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+      "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+      "dev": true
+    },
     "astral-regex": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
@@ -1241,6 +1279,19 @@
       "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==",
       "dev": true
     },
+    "check-dts": {
+      "version": "0.5.5",
+      "resolved": "https://registry.npmjs.org/check-dts/-/check-dts-0.5.5.tgz",
+      "integrity": "sha512-jOTEnbuyNeRE4ljNJeFuqh5R0gV6vbQH/U29gO0+dy92x88TwNyPozckObgZhaB5UCYXTvrx64nLQWJGpfIwcQ==",
+      "dev": true,
+      "requires": {
+        "colorette": "^1.2.2",
+        "globby": "^11.0.4",
+        "mico-spinner": "^1.1.1",
+        "typescript": "^4.3.5",
+        "vfile-location": "^4.0.1"
+      }
+    },
     "chokidar": {
       "version": "3.5.2",
       "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
@@ -1510,6 +1561,15 @@
       "integrity": "sha512-ag6wfpBFyNXZ0p8pcuIDS//D8H062ZQJ3fzYxjpmeKjnz8W4pekL3AI8VohmyZmsWW2PWaHgjsmqR6L13101VQ==",
       "dev": true
     },
+    "dir-glob": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+      "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+      "dev": true,
+      "requires": {
+        "path-type": "^4.0.0"
+      }
+    },
     "doctrine": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
@@ -1973,6 +2033,19 @@
       "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
       "dev": true
     },
+    "fast-glob": {
+      "version": "3.2.7",
+      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz",
+      "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==",
+      "dev": true,
+      "requires": {
+        "@nodelib/fs.stat": "^2.0.2",
+        "@nodelib/fs.walk": "^1.2.3",
+        "glob-parent": "^5.1.2",
+        "merge2": "^1.3.0",
+        "micromatch": "^4.0.4"
+      }
+    },
     "fast-json-stable-stringify": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -1990,6 +2063,15 @@
       "resolved": "https://registry.npmjs.org/fast-stats/-/fast-stats-0.0.6.tgz",
       "integrity": "sha512-m0zkwa7Z07Wc4xm1YtcrCHmhzNxiYRrrfUyhkdhSZPzaAH/Ewbocdaq7EPVBFz19GWfIyyPcLfRHjHJYe83jlg=="
     },
+    "fastq": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.1.tgz",
+      "integrity": "sha512-HOnr8Mc60eNYl1gzwp6r5RoUyAn5/glBolUzP/Ez6IFVPMPirxn/9phgL6zhOtaTy7ISwPvQ+wT+hfcRZh/bzw==",
+      "dev": true,
+      "requires": {
+        "reusify": "^1.0.4"
+      }
+    },
     "fb-watchman": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz",
@@ -2150,6 +2232,28 @@
       "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
       "dev": true
     },
+    "globby": {
+      "version": "11.0.4",
+      "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz",
+      "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==",
+      "dev": true,
+      "requires": {
+        "array-union": "^2.1.0",
+        "dir-glob": "^3.0.1",
+        "fast-glob": "^3.1.1",
+        "ignore": "^5.1.4",
+        "merge2": "^1.3.0",
+        "slash": "^3.0.0"
+      },
+      "dependencies": {
+        "ignore": {
+          "version": "5.1.8",
+          "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
+          "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==",
+          "dev": true
+        }
+      }
+    },
     "graceful-fs": {
       "version": "4.2.4",
       "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz",
@@ -2300,6 +2404,12 @@
         "binary-extensions": "^2.0.0"
       }
     },
+    "is-buffer": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
+      "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==",
+      "dev": true
+    },
     "is-ci": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.0.tgz",
@@ -3244,6 +3354,21 @@
       "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
       "dev": true
     },
+    "merge2": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+      "dev": true
+    },
+    "mico-spinner": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/mico-spinner/-/mico-spinner-1.2.2.tgz",
+      "integrity": "sha512-jIsOIIk5xa+TmZJU2mPVTEk1AC7aKnrj8UJR58cTiK8msNWYvPuQxPx0ojNbtVOut/TjTgGkVLrEt1jsrceYfA==",
+      "dev": true,
+      "requires": {
+        "colorette": "^1.2.2"
+      }
+    },
     "micromatch": {
       "version": "4.0.4",
       "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
@@ -3653,6 +3778,12 @@
       "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
       "dev": true
     },
+    "path-type": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+      "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+      "dev": true
+    },
     "picomatch": {
       "version": "2.3.0",
       "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
@@ -3813,6 +3944,12 @@
       "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
       "dev": true
     },
+    "queue-microtask": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+      "dev": true
+    },
     "react-is": {
       "version": "17.0.2",
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
@@ -3886,6 +4023,12 @@
       "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
       "dev": true
     },
+    "reusify": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+      "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+      "dev": true
+    },
     "rimraf": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@@ -3895,6 +4038,15 @@
         "glob": "^7.1.3"
       }
     },
+    "run-parallel": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+      "dev": true,
+      "requires": {
+        "queue-microtask": "^1.2.2"
+      }
+    },
     "safe-buffer": {
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@@ -4296,6 +4448,21 @@
         "is-typedarray": "^1.0.0"
       }
     },
+    "typescript": {
+      "version": "4.3.5",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz",
+      "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==",
+      "dev": true
+    },
+    "unist-util-stringify-position": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.0.tgz",
+      "integrity": "sha512-SdfAl8fsDclywZpfMDTVDxA2V7LjtRDTOFd44wUJamgl6OlVngsqWjxvermMYf60elWHbxhuRCZml7AnuXCaSA==",
+      "dev": true,
+      "requires": {
+        "@types/unist": "^2.0.0"
+      }
+    },
     "universalify": {
       "version": "0.1.2",
       "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
@@ -4342,6 +4509,38 @@
         }
       }
     },
+    "vfile": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.0.2.tgz",
+      "integrity": "sha512-5cV+K7tX83MT3bievROc+7AvHv0GXDB0zqbrTjbOe+HRbkzvY4EP+wS3IR77kUBCoWFMdG9py18t0sesPtQ1Rw==",
+      "dev": true,
+      "requires": {
+        "@types/unist": "^2.0.0",
+        "is-buffer": "^2.0.0",
+        "unist-util-stringify-position": "^3.0.0",
+        "vfile-message": "^3.0.0"
+      }
+    },
+    "vfile-location": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-4.0.1.tgz",
+      "integrity": "sha512-JDxPlTbZrZCQXogGheBHjbRWjESSPEak770XwWPfw5mTc1v1nWGLB/apzZxsx8a0SJVfF8HK8ql8RD308vXRUw==",
+      "dev": true,
+      "requires": {
+        "@types/unist": "^2.0.0",
+        "vfile": "^5.0.0"
+      }
+    },
+    "vfile-message": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.0.1.tgz",
+      "integrity": "sha512-gYmSHcZZUEtYpTmaWaFJwsuUD70/rTY4v09COp8TGtOkix6gGxb/a8iTQByIY9ciTk9GwAwIXd/J9OPfM4Bvaw==",
+      "dev": true,
+      "requires": {
+        "@types/unist": "^2.0.0",
+        "unist-util-stringify-position": "^3.0.0"
+      }
+    },
     "w3c-hr-time": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
diff --git a/package.json b/package.json
index f58cbf1..f47363a 100644
--- a/package.json
+++ b/package.json
@@ -4,6 +4,7 @@
   "author": "Maciej Brencz <maciej.brencz@gmail.com> (https://github.com/macbre)",
   "description": "CSS selectors complexity and performance analyzer",
   "main": "./lib/index.js",
+  "types": "./lib/index.d.ts",
   "repository": {
     "type": "git",
     "url": "git://github.com/macbre/analyze-css.git"
@@ -36,6 +37,7 @@
   "devDependencies": {
     "autoprefixer": "^10.2.4",
     "browserslist": "^4.11.1",
+    "check-dts": "^0.5.5",
     "eslint": "^7.12.1",
     "eslint-config-prettier": "8.3.0",
     "eslint-plugin-node": "^11.1.0",
diff --git a/rules/bodySelectors.js b/rules/bodySelectors.js
index b091f78..a76bfc0 100644
--- a/rules/bodySelectors.js
+++ b/rules/bodySelectors.js
@@ -1,5 +1,8 @@
 "use strict";
 
+/**
+ * @param { import("../lib/index").CSSAnalyzer } analyzer
+ */
 function rule(analyzer) {
   analyzer.setMetric("redundantBodySelectors");
 
diff --git a/rules/childSelectors.js b/rules/childSelectors.js
index eec93cb..f0930f7 100644
--- a/rules/childSelectors.js
+++ b/rules/childSelectors.js
@@ -1,5 +1,8 @@
 "use strict";
 
+/**
+ * @param { import("../lib/index").CSSAnalyzer } analyzer
+ */
 function rule(analyzer) {
   // definition of redundant child nodes selectors (see #51 for the initial idea):
   // ul li
diff --git a/rules/colors.js b/rules/colors.js
index 836819e..162be90 100644
--- a/rules/colors.js
+++ b/rules/colors.js
@@ -15,6 +15,9 @@ function extractColors(value) {
   return matches || false;
 }
 
+/**
+ * @param { import("../lib/index").CSSAnalyzer } analyzer
+ */
 function rule(analyzer) {
   // store unique colors with the counter
   var colors = new collection();
diff --git a/rules/comments.js b/rules/comments.js
index 5648396..a1e2d90 100644
--- a/rules/comments.js
+++ b/rules/comments.js
@@ -1,8 +1,11 @@
 "use strict";
 
-var format = require("util").format,
+const format = require("util").format,
   MAX_LENGTH = 256;
 
+/**
+ * @param { import("../lib/index").CSSAnalyzer } analyzer
+ */
 function rule(analyzer) {
   analyzer.setMetric("comments");
   analyzer.setMetric("commentsLength");
diff --git a/rules/complex.js b/rules/complex.js
index c2448cb..a2d521e 100644
--- a/rules/complex.js
+++ b/rules/complex.js
@@ -2,6 +2,9 @@
 
 var COMPLEX_SELECTOR_THRESHOLD = 3;
 
+/**
+ * @param { import("../lib/index").CSSAnalyzer } analyzer
+ */
 function rule(analyzer) {
   analyzer.setMetric("complexSelectors");
 
diff --git a/rules/duplicated.js b/rules/duplicated.js
index 2a82739..e40674f 100644
--- a/rules/duplicated.js
+++ b/rules/duplicated.js
@@ -1,11 +1,14 @@
 "use strict";
 
-var collection = require("../lib/collection"),
+const Collection = require("../lib/collection"),
   debug = require("debug")("analyze-css:duplicated"),
   format = require("util").format;
 
+/**
+ * @param { import("../lib/index").CSSAnalyzer } analyzer
+ */
 function rule(analyzer) {
-  var selectors = new collection(),
+  var selectors = new Collection(),
     mediaQueryStack = [],
     browserPrefixRegEx = /^-(moz|o|webkit|ms)-/;
 
@@ -97,7 +100,7 @@ function rule(analyzer) {
   analyzer.on("report", function () {
     analyzer.setCurrentPosition(undefined);
 
-    selectors.sort().forEach(function (selector, cnt) {
+    selectors.sort().forEach((selector, cnt) => {
       if (cnt > 1) {
         analyzer.incrMetric("duplicatedSelectors");
         analyzer.addOffender(
diff --git a/rules/emptyRules.js b/rules/emptyRules.js
index ec371ad..5e98590 100644
--- a/rules/emptyRules.js
+++ b/rules/emptyRules.js
@@ -1,5 +1,8 @@
 "use strict";
 
+/**
+ * @param { import("../lib/index").CSSAnalyzer } analyzer
+ */
 function rule(analyzer) {
   analyzer.setMetric("emptyRules");
 
diff --git a/rules/expressions.js b/rules/expressions.js
index 510d765..1db1eff 100644
--- a/rules/expressions.js
+++ b/rules/expressions.js
@@ -2,6 +2,9 @@
 
 var format = require("util").format;
 
+/**
+ * @param { import("../lib/index").CSSAnalyzer } analyzer
+ */
 function rule(analyzer) {
   var re = /^expression/i;
 
diff --git a/rules/ieFixes.js b/rules/ieFixes.js
index aa9dfb3..13e7c92 100644
--- a/rules/ieFixes.js
+++ b/rules/ieFixes.js
@@ -6,6 +6,7 @@ var format = require("util").format;
  * Rules below match ugly fixes for IE9 and below
  *
  * @see http://browserhacks.com/
+ * @param { import("../lib/index").CSSAnalyzer } analyzer
  */
 function rule(analyzer) {
   var re = {
diff --git a/rules/import.js b/rules/import.js
index 3e20da5..0c37745 100644
--- a/rules/import.js
+++ b/rules/import.js
@@ -1,5 +1,8 @@
 "use strict";
 
+/**
+ * @param { import("../lib/index").CSSAnalyzer } analyzer
+ */
 function rule(analyzer) {
   analyzer.setMetric("imports");
 
diff --git a/rules/important.js b/rules/important.js
index 9a0dcaf..217c025 100644
--- a/rules/important.js
+++ b/rules/important.js
@@ -1,7 +1,10 @@
 "use strict";
 
-var format = require("util").format;
+const format = require("util").format;
 
+/**
+ * @param { import("../lib/index").CSSAnalyzer } analyzer
+ */
 function rule(analyzer) {
   analyzer.setMetric("importants");
 
diff --git a/rules/length.js b/rules/length.js
index 00bfc38..e849ec5 100644
--- a/rules/length.js
+++ b/rules/length.js
@@ -1,5 +1,8 @@
 "use strict";
 
+/**
+ * @param { import("../lib/index").CSSAnalyzer } analyzer
+ */
 function rule(analyzer) {
   analyzer.on("css", function (css) {
     analyzer.setMetric("length", css.length);
diff --git a/rules/mediaQueries.js b/rules/mediaQueries.js
index 5166250..6ff8ecb 100644
--- a/rules/mediaQueries.js
+++ b/rules/mediaQueries.js
@@ -2,6 +2,9 @@
 
 var format = require("util").format;
 
+/**
+ * @param { import("../lib/index").CSSAnalyzer } analyzer
+ */
 function rule(analyzer) {
   analyzer.setMetric("mediaQueries");
 
diff --git a/rules/minified.js b/rules/minified.js
index a5bf246..4620160 100644
--- a/rules/minified.js
+++ b/rules/minified.js
@@ -2,6 +2,8 @@
 
 /**
  * Detect not minified CSS
+ *
+ * @param { import("../lib/index").CSSAnalyzer } analyzer
  */
 function rule(analyzer) {
   analyzer.setMetric("notMinified");
diff --git a/rules/multiClassesSelectors.js b/rules/multiClassesSelectors.js
index 8245644..f67a44e 100644
--- a/rules/multiClassesSelectors.js
+++ b/rules/multiClassesSelectors.js
@@ -1,5 +1,8 @@
 "use strict";
 
+/**
+ * @param { import("../lib/index").CSSAnalyzer } analyzer
+ */
 function rule(analyzer) {
   analyzer.setMetric("multiClassesSelectors");
 
diff --git a/rules/parsingErrors.js b/rules/parsingErrors.js
index 0fcc300..b6e4e25 100644
--- a/rules/parsingErrors.js
+++ b/rules/parsingErrors.js
@@ -1,5 +1,8 @@
 "use strict";
 
+/**
+ * @param { import("../lib/index").CSSAnalyzer } analyzer
+ */
 function rule(analyzer) {
   analyzer.setMetric("parsingErrors");
 
diff --git a/rules/prefixes.js b/rules/prefixes.js
index f8b6125..a463af3 100644
--- a/rules/prefixes.js
+++ b/rules/prefixes.js
@@ -3,8 +3,11 @@
 var debug = require("debug")("analyze-css:prefixes"),
   format = require("util").format;
 
+/**
+ * @param { import("../lib/index").CSSAnalyzer } analyzer
+ */
 function rule(analyzer) {
-  var data = require("./prefixes.json"),
+  var data = require(__dirname + "/prefixes.json"),
     prefixes = data.prefixes;
 
   debug("Using data generated on %s", data.generated);
diff --git a/rules/propertyResets.js b/rules/propertyResets.js
index 1f866d5..98c2ce6 100644
--- a/rules/propertyResets.js
+++ b/rules/propertyResets.js
@@ -7,6 +7,7 @@ var format = require("util").format,
  * Detect accidental property resets
  *
  * @see http://css-tricks.com/accidental-css-resets/
+ * @param { import("../lib/index").CSSAnalyzer } analyzer
  */
 function rule(analyzer) {
   var debug = require("debug");
diff --git a/rules/qualified.js b/rules/qualified.js
index 221f75d..e3cb4c7 100644
--- a/rules/qualified.js
+++ b/rules/qualified.js
@@ -1,5 +1,8 @@
 "use strict";
 
+/**
+ * @param { import("../lib/index").CSSAnalyzer } analyzer
+ */
 function rule(analyzer) {
   analyzer.setMetric("qualifiedSelectors");
 
diff --git a/rules/specificity.js b/rules/specificity.js
index d844d15..4f2b014 100644
--- a/rules/specificity.js
+++ b/rules/specificity.js
@@ -4,6 +4,9 @@ var debug = require("debug")("analyze-css:specificity"),
   specificity = require("specificity"),
   stats = require("fast-stats").Stats;
 
+/**
+ * @param { import("../lib/index").CSSAnalyzer } analyzer
+ */
 function rule(analyzer) {
   var types = ["Id", "Class", "Tag"],
     values = [];
diff --git a/rules/stats.js b/rules/stats.js
index baa7835..7bbfbec 100644
--- a/rules/stats.js
+++ b/rules/stats.js
@@ -1,5 +1,8 @@
 "use strict";
 
+/**
+ * @param { import("../lib/index").CSSAnalyzer } analyzer
+ */
 function rule(analyzer) {
   var selectors = 0,
     selectorsLength = 0;


Test patch:
diff --git a/test/errors.test.js b/test/errors.test.js
index 381144d..9f99822 100644
--- a/test/errors.test.js
+++ b/test/errors.test.js
@@ -1,6 +1,6 @@
 const { describe, it } = require("@jest/globals");
 
-var analyzer = require('../'),
+var analyzer = require('../').analyze,
 	assert = require('assert'),
 	tests;
 
diff --git a/test/fixes/280-url-with-semicolons.test.js b/test/fixes/280-url-with-semicolons.test.js
index 87f4267..3f0a6ee 100644
--- a/test/fixes/280-url-with-semicolons.test.js
+++ b/test/fixes/280-url-with-semicolons.test.js
@@ -3,7 +3,7 @@ const assert = require('assert');
 
 describe('@import rule with url containing semicolon', () => {
 	it('is properly parsed', () => {
-        const analyzer = require('../..'),
+        const analyzer = require('../..').analyze,
             css = `
 @import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;800&display=swap');
 
diff --git a/test/opts.test.js b/test/opts.test.js
index abee8f8..46500b1 100644
--- a/test/opts.test.js
+++ b/test/opts.test.js
@@ -1,6 +1,6 @@
 const { describe, it } = require("@jest/globals");
 
-var analyzer = require('../'),
+var analyzer = require('../').analyze,
 	assert = require('assert'),
 	css = '.foo { color: white }';
 
diff --git a/test/rules.test.js b/test/rules.test.js
index 6921a99..7740eb6 100644
--- a/test/rules.test.js
+++ b/test/rules.test.js
@@ -1,6 +1,6 @@
 const { describe, it } = require("@jest/globals");
 
-const analyzer = require('../'),
+const analyzer = require('../').analyze,
 	assert = require('assert'),
 	glob = require('glob');
 
diff --git a/test/sass.test.js b/test/sass.test.js
index 8bfedd3..d1d8589 100644
--- a/test/sass.test.js
+++ b/test/sass.test.js
@@ -1,6 +1,6 @@
 const { describe, it } = require("@jest/globals");
 
-var analyzer = require('../'),
+var analyzer = require('../').analyze,
 	fs = require('fs'),
 	isSassInstalled = true,
 	assert = require('assert'),