« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--package-lock.json279
-rw-r--r--package.json1
-rwxr-xr-xsrc/upd8.js30
-rw-r--r--test/data-tests/index.js125
-rw-r--r--test/data-tests/test-no-short-tracks.js25
-rw-r--r--test/data-tests/test-order-of-album-groups.js55
6 files changed, 502 insertions, 13 deletions
diff --git a/package-lock.json b/package-lock.json
index e12b9c7e..65e8bcac 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,6 +18,7 @@
                 "hsmusic": "src/upd8.js"
             },
             "devDependencies": {
+                "chokidar": "^3.5.3",
                 "eslint": "^8.18.0",
                 "tape": "^5.4.1"
             }
@@ -123,6 +124,19 @@
                 "url": "https://github.com/chalk/ansi-styles?sponsor=1"
             }
         },
+        "node_modules/anymatch": {
+            "version": "3.1.3",
+            "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+            "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+            "dev": true,
+            "dependencies": {
+                "normalize-path": "^3.0.0",
+                "picomatch": "^2.0.4"
+            },
+            "engines": {
+                "node": ">= 8"
+            }
+        },
         "node_modules/argparse": {
             "version": "2.0.1",
             "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -164,6 +178,15 @@
             "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
             "dev": true
         },
+        "node_modules/binary-extensions": {
+            "version": "2.2.0",
+            "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+            "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+            "dev": true,
+            "engines": {
+                "node": ">=8"
+            }
+        },
         "node_modules/brace-expansion": {
             "version": "1.1.11",
             "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -174,6 +197,18 @@
                 "concat-map": "0.0.1"
             }
         },
+        "node_modules/braces": {
+            "version": "3.0.2",
+            "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+            "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+            "dev": true,
+            "dependencies": {
+                "fill-range": "^7.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
         "node_modules/call-bind": {
             "version": "1.0.2",
             "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
@@ -212,6 +247,45 @@
                 "url": "https://github.com/chalk/chalk?sponsor=1"
             }
         },
+        "node_modules/chokidar": {
+            "version": "3.5.3",
+            "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+            "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+            "dev": true,
+            "funding": [
+                {
+                    "type": "individual",
+                    "url": "https://paulmillr.com/funding/"
+                }
+            ],
+            "dependencies": {
+                "anymatch": "~3.1.2",
+                "braces": "~3.0.2",
+                "glob-parent": "~5.1.2",
+                "is-binary-path": "~2.1.0",
+                "is-glob": "~4.0.1",
+                "normalize-path": "~3.0.0",
+                "readdirp": "~3.6.0"
+            },
+            "engines": {
+                "node": ">= 8.10.0"
+            },
+            "optionalDependencies": {
+                "fsevents": "~2.3.2"
+            }
+        },
+        "node_modules/chokidar/node_modules/glob-parent": {
+            "version": "5.1.2",
+            "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+            "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+            "dev": true,
+            "dependencies": {
+                "is-glob": "^4.0.1"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
         "node_modules/chroma-js": {
             "version": "2.4.2",
             "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz",
@@ -620,6 +694,18 @@
                 "node": "^10.12.0 || >=12.0.0"
             }
         },
+        "node_modules/fill-range": {
+            "version": "7.0.1",
+            "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+            "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+            "dev": true,
+            "dependencies": {
+                "to-regex-range": "^5.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
         "node_modules/flat-cache": {
             "version": "3.0.4",
             "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
@@ -660,6 +746,20 @@
             "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
             "dev": true
         },
+        "node_modules/fsevents": {
+            "version": "2.3.2",
+            "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+            "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+            "dev": true,
+            "hasInstallScript": true,
+            "optional": true,
+            "os": [
+                "darwin"
+            ],
+            "engines": {
+                "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+            }
+        },
         "node_modules/function-bind": {
             "version": "1.1.1",
             "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@@ -928,6 +1028,18 @@
                 "url": "https://github.com/sponsors/ljharb"
             }
         },
+        "node_modules/is-binary-path": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+            "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+            "dev": true,
+            "dependencies": {
+                "binary-extensions": "^2.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
         "node_modules/is-boolean-object": {
             "version": "1.1.2",
             "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
@@ -1025,6 +1137,15 @@
                 "url": "https://github.com/sponsors/ljharb"
             }
         },
+        "node_modules/is-number": {
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+            "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+            "dev": true,
+            "engines": {
+                "node": ">=0.12.0"
+            }
+        },
         "node_modules/is-number-object": {
             "version": "1.0.6",
             "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz",
@@ -1241,6 +1362,15 @@
             "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
             "dev": true
         },
+        "node_modules/normalize-path": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+            "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+            "dev": true,
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
         "node_modules/object-inspect": {
             "version": "1.12.0",
             "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz",
@@ -1355,6 +1485,18 @@
             "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
             "dev": true
         },
+        "node_modules/picomatch": {
+            "version": "2.3.1",
+            "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+            "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+            "dev": true,
+            "engines": {
+                "node": ">=8.6"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/jonschlinkert"
+            }
+        },
         "node_modules/prelude-ls": {
             "version": "1.2.1",
             "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -1373,6 +1515,18 @@
                 "node": ">=6"
             }
         },
+        "node_modules/readdirp": {
+            "version": "3.6.0",
+            "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+            "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+            "dev": true,
+            "dependencies": {
+                "picomatch": "^2.2.1"
+            },
+            "engines": {
+                "node": ">=8.10.0"
+            }
+        },
         "node_modules/regexp.prototype.flags": {
             "version": "1.4.1",
             "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.1.tgz",
@@ -1605,6 +1759,18 @@
             "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
             "dev": true
         },
+        "node_modules/to-regex-range": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+            "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+            "dev": true,
+            "dependencies": {
+                "is-number": "^7.0.0"
+            },
+            "engines": {
+                "node": ">=8.0"
+            }
+        },
         "node_modules/type-check": {
             "version": "0.4.0",
             "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -1816,6 +1982,16 @@
                 "color-convert": "^2.0.1"
             }
         },
+        "anymatch": {
+            "version": "3.1.3",
+            "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+            "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+            "dev": true,
+            "requires": {
+                "normalize-path": "^3.0.0",
+                "picomatch": "^2.0.4"
+            }
+        },
         "argparse": {
             "version": "2.0.1",
             "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -1845,6 +2021,12 @@
             "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
             "dev": true
         },
+        "binary-extensions": {
+            "version": "2.2.0",
+            "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+            "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+            "dev": true
+        },
         "brace-expansion": {
             "version": "1.1.11",
             "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -1855,6 +2037,15 @@
                 "concat-map": "0.0.1"
             }
         },
+        "braces": {
+            "version": "3.0.2",
+            "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+            "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+            "dev": true,
+            "requires": {
+                "fill-range": "^7.0.1"
+            }
+        },
         "call-bind": {
             "version": "1.0.2",
             "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
@@ -1881,6 +2072,33 @@
                 "supports-color": "^7.1.0"
             }
         },
+        "chokidar": {
+            "version": "3.5.3",
+            "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+            "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+            "dev": true,
+            "requires": {
+                "anymatch": "~3.1.2",
+                "braces": "~3.0.2",
+                "fsevents": "~2.3.2",
+                "glob-parent": "~5.1.2",
+                "is-binary-path": "~2.1.0",
+                "is-glob": "~4.0.1",
+                "normalize-path": "~3.0.0",
+                "readdirp": "~3.6.0"
+            },
+            "dependencies": {
+                "glob-parent": {
+                    "version": "5.1.2",
+                    "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+                    "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+                    "dev": true,
+                    "requires": {
+                        "is-glob": "^4.0.1"
+                    }
+                }
+            }
+        },
         "chroma-js": {
             "version": "2.4.2",
             "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz",
@@ -2199,6 +2417,15 @@
                 "flat-cache": "^3.0.4"
             }
         },
+        "fill-range": {
+            "version": "7.0.1",
+            "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+            "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+            "dev": true,
+            "requires": {
+                "to-regex-range": "^5.0.1"
+            }
+        },
         "flat-cache": {
             "version": "3.0.4",
             "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
@@ -2236,6 +2463,13 @@
             "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
             "dev": true
         },
+        "fsevents": {
+            "version": "2.3.2",
+            "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+            "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+            "dev": true,
+            "optional": true
+        },
         "function-bind": {
             "version": "1.1.1",
             "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@@ -2426,6 +2660,15 @@
                 "has-bigints": "^1.0.1"
             }
         },
+        "is-binary-path": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+            "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+            "dev": true,
+            "requires": {
+                "binary-extensions": "^2.0.0"
+            }
+        },
         "is-boolean-object": {
             "version": "1.1.2",
             "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
@@ -2487,6 +2730,12 @@
             "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==",
             "dev": true
         },
+        "is-number": {
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+            "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+            "dev": true
+        },
         "is-number-object": {
             "version": "1.0.6",
             "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz",
@@ -2649,6 +2898,12 @@
             "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
             "dev": true
         },
+        "normalize-path": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+            "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+            "dev": true
+        },
         "object-inspect": {
             "version": "1.12.0",
             "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz",
@@ -2733,6 +2988,12 @@
             "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
             "dev": true
         },
+        "picomatch": {
+            "version": "2.3.1",
+            "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+            "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+            "dev": true
+        },
         "prelude-ls": {
             "version": "1.2.1",
             "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -2745,6 +3006,15 @@
             "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
             "dev": true
         },
+        "readdirp": {
+            "version": "3.6.0",
+            "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+            "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+            "dev": true,
+            "requires": {
+                "picomatch": "^2.2.1"
+            }
+        },
         "regexp.prototype.flags": {
             "version": "1.4.1",
             "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.1.tgz",
@@ -2917,6 +3187,15 @@
             "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
             "dev": true
         },
+        "to-regex-range": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+            "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+            "dev": true,
+            "requires": {
+                "is-number": "^7.0.0"
+            }
+        },
         "type-check": {
             "version": "0.4.0",
             "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
diff --git a/package.json b/package.json
index 052aae22..c9f0c1d7 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
     },
     "license": "GPL-3.0",
     "devDependencies": {
+        "chokidar": "^3.5.3",
         "eslint": "^8.18.0",
         "tape": "^5.4.1"
     }
diff --git a/src/upd8.js b/src/upd8.js
index 217c985d..70297c6c 100755
--- a/src/upd8.js
+++ b/src/upd8.js
@@ -60,6 +60,7 @@ import find, {bindFind} from './util/find.js';
 import * as html from './util/html.js';
 import {getColors} from './util/colors.js';
 import {findFiles} from './util/io.js';
+import {isMain} from './util/node-utils.js';
 
 import CacheableObject from './data/things/cacheable-object.js';
 
@@ -1599,7 +1600,8 @@ function generateRedirectPage(title, target, {language}) {
   ]);
 }
 
-async function processLanguageFile(file) {
+// TODO: define somewhere besides upd8.js obviously
+export async function processLanguageFile(file) {
   const contents = await readFile(file, 'utf-8');
   const json = JSON.parse(contents);
 
@@ -2608,15 +2610,17 @@ async function main() {
   logInfo`Written!`;
 }
 
-main()
-  .catch((error) => {
-    if (error instanceof AggregateError) {
-      showAggregate(error);
-    } else {
-      console.error(error);
-    }
-  })
-  .then(() => {
-    decorateTime.displayTime();
-    CacheableObject.showInvalidAccesses();
-  });
+if (isMain(import.meta.url)) {
+  main()
+    .catch((error) => {
+      if (error instanceof AggregateError) {
+        showAggregate(error);
+      } else {
+        console.error(error);
+      }
+    })
+    .then(() => {
+      decorateTime.displayTime();
+      CacheableObject.showInvalidAccesses();
+    });
+}
diff --git a/test/data-tests/index.js b/test/data-tests/index.js
new file mode 100644
index 00000000..1b9ec990
--- /dev/null
+++ b/test/data-tests/index.js
@@ -0,0 +1,125 @@
+import chokidar from 'chokidar';
+import * as path from 'path';
+import {fileURLToPath} from 'url';
+
+import {quickLoadAllFromYAML} from '../../src/data/yaml.js';
+import {isMain} from '../../src/util/node-utils.js';
+import {getContextAssignments} from '../../src/repl.js';
+
+import {
+  color,
+  logError,
+  logInfo,
+  logWarn,
+  parseOptions,
+} from '../../src/util/cli.js';
+
+import {
+  bindOpts,
+  showAggregate,
+} from '../../src/util/sugar.js';
+
+async function main() {
+  const miscOptions = await parseOptions(process.argv.slice(2), {
+    'data-path': {
+      type: 'value',
+    },
+  });
+
+  const dataPath = miscOptions['data-path'] || process.env.HSMUSIC_DATA;
+
+  if (!dataPath) {
+    logError`Expected --data-path option or HSMUSIC_DATA to be set`;
+    return;
+  }
+
+  console.log(`HSMusic automated data tests`);
+  console.log(`${color.bright(color.yellow(`:star:`))} Now featuring quick-reloading! ${color.bright(color.cyan(`:earth:`))}`);
+
+  // Watch adjacent files in data-tests directory
+  const metaPath = fileURLToPath(import.meta.url);
+  const metaDirname = path.dirname(metaPath);
+  const watcher = chokidar.watch(metaDirname);
+
+  const wikiData = await quickLoadAllFromYAML(dataPath, {
+    showAggregate: bindOpts(showAggregate, {
+      showTraces: false,
+    }),
+  });
+
+  const context = await getContextAssignments({
+    wikiData,
+  });
+
+  let resolveNext;
+
+  const queue = [];
+
+  watcher.on('all', (event, path) => {
+    if (!['add', 'change'].includes(event)) return;
+    if (path === metaPath) return;
+    if (resolveNext) {
+      resolveNext(path);
+    } else if (!queue.includes(path)) {
+      queue.push(path);
+    }
+  });
+
+  logInfo`Awaiting file changes.`;
+
+  /* eslint-disable-next-line no-constant-condition */
+  while (true) {
+    const testPath = (queue.length
+      ? queue.shift()
+      : await new Promise(resolve => {
+          resolveNext = resolve;
+        }));
+
+    resolveNext = null;
+
+    const shortPath = path.basename(testPath);
+
+    logInfo`Path updated: ${shortPath} - running this test!`;
+
+    let imp;
+    try {
+      imp = await import(`${testPath}?${Date.now()}`)
+    } catch (error) {
+      logWarn`Failed to import ${shortPath} - ${error.constructor.name} details below:`;
+      console.error(error);
+      continue;
+    }
+
+    const {default: testFn} = imp;
+
+    if (!testFn) {
+      logWarn`No default export for ${shortPath}`;
+      logWarn`Skipping this test for now!`;
+      continue;
+    }
+
+    if (typeof testFn !== 'function') {
+      logWarn`Default export for ${shortPath} is ${typeof testFn}, not function`;
+      logWarn`Skipping this test for now!`;
+      continue;
+    }
+
+    try {
+      await testFn(context);
+    } catch (error) {
+      showAggregate(error, {
+        pathToFileURL: f => path.relative(metaDirname, fileURLToPath(f)),
+      });
+    }
+  }
+}
+
+if (isMain(import.meta.url)) {
+  main().catch((error) => {
+    if (error instanceof AggregateError) {
+      showAggregate(error);
+    } else {
+      console.error(error);
+    }
+  });
+}
diff --git a/test/data-tests/test-no-short-tracks.js b/test/data-tests/test-no-short-tracks.js
new file mode 100644
index 00000000..76356099
--- /dev/null
+++ b/test/data-tests/test-no-short-tracks.js
@@ -0,0 +1,25 @@
+export default function({
+  albumData,
+  getTotalDuration,
+}) {
+  const shortAlbums = albumData
+    .filter(album => album.tracks.length > 1)
+    .map(album => ({
+      album,
+      duration: getTotalDuration(album.tracks),
+    }))
+    .filter(album => album.duration)
+    .filter(album => album.duration < 60 * 15);
+
+  if (!shortAlbums.length) return true;
+
+  shortAlbums.sort((a, b) => a.duration - b.duration);
+
+  console.log(`Found ${shortAlbums.length} short albums! Oh nooooooo!`);
+  console.log(`Here are the shortest 10:`);
+  for (const {duration, album} of shortAlbums.slice(0, 10)) {
+    console.log(`- (${duration}s)`, album);
+  }
+
+  return false;
+}
diff --git a/test/data-tests/test-order-of-album-groups.js b/test/data-tests/test-order-of-album-groups.js
new file mode 100644
index 00000000..de2fcbed
--- /dev/null
+++ b/test/data-tests/test-order-of-album-groups.js
@@ -0,0 +1,55 @@
+import * as util from 'util';
+
+export default function({
+  albumData,
+  groupCategoryData,
+}) {
+  const groupSchemaTemplate = [
+    ['Projects beyond Homestuck', 'Fandom projects'],
+    ['Solo musicians', 'Fan-musician groups'],
+    ['HSMusic'],
+  ];
+
+  const groupSchema =
+    groupSchemaTemplate.map(names => names.flatMap(
+      name => groupCategoryData
+        .find(gc => gc.name === name)
+        .groups));
+
+  const badAlbums = albumData.filter(album => {
+    const groups = album.groups.slice();
+    const disallowed = [];
+    for (const allowed of groupSchema) {
+      while (groups.length) {
+        if (disallowed.includes(groups[0]))
+          return true;
+        else if (allowed.includes(groups[0]))
+          groups.shift();
+        else break;
+      }
+      disallowed.push(...allowed);
+    }
+    return false;
+  });
+
+  if (!badAlbums.length) return true;
+
+  console.log(`Some albums don't list their groups in the right order:`);
+  for (const album of badAlbums) {
+    console.log('-', album);
+    for (const group of album.groups) {
+      console.log(`  - ${util.inspect(group)}`)
+    }
+  }
+
+  console.log(`Here's the group schema they should be updated to match:`);
+  for (const section of groupSchemaTemplate) {
+    if (section.length > 1) {
+      console.log(`- Groups from any of: ${section.join(', ')}`);
+    } else {
+      console.log(`- Groups from: ${section}`);
+    }
+  }
+
+  return false;
+}