diff --git a/.github/helpers/create-npm-dep-report-issues.js b/.github/helpers/create-npm-dep-report-issues.js
new file mode 100644
index 0000000000..caa9b6cf60
--- /dev/null
+++ b/.github/helpers/create-npm-dep-report-issues.js
@@ -0,0 +1,22 @@
+const path = require("path");
+const createAndCloseExistingIssue = require("./github-api/create-and-close-existing-issue");
+const outputText = require(path.resolve(__dirname, `../../outputText.json`));
+
+/**
+ * THIS FILE DOES NOT REQUIRE ANY EDITING.
+ * Place within .github/helpers/
+ */
+
+// Get package.json paths from env.
+const packageJsonPaths = JSON.parse(process.env.packageJsonPaths);
+
+(async () => {
+ // Create an array of promises for each packageJsonPath.
+ const promises = packageJsonPaths.map(async (packagePath) => {
+ // Await the completion of create and close existing issue.
+ await createAndCloseExistingIssue(packagePath, outputText[packagePath]);
+ });
+
+ // Wait for all issues to be created.
+ await Promise.all(promises);
+})();
diff --git a/.github/helpers/create-npm-dep-report.js b/.github/helpers/create-npm-dep-report.js
new file mode 100644
index 0000000000..5891661208
--- /dev/null
+++ b/.github/helpers/create-npm-dep-report.js
@@ -0,0 +1,261 @@
+const path = require("path");
+const outdatedDeps = require(path.resolve(
+ __dirname,
+ `../../outdatedDeps.json`
+));
+
+const LOCAL_TEST = false;
+const TEST_PACKAGEJSON_PATHS = ["src/frontend", "src/backend"];
+
+/**
+ * THIS FILE DOES NOT REQUIRE ANY EDITING.
+ * Place within .github/helpers/
+ *
+ * To test this file locally,
+ * - Generate output from parse-npm-deps.js
+ * - Set LOCAL_TEST variable to true.
+ * - Edit TEST_PACKAGEJSON_PATHS if necessary.
+ * - From root, run "node .github/helpers/create-npm-dep-report > outputText.json"
+ * - Check the outputText.json file, then delete it.
+ */
+
+// Get package.json paths from env.
+const packageJsonPaths = LOCAL_TEST
+ ? TEST_PACKAGEJSON_PATHS
+ : JSON.parse(process.env.packageJsonPaths);
+
+// Save results to json.
+let results = {};
+
+// Emojis.
+const check = "✔️";
+const attention = "⚠️";
+
+// Badge color codes (checked for WCAG standards).
+const red = "701807"; // White text.
+const orange = "9e3302"; // White text.
+const yellow = "f5c60c"; // Black text.
+const green = "0B6018"; // White text.
+const blue = "0859A1"; // White text.
+
+// GitHub Markdown Formatting.
+const heading = (text, size) => `${"#".repeat(size)} ${text}\n`;
+const codeBlock = (text, language) => `\`\`\`${language}\n${text}\n\`\`\`\n\n`;
+const lineBreak = () => `\n
\n`;
+const line = (text) => `${text}\n`;
+
+// Formatted date.
+const getFormattedDate = () => {
+ const date = new Date();
+
+ // Get day of the month.
+ const day = date.getDate();
+
+ // Determine the ordinal suffix.
+ const ordinal = (day) => {
+ const s = ["th", "st", "nd", "rd"];
+ const v = day % 100;
+ return day + (s[(v - 20) % 10] || s[v] || s[0]);
+ };
+
+ // Formatter for the rest of the date.
+ const formatter = new Intl.DateTimeFormat("en-US", {
+ weekday: "long",
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ });
+
+ // Format the date and replace the day number with ordinal.
+ return formatter.format(date).replace(/\d+/, ordinal(day));
+};
+
+// Messages.
+const title = `NPM Dependency Report - ${getFormattedDate()}`;
+const subTitle =
+ "Versions of npm packages have been checked against their latest versions from the npm registry.";
+const upToDateMsg = "dependencies are all up-to-date.";
+const outOfDateMsg = "dependencies are out-of-date.";
+
+// Calculate percentage of packages up to date.
+const calculateUpToDatePercentage = (total, outdated) => {
+ if (total === 0) return 0;
+
+ const upToDatePackages = total - outdated;
+ const percentage = (upToDatePackages / total) * 100;
+ return Math.round(percentage);
+};
+
+// Output a command to install all dependencies in array.
+const outputMultiPackageInstallCmd = (dependencies, packagePath, isDevDep) => {
+ let installCmd = `npm install${isDevDep ? " -D" : ""} `;
+ installCmd += dependencies
+ .map((obj) => `${obj.dependency}@${obj.latestVersion}`)
+ .join(" ");
+
+ results[packagePath] += `${codeBlock(installCmd, "")}\n`;
+};
+
+// Output Dependencies in an array.
+const outputDepsByVersionChange = (
+ dependencies,
+ versionChange,
+ packagePath,
+ isDevDep
+) => {
+ const headerTag = isDevDep ? `${versionChange}_dev` : `${versionChange}`;
+ const badgeColor =
+ versionChange === "major"
+ ? orange
+ : versionChange === "minor"
+ ? blue
+ : green;
+
+ // Output header.
+ results[packagePath] += `${line(`![${headerTag}]`)}\n\n`;
+
+ // Output start of spoiler.
+ results[packagePath] += `${line(``)}\n`;
+ results[packagePath] += `${line(``)}`;
+ results[packagePath] += `${line(
+ `Expand to see individual installs.
\n`
+ )}`;
+
+ // Output a command to install all dependencies in array.
+ outputMultiPackageInstallCmd(dependencies, packagePath, isDevDep);
+
+ // Output end of spoiler summary.
+ results[packagePath] += `${line(`
\n`)}`;
+
+ // List dependency updates.
+ for (const key in dependencies) {
+ const { dependency, version, latestVersion } = dependencies[key];
+
+ results[packagePath] += `${line(
+ `- [ ] \`${dependency}\` Update from version \`${version}\` to \`${latestVersion}\` by running...`
+ )}`;
+ results[packagePath] += `${codeBlock(
+ `npm install${isDevDep ? " -D" : ""} ${dependency}@${latestVersion}`,
+ ""
+ )}`;
+ }
+
+ // Output end of spoiler.
+ results[packagePath] += `${line(` \n`)}`;
+
+ // Add Header text
+ results[packagePath] += `${line(
+ `[${headerTag}]: https://img.shields.io/badge/${versionChange}_updates_(${dependencies.length})-${badgeColor}?style=for-the-badge \n`
+ )}`;
+
+ results[packagePath] += `${lineBreak()}\n`;
+};
+
+// Output dependencies that need updating.
+const outputDeps = (dependenciesObj, packagePath, isDevDep) => {
+ // Return if no dependencies to update.
+ if (dependenciesObj.outdated <= 0) return;
+
+ // Output title.
+ results[packagePath] += `${lineBreak()}\n`;
+ if (isDevDep)
+ results[packagePath] += `${heading(
+ "Development Dependencies to Update:",
+ 3
+ )}`;
+ else
+ results[packagePath] += `${heading(
+ "Production Dependencies to Update:",
+ 3
+ )}`;
+
+ // Output MAJOR depedencies to update.
+ const major = dependenciesObj.major;
+ if (major.length > 0)
+ outputDepsByVersionChange(major, "major", packagePath, isDevDep);
+
+ // Output MINOR depedencies to update.
+ const minor = dependenciesObj.minor;
+ if (minor.length > 0)
+ outputDepsByVersionChange(minor, "minor", packagePath, isDevDep);
+
+ // Output PATCH depedencies to update.
+ const patch = dependenciesObj.patch;
+ if (patch.length > 0)
+ outputDepsByVersionChange(patch, "patch", packagePath, isDevDep);
+};
+
+// Escape special characters for GitHub Actions.
+const escapeForGitHubActions = (str) =>
+ str.replace(/%/g, "%25").replace(/\n/g, "%0A").replace(/\r/g, "%0D");
+
+(async () => {
+ // Create an array of promises for each packageJsonPath.
+ const promises = packageJsonPaths.map(async (packagePath) => {
+ results[packagePath] = "";
+
+ // Read the outdatedDeps file and get dependencies and devDependencies.
+ const deps = outdatedDeps[packagePath].deps ?? {};
+ const devDeps = outdatedDeps[packagePath].devDeps ?? {};
+
+ // Output title.
+ results[packagePath] += `${heading(title, 2)}`;
+ results[packagePath] += `${line(subTitle)}\n`;
+
+ // Get percentage of packages up to date.
+ const percentageUpToDate = calculateUpToDatePercentage(
+ deps.total + devDeps.total,
+ deps.outdated + devDeps.outdated
+ );
+
+ let percentageColor = green;
+ if (percentageUpToDate <= 50) percentageColor = red;
+ else if (percentageUpToDate <= 70) percentageColor = orange;
+ else if (percentageUpToDate <= 90) percentageColor = yellow;
+
+ // Output percentage.
+ results[packagePath] += `${line("![COVERAGE_PERCENTAGE]\n")}`;
+ results[packagePath] += `${line(
+ `\n[COVERAGE_PERCENTAGE]: https://img.shields.io/badge/percentage_of_dependencies_up_to_date-${percentageUpToDate}-${percentageColor}?style=for-the-badge \n\n`
+ )}`;
+
+ // Output summary.
+ if (deps.outdated === 0)
+ results[packagePath] += `${line(`${check} - Production ${upToDateMsg}`)}`;
+ else
+ results[packagePath] += `${line(
+ `${attention} - ${deps.outdated} Production ${outOfDateMsg}`
+ )}`;
+
+ if (devDeps.outdated === 0)
+ results[packagePath] += `${line(
+ `${check} - Development ${upToDateMsg}`
+ )}`;
+ else
+ results[packagePath] += `${line(
+ `${attention} - ${devDeps.outdated} Development ${outOfDateMsg}`
+ )}`;
+
+ // Output reminder.
+ if (deps.outdated > 0 || devDeps.outdated > 0) {
+ results[packagePath] += `${line(
+ `\n**Make sure to change directory to where the package.json is located using...**`
+ )}`;
+ results[packagePath] += `${codeBlock(`cd ${packagePath}`, "")}`;
+ }
+
+ // Await the completion of output for both dependencies and devDependencies.
+ await Promise.all([
+ outputDeps(deps, packagePath, false),
+ outputDeps(devDeps, packagePath, true),
+ ]);
+
+ results[packagePath] = escapeForGitHubActions(results[packagePath]);
+ });
+
+ // Wait for all outputs to complete.
+ await Promise.all(promises);
+
+ // Once all promises are resolved, log the results.
+ console.log(JSON.stringify(results, null, 2));
+})();
\ No newline at end of file
diff --git a/.github/helpers/parse-json5-config.js b/.github/helpers/parse-json5-config.js
new file mode 100644
index 0000000000..ac97f174fe
--- /dev/null
+++ b/.github/helpers/parse-json5-config.js
@@ -0,0 +1,72 @@
+const fs = require("fs");
+const json5 = require("json5");
+
+/**
+ * THIS FILE DOES NOT REQUIRE ANY EDITING.
+ * Place within .github/helpers/
+ */
+
+// Check if a file path is provided.
+if (process.argv.length < 3) {
+ console.log("Usage: node parse-json5-config ");
+ process.exit(1);
+}
+const filePath = process.argv[2];
+
+/**
+ * Read a Json5 file and parse out its values to a github workflow as output vars.
+ *
+ * Usage in GitHub Workflow:
+ *
+ * jobs:
+ * # Parse Output Vars from config.
+ * parse-json5-config:
+ * runs-on: ubuntu-22.04
+ * outputs:
+ varFromConfig: ${{ steps.parse_config.outputs.varFromConfig }}
+ *
+ * steps:
+ * # Checkout branch.
+ * - name: Checkout Repository
+ * uses: actions/checkout@v4
+ *
+ * # Install json5 npm package for parsing config.
+ * - name: Install Dependencies
+ * run: npm install json5
+ *
+ * # Run script to convert json5 config to output vars.
+ * - name: Run Script
+ * id: parse_config
+ * run: node .github/helpers/parse-json5-config
+ *
+ * # Another job...
+ * another-job:
+ * runs-on: ubuntu-22.04
+ * needs: parse-json5-config
+ * env:
+ * varFromConfig: ${{ needs.parse-json5-config.outputs.varFromConfig }}
+ *
+ */
+fs.readFile(filePath, "utf8", (err, data) => {
+ if (err) {
+ console.error("Error reading the file:", err);
+ return;
+ }
+
+ try {
+ // Parse the JSON5 data
+ const jsonData = json5.parse(data);
+
+ // Set each key-value pair as an environment variable
+ for (const [key, value] of Object.entries(jsonData)) {
+ // Serialize arrays and objects to JSON strings
+ const envValue =
+ typeof value === "object" ? JSON.stringify(value) : value;
+
+ // Output each key-value pair for GitHub Actions
+ console.log(`::set-output name=${key}::${envValue}`);
+ }
+ } catch (parseError) {
+ console.error("Error parsing JSON5:", parseError);
+ }
+});
\ No newline at end of file
diff --git a/.github/helpers/parse-npm-deps.js b/.github/helpers/parse-npm-deps.js
new file mode 100644
index 0000000000..8f59e53570
--- /dev/null
+++ b/.github/helpers/parse-npm-deps.js
@@ -0,0 +1,190 @@
+const https = require("https");
+const path = require("path");
+
+const LOCAL_TEST = false;
+const TEST_PACKAGEJSON_PATHS = ["src/frontend", "src/backend"];
+const TEST_IGNORE_PACKAGES = {
+ "src/frontend": [],
+ "src/backend": [],
+};
+
+/**
+ * THIS FILE DOES NOT REQUIRE ANY EDITING.
+ * Place within .github/helpers/
+ *
+ * To test this file locally,
+ * - Set LOCAL_TEST variable to true.
+ * - Edit TEST_PACKAGEJSON_PATHS and TEST_IGNORE_PACKAGES if necessary.
+ * - From root, run "node .github/helpers/parse-npm-deps > outdatedDeps.json"
+ * - Check the outdatedDeps.json file, then delete it.
+ */
+
+// Get package.json paths from env.
+const packageJsonPaths = LOCAL_TEST
+ ? TEST_PACKAGEJSON_PATHS
+ : JSON.parse(process.env.packageJsonPaths);
+
+// Ignore packages from env.
+const ignorePackages = LOCAL_TEST
+ ? TEST_IGNORE_PACKAGES
+ : JSON.parse(process.env.ignorePackages);
+
+// Save results to json.
+let results = {};
+
+// Save information on the dependency including latestVersion to the above results arrays.
+const saveDependencyResults = (
+ packagePath,
+ isDevDep,
+ dependency,
+ version,
+ latestVersion
+) => {
+ // Create arrays of version triplets ie. 2.7.4 >> [2, 7, 4]
+ const versionTriplet = version.split(".");
+ const latestVersionTriplet = latestVersion.split(".");
+
+ // Determine version change.
+ let versionChange = "patch";
+ if (versionTriplet[0] !== latestVersionTriplet[0]) versionChange = "major";
+ else if (versionTriplet[1] !== latestVersionTriplet[1])
+ versionChange = "minor";
+
+ // Save results.
+ const saveInfo = {
+ dependency,
+ version,
+ latestVersion,
+ };
+
+ if (isDevDep)
+ results[packagePath].devDeps[versionChange].push(saveInfo); // devDep.
+ else results[packagePath].deps[versionChange].push(saveInfo); // dep.
+};
+
+// Check the latest version of each dependency.
+const checkVersions = async (dependencyList, packagePath, isDevDep) => {
+ // For each dependency in the dependencyList.
+ for (let key in dependencyList) {
+ const [dependency, version] = dependencyList[key];
+ const url = `https://registry.npmjs.org/${dependency}/latest`;
+
+ // Skip dependency if in ignorePackages array.
+ if (
+ ignorePackages.hasOwnProperty(packagePath) &&
+ ignorePackages[packagePath].includes(dependency)
+ )
+ continue;
+
+ // Add to total.
+ if (isDevDep) ++results[packagePath].devDeps.total;
+ else ++results[packagePath].deps.total;
+
+ try {
+ // Make an http request to the npm registry.
+ const data = await new Promise((resolve, reject) => {
+ https.get(url, (res) => {
+ let data = "";
+ res.on("data", (chunk) => {
+ data += chunk;
+ });
+ res.on("end", () => {
+ resolve(data);
+ });
+ res.on("error", (error) => {
+ reject(error);
+ });
+ });
+ });
+
+ // Parse response data for latest version.
+ const latestVersion = JSON.parse(data).version;
+
+ // Check if theres a difference in version and latestVersion.
+ if (
+ latestVersion &&
+ latestVersion !== "0.0.0" &&
+ latestVersion !== version
+ ) {
+ if (!latestVersion.includes("-"))
+ saveDependencyResults(
+ packagePath,
+ isDevDep,
+ dependency,
+ version,
+ latestVersion
+ );
+ else {
+ // Latest version includes '-'.
+ const data = await new Promise((resolve, reject) => {
+ https.get(`https://registry.npmjs.org/${dependency}`, (res) => {
+ let data = "";
+ res.on("data", (chunk) => {
+ data += chunk;
+ });
+ res.on("end", () => {
+ resolve(data);
+ });
+ res.on("error", (error) => {
+ reject(error);
+ });
+ });
+ });
+
+ const versions = Object.keys(JSON.parse(data).versions);
+ // Remove all versions containing '-' and select the last item in the array.
+ const filteredLatestVersions = versions.filter(
+ (item) => !item.includes("-")
+ );
+ const latestVersion =
+ filteredLatestVersions[filteredLatestVersions.length - 1];
+
+ if (latestVersion !== version)
+ saveDependencyResults(
+ packagePath,
+ isDevDep,
+ dependency,
+ version,
+ latestVersion
+ );
+ }
+
+ // Add to outdated sum.
+ if (isDevDep) ++results[packagePath].devDeps.outdated;
+ else ++results[packagePath].deps.outdated;
+ }
+ } catch (error) {
+ console.error(`Error checking ${dependency}: ${error.message}`);
+ }
+ }
+};
+
+(async () => {
+ // Create an array of promises for each packageJsonPath.
+ const promises = packageJsonPaths.map(async (packagePath) => {
+ const packageJson = require(path.resolve(
+ __dirname,
+ `../../${packagePath}/package.json`
+ ));
+ results[packagePath] = {
+ deps: { total: 0, outdated: 0, major: [], minor: [], patch: [] },
+ devDeps: { total: 0, outdated: 0, major: [], minor: [], patch: [] },
+ };
+
+ // Read the package.json file and get the list of dependencies and devDependencies.
+ const deps = Object.entries(packageJson.dependencies) ?? [];
+ const devDeps = Object.entries(packageJson.devDependencies) ?? [];
+
+ // Await the completion of version checks for both dependencies and devDependencies.
+ await Promise.all([
+ checkVersions(deps, packagePath, false),
+ checkVersions(devDeps, packagePath, true),
+ ]);
+ });
+
+ // Wait for all package checks to complete.
+ await Promise.all(promises);
+
+ // Once all promises are resolved, log the results.
+ console.log(JSON.stringify(results, null, 2));
+})();
\ No newline at end of file