diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..0fd21d3
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,3 @@
+/node_modules
+/coverage
+/.nyc_output
diff --git a/.eslintrc b/.eslintrc
deleted file mode 100644
index 4739f5f..0000000
--- a/.eslintrc
+++ /dev/null
@@ -1,16 +0,0 @@
-{
-    "rules": {
-        "curly": [2, "multi-line"],
-        "eol-last": [2],
-        "indent": [2, 4],
-        "quotes": [2, "single"],
-        "linebreak-style": [2, "unix"],
-        "max-len": [2, 80, 4],
-        "semi": [2, "always"],
-        "strict": [2, "never"]
-    },
-    "env": {
-        "node": true
-    },
-    "extends": "eslint:recommended"
-}
diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 0000000..36ff570
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,3 @@
+module.exports = {
+    extends: ['scratch', 'scratch/node']
+};
diff --git a/.gitignore b/.gitignore
index db98532..2170d08 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@
 # NPM
 /node_modules
 npm-*
+package-lock.json
 
 # Testing
 /.nyc_output
diff --git a/.travis.yml b/.travis.yml
index 6276265..2144f49 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,7 +6,7 @@ cache:
 notifications:
   email: false
 node_js:
-  - '4'
+  - 'node'
 before_install:
   - npm i -g npm@^2.0.0
 before_script:
diff --git a/Makefile b/Makefile
deleted file mode 100644
index 794e43e..0000000
--- a/Makefile
+++ /dev/null
@@ -1,25 +0,0 @@
-ESLINT=./node_modules/.bin/eslint
-NODE=node
-TAP=./node_modules/.bin/tap
-
-# ------------------------------------------------------------------------------
-
-lint:
-	$(ESLINT) ./*.js
-	$(ESLINT) ./bin/*.js
-	$(ESLINT) ./lib/*.js
-	$(ESLINT) ./test/**/*.js
-
-test:
-	@make lint
-	$(TAP) ./test/{unit,integration}/*.js
-
-coverage:
-	$(TAP) ./test/{unit,integration}/*.js --coverage --coverage-report=lcov
-
-benchmark:
-	$(NODE) ./test/benchmark/performance.js
-
-# ------------------------------------------------------------------------------
-
-.PHONY: lint test coverage benchmark
diff --git a/index.js b/index.js
index e93ea85..4c9a32f 100644
--- a/index.js
+++ b/index.js
@@ -8,10 +8,8 @@ var analyze = require('./lib/analyze');
 /**
  * Unpacks, parses, validates, and analyzes Scratch projects. If successful,
  * will return a valid Scratch project object with appended metadata.
- *
- * @param {Buffer | string} Input buffer or string representing scratch project
- *
- * @return {Object}
+ * @param {Buffer | string} input    Buffer or string representing project
+ * @param {Function}        callback Returns error or project data
  */
 module.exports = function (input, callback) {
     async.waterfall([
diff --git a/lib/parse.js b/lib/parse.js
index 162bce2..1aab493 100644
--- a/lib/parse.js
+++ b/lib/parse.js
@@ -3,14 +3,14 @@
  * will be expanded greatly in the future in order to support the Scratch 1.4
  * file format. For now, this is nothing but an (awkward) async wrapper around
  * the `JSON.parse` function.
- *
- * @param {String} Input
- *
- * @return {Object}
+ * @param {string}   input    Stringified JSON object
+ * @param {Function} callback Returns error or parsed JSON object
+ * @return {void}
  */
 module.exports = function (input, callback) {
     try {
-        callback(null, JSON.parse(input));
+        var result = JSON.parse(input);
+        callback(null, result);
     } catch (e) {
         return callback(e.toString());
     }
diff --git a/lib/schema.json b/lib/schema.json
index 0db1eed..b8b49c9 100644
--- a/lib/schema.json
+++ b/lib/schema.json
@@ -1,5 +1,5 @@
 {
-    "id": "https://scratch.mit.edu",
+    "$id": "https://scratch.mit.edu",
     "description": "Scratch project schema",
     "type": "object",
     "properties": {
diff --git a/lib/unpack.js b/lib/unpack.js
index b4d4746..17350ae 100644
--- a/lib/unpack.js
+++ b/lib/unpack.js
@@ -4,10 +4,9 @@ var JSZip = require('jszip');
  * If input a buffer, transforms buffer into a UTF-8 string.
  * If input is encoded in ZIP format, the input will be extracted and decoded.
  * If input is a string, passes that string along to the given callback.
- *
- * @param {Buffer | string} Input
- *
- * @return {String}
+ * @param {Buffer | string} input    Project data
+ * @param {Function}        callback Error or stringified project data
+ * @return {void}
  */
 module.exports = function (input, callback) {
     if (typeof input === 'string') {
diff --git a/package.json b/package.json
index 15e1fda..4e7b834 100644
--- a/package.json
+++ b/package.json
@@ -10,24 +10,31 @@
     "url": "https://github.com/LLK/scratch-parser.git"
   },
   "scripts": {
-    "test": "make test",
+    "test:lint": "eslint . --ext=js",
+    "test:unit": "tap test/unit/*.js",
+    "test:integration": "tap test/unit/*.js",
+    "test:coverage": "tap test/{unit,integration}/*.js --coverage --coverage-report=lcov",
+    "test:benchmark": "node test/benchmark/performance.js",
+    "test": "npm run test:lint && npm run test:unit && npm run test:integration",
     "semantic-release": "semantic-release pre && npm publish && semantic-release post"
   },
   "dependencies": {
-    "ajv": "4.7.0",
-    "async": "2.0.1",
+    "ajv": "6.3.0",
+    "async": "2.6.0",
     "jszip": "3.1.5"
   },
   "devDependencies": {
+    "babel-eslint": "8.2.2",
     "benchmark": "^2.1.1",
-    "cz-conventional-changelog": "2.0.0",
-    "eslint": "^3.5.0",
+    "cz-conventional-changelog": "^2.1.0",
+    "eslint": "^4.19.1",
+    "eslint-config-scratch": "5.0.0",
     "glob": "^7.0.6",
-    "tap": "^7.1.2",
-    "semantic-release": "^6.3.2"
+    "semantic-release": "^15.1.4",
+    "tap": "^11.1.3"
   },
   "engines": {
-    "node": ">=4.0"
+    "node": ">=8.0"
   },
   "config": {
     "commitizen": {
diff --git a/test/fixtures/meta.json b/test/fixtures/meta.json
index 85eb502..2f59799 100644
--- a/test/fixtures/meta.json
+++ b/test/fixtures/meta.json
@@ -1,5 +1,5 @@
 {
-    "id": "http://json-schema.org/draft-04/schema#",
+    "$id": "http://json-schema.org/draft-04/schema#",
     "$schema": "http://json-schema.org/draft-04/schema#",
     "description": "Core schema meta-schema",
     "definitions": {
diff --git a/test/unit/parser.js b/test/unit/parser.js
index 75cc58f..d07eeab 100644
--- a/test/unit/parser.js
+++ b/test/unit/parser.js
@@ -20,7 +20,7 @@ test('valid', function (t) {
 test('invalid', function (t) {
     parse('&%@', function (err, res) {
         t.type(err, 'string');
-        t.equal(res, undefined);
+        t.type(res, 'undefined');
         t.end();
     });
 });
diff --git a/test/unit/unpack.js b/test/unit/unpack.js
index 6475bcf..5b5c365 100644
--- a/test/unit/unpack.js
+++ b/test/unit/unpack.js
@@ -75,9 +75,10 @@ test('invalid string', function (t) {
 });
 
 test('undefined', function (t) {
-    unpack(undefined, function (err, res) {
+    var foo;
+    unpack(foo, function (err, res) {
         t.type(err, 'string');
-        t.equal(res, undefined);
+        t.type(res, 'undefined');
         t.end();
     });
 });
@@ -85,7 +86,7 @@ test('undefined', function (t) {
 test('null', function (t) {
     unpack(null, function (err, obj) {
         t.type(err, 'string');
-        t.equal(obj, undefined);
+        t.type(obj, 'undefined');
         t.end();
     });
 });
@@ -93,7 +94,7 @@ test('null', function (t) {
 test('object', function (t) {
     unpack({}, function (err, obj) {
         t.type(err, 'string');
-        t.equal(obj, undefined);
+        t.type(obj, 'undefined');
         t.end();
     });
 });
diff --git a/test/unit/validate.js b/test/unit/validate.js
index b39823b..4d92575 100644
--- a/test/unit/validate.js
+++ b/test/unit/validate.js
@@ -16,9 +16,9 @@ test('valid', function (t) {
 });
 
 test('invalid', function (t) {
-    validate({foo:1}, function (err, res) {
+    validate({foo: 1}, function (err, res) {
         t.equal(Array.isArray(err), true);
-        t.equal(res, undefined);
+        t.type(res, 'undefined');
         t.type(err[0], 'object');
         t.type(err[0].keyword, 'string');
         t.type(err[0].dataPath, 'string');