From 7778471999c7e42236ce404229660d80ecc2acd6 Mon Sep 17 00:00:00 2001
From: Manuel <5673677+mtrezza@users.noreply.github.com>
Date: Tue, 9 Jul 2024 12:58:51 +0200
Subject: [PATCH] feat: Add Node 22 support (#9187)

---
 .github/workflows/ci.yml |  18 ++++---
 README.md                |  23 ++++-----
 package-lock.json        |  76 ++++++++++-------------------
 package.json             |   3 +-
 spec/CLI.spec.js         | 101 ++++++++++++++++++++-------------------
 src/vendor/mongodbUrl.js |   2 +-
 6 files changed, 103 insertions(+), 120 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index fe8ade4399..4737e37cf3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -143,32 +143,36 @@ jobs:
           - name: MongoDB 4.2, ReplicaSet
             MONGODB_VERSION: 4.2.25
             MONGODB_TOPOLOGY: replset
-            NODE_VERSION: 20.12.0
+            NODE_VERSION: 22
           - name: MongoDB 4.4, ReplicaSet
             MONGODB_VERSION: 4.4.29
             MONGODB_TOPOLOGY: replset
-            NODE_VERSION: 20.12.0
+            NODE_VERSION: 22
           - name: MongoDB 5, ReplicaSet
             MONGODB_VERSION: 5.0.26
             MONGODB_TOPOLOGY: replset
-            NODE_VERSION: 20.12.0
+            NODE_VERSION: 22
           - name: MongoDB 6, ReplicaSet
             MONGODB_VERSION: 6.0.14
             MONGODB_TOPOLOGY: replset
-            NODE_VERSION: 20.12.0
+            NODE_VERSION: 22
           - name: MongoDB 7, ReplicaSet
             MONGODB_VERSION: 7.0.8
             MONGODB_TOPOLOGY: replset
-            NODE_VERSION: 20.12.0
+            NODE_VERSION: 22
           - name: Redis Cache
             PARSE_SERVER_TEST_CACHE: redis
             MONGODB_VERSION: 7.0.8
             MONGODB_TOPOLOGY: standalone
-            NODE_VERSION: 20.12.0
+            NODE_VERSION: 22
           - name: Node 18
             MONGODB_VERSION: 7.0.8
             MONGODB_TOPOLOGY: standalone
-            NODE_VERSION: 18.20.0
+            NODE_VERSION: 18
+          - name: Node 20
+            MONGODB_VERSION: 7.0.8
+            MONGODB_TOPOLOGY: standalone
+            NODE_VERSION: 20
       fail-fast: false
     name: ${{ matrix.name }}
     timeout-minutes: 15
diff --git a/README.md b/README.md
index c44c620a8f..1b543edd61 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
 [![Coverage](https://codecov.io/github/parse-community/parse-server/branch/alpha/graph/badge.svg)](https://app.codecov.io/github/parse-community/parse-server/tree/alpha)
 [![auto-release](https://img.shields.io/badge/%F0%9F%9A%80-auto--release-9e34eb.svg)](https://github.com/parse-community/parse-dashboard/releases)
 
-[![Node Version](https://img.shields.io/badge/nodejs-18,_20-green.svg?logo=node.js&style=flat)](https://nodejs.org)
+[![Node Version](https://img.shields.io/badge/nodejs-18,_20,_22-green.svg?logo=node.js&style=flat)](https://nodejs.org)
 [![MongoDB Version](https://img.shields.io/badge/mongodb-4.2,_4.4,_5,_6,_7-green.svg?logo=mongodb&style=flat)](https://www.mongodb.com)
 [![Postgres Version](https://img.shields.io/badge/postgresql-13,_14,_15,_16-green.svg?logo=postgresql&style=flat)](https://www.postgresql.org)
 
@@ -127,22 +127,23 @@ Before you start make sure you have installed:
 
 Parse Server is continuously tested with the most recent releases of Node.js to ensure compatibility. We follow the [Node.js Long Term Support plan](https://github.com/nodejs/Release) and only test against versions that are officially supported and have not reached their end-of-life date.
 
-| Version    | Latest Version | End-of-Life | Compatible |
-|------------|----------------|-------------|------------|
-| Node.js 18 | 18.20.0        | April 2025  | ✅ Yes      |
-| Node.js 20 | 20.12.0        | April 2026  | ✅ Yes      |
+| Version    | End-of-Life | Compatible |
+|------------|-------------|------------|
+| Node.js 18 | April 2025  | ✅ Yes      |
+| Node.js 20 | April 2026  | ✅ Yes      |
+| Node.js 22 | April 2027  | ✅ Yes      |
 
 #### MongoDB
 
 Parse Server is continuously tested with the most recent releases of MongoDB to ensure compatibility. We follow the [MongoDB support schedule](https://www.mongodb.com/support-policy) and [MongoDB lifecycle schedule](https://www.mongodb.com/support-policy/lifecycles) and only test against versions that are officially supported and have not reached their end-of-life date. MongoDB "rapid releases" are ignored as these are considered pre-releases of the next major version.
 
 | Version     | Latest Version | End-of-Life   | Compatible |
-| ----------- | -------------- | ------------- | ---------- |
-| MongoDB 4.2 | 4.2.25         | April 2023    | ✅ Yes     |
-| MongoDB 4.4 | 4.4.29         | February 2024 | ✅ Yes     |
-| MongoDB 5   | 5.0.26         | October 2024  | ✅ Yes     |
-| MongoDB 6   | 6.0.14         | July 2025     | ✅ Yes     |
-| MongoDB 7   | 7.0.8          | TDB           | ✅ Yes     |
+|-------------|----------------|---------------|------------|
+| MongoDB 4.2 | 4.2.25         | April 2023    | ✅ Yes      |
+| MongoDB 4.4 | 4.4.29         | February 2024 | ✅ Yes      |
+| MongoDB 5   | 5.0.26         | October 2024  | ✅ Yes      |
+| MongoDB 6   | 6.0.14         | July 2025     | ✅ Yes      |
+| MongoDB 7   | 7.0.8          | TDB           | ✅ Yes      |
 
 #### PostgreSQL
 
diff --git a/package-lock.json b/package-lock.json
index 34c75aa4e0..0d760f241c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -45,6 +45,7 @@
         "pg-monitor": "2.0.0",
         "pg-promise": "11.7.8",
         "pluralize": "8.0.0",
+        "punycode": "2.3.1",
         "rate-limit-redis": "4.2.0",
         "redis": "4.6.13",
         "semver": "7.6.0",
@@ -102,7 +103,7 @@
         "yaml": "1.10.0"
       },
       "engines": {
-        "node": ">=18.0.0 <21"
+        "node": "18 || 19 || 20 || 22"
       },
       "funding": {
         "type": "opencollective",
@@ -13534,15 +13535,6 @@
         "webidl-conversions": "^3.0.0"
       }
     },
-    "node_modules/mongodb-runner/node_modules/punycode": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
-      "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
-      "dev": true,
-      "engines": {
-        "node": ">=6"
-      }
-    },
     "node_modules/mongodb-runner/node_modules/tr46": {
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz",
@@ -18026,9 +18018,13 @@
       }
     },
     "node_modules/punycode": {
-      "version": "1.4.1",
-      "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
-      "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ=="
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+      "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
     },
     "node_modules/qs": {
       "version": "6.11.0",
@@ -20426,6 +20422,11 @@
         "node": ">=0.8"
       }
     },
+    "node_modules/tough-cookie/node_modules/punycode": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+      "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ=="
+    },
     "node_modules/tr46": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
@@ -20437,14 +20438,6 @@
         "node": ">=12"
       }
     },
-    "node_modules/tr46/node_modules/punycode": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
-      "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
-      "engines": {
-        "node": ">=6"
-      }
-    },
     "node_modules/traverse": {
       "version": "0.6.7",
       "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.7.tgz",
@@ -20831,14 +20824,6 @@
         "punycode": "^2.1.0"
       }
     },
-    "node_modules/uri-js/node_modules/punycode": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
-      "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
-      "engines": {
-        "node": ">=6"
-      }
-    },
     "node_modules/url-join": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz",
@@ -31317,12 +31302,6 @@
             }
           }
         },
-        "punycode": {
-          "version": "2.3.1",
-          "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
-          "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
-          "dev": true
-        },
         "tr46": {
           "version": "4.1.1",
           "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz",
@@ -34465,9 +34444,9 @@
       }
     },
     "punycode": {
-      "version": "1.4.1",
-      "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
-      "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ=="
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+      "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="
     },
     "qs": {
       "version": "6.11.0",
@@ -36222,6 +36201,13 @@
       "requires": {
         "psl": "^1.1.24",
         "punycode": "^1.4.1"
+      },
+      "dependencies": {
+        "punycode": {
+          "version": "1.4.1",
+          "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+          "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ=="
+        }
       }
     },
     "tr46": {
@@ -36230,13 +36216,6 @@
       "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
       "requires": {
         "punycode": "^2.1.1"
-      },
-      "dependencies": {
-        "punycode": {
-          "version": "2.1.1",
-          "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
-          "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
-        }
       }
     },
     "traverse": {
@@ -36509,13 +36488,6 @@
       "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
       "requires": {
         "punycode": "^2.1.0"
-      },
-      "dependencies": {
-        "punycode": {
-          "version": "2.1.1",
-          "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
-          "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
-        }
       }
     },
     "url-join": {
diff --git a/package.json b/package.json
index bac0a958cb..7811240ac2 100644
--- a/package.json
+++ b/package.json
@@ -54,6 +54,7 @@
     "pg-monitor": "2.0.0",
     "pg-promise": "11.7.8",
     "pluralize": "8.0.0",
+    "punycode": "2.3.1",
     "rate-limit-redis": "4.2.0",
     "redis": "4.6.13",
     "semver": "7.6.0",
@@ -138,7 +139,7 @@
     "madge:circular": "node_modules/.bin/madge ./src --circular"
   },
   "engines": {
-    "node": ">=18.0.0 <21"
+    "node": "18 || 19 || 20 || 22"
   },
   "bin": {
     "parse-server": "bin/parse-server"
diff --git a/spec/CLI.spec.js b/spec/CLI.spec.js
index 9cec0e77b3..9c6a4775a5 100644
--- a/spec/CLI.spec.js
+++ b/spec/CLI.spec.js
@@ -207,6 +207,36 @@ describe('LiveQuery definitions', () => {
 describe('execution', () => {
   const binPath = path.resolve(__dirname, '../bin/parse-server');
   let childProcess;
+  let aggregatedData;
+
+  function handleStdout(childProcess, done, aggregatedData, requiredData) {
+    childProcess.stdout.on('data', data => {
+      data = data.toString();
+      aggregatedData.push(data);
+      if (requiredData.every(required => aggregatedData.some(aggregated => aggregated.includes(required)))) {
+        done();
+      }
+    });
+  }
+
+  function handleStderr(childProcess, done) {
+    childProcess.stderr.on('data', data => {
+      data = data.toString();
+      if (!data.includes('[DEP0040] DeprecationWarning')) {
+        done.fail(data);
+      }
+    });
+  }
+
+  function handleError(childProcess, done) {
+    childProcess.on('error', err => {
+      done.fail(err);
+    });
+  }
+
+  beforeEach(() => {
+    aggregatedData = [];
+  });
 
   afterEach(done => {
     if (childProcess) {
@@ -220,26 +250,20 @@ describe('execution', () => {
 
   it('should start Parse Server', done => {
     const env = { ...process.env };
-    env.NODE_OPTIONS = '--dns-result-order=ipv4first';
+    env.NODE_OPTIONS = '--dns-result-order=ipv4first --trace-deprecation';
     childProcess = spawn(
       binPath,
       ['--appId', 'test', '--masterKey', 'test', '--databaseURI', databaseURI, '--port', '1339'],
       { env }
     );
-    childProcess.stdout.on('data', data => {
-      data = data.toString();
-      if (data.includes('parse-server running on')) {
-        done();
-      }
-    });
-    childProcess.stderr.on('data', data => {
-      done.fail(data.toString());
-    });
+    handleStdout(childProcess, done, aggregatedData, ['parse-server running on']);
+    handleStderr(childProcess, done);
+    handleError(childProcess, done);
   });
 
   it('should start Parse Server with GraphQL', async done => {
     const env = { ...process.env };
-    env.NODE_OPTIONS = '--dns-result-order=ipv4first';
+    env.NODE_OPTIONS = '--dns-result-order=ipv4first --trace-deprecation';
     childProcess = spawn(
       binPath,
       [
@@ -255,23 +279,17 @@ describe('execution', () => {
       ],
       { env }
     );
-    let output = '';
-    childProcess.stdout.on('data', data => {
-      data = data.toString();
-      output += data;
-      if (data.includes('GraphQL running on')) {
-        expect(output).toMatch('parse-server running on');
-        done();
-      }
-    });
-    childProcess.stderr.on('data', data => {
-      done.fail(data.toString());
-    });
+    handleStdout(childProcess, done, aggregatedData, [
+      'parse-server running on',
+      'GraphQL running on',
+    ]);
+    handleStderr(childProcess, done);
+    handleError(childProcess, done);
   });
 
   it('should start Parse Server with GraphQL and Playground', async done => {
     const env = { ...process.env };
-    env.NODE_OPTIONS = '--dns-result-order=ipv4first';
+    env.NODE_OPTIONS = '--dns-result-order=ipv4first --trace-deprecation';
     childProcess = spawn(
       binPath,
       [
@@ -288,38 +306,25 @@ describe('execution', () => {
       ],
       { env }
     );
-    let output = '';
-    childProcess.stdout.on('data', data => {
-      data = data.toString();
-      output += data;
-      if (data.includes('Playground running on')) {
-        expect(output).toMatch('GraphQL running on');
-        expect(output).toMatch('parse-server running on');
-        done();
-      }
-    });
-    childProcess.stderr.on('data', data => {
-      done.fail(data.toString());
-    });
+    handleStdout(childProcess, done, aggregatedData, [
+      'parse-server running on',
+      'Playground running on',
+      'GraphQL running on',
+    ]);
+    handleStderr(childProcess, done);
+    handleError(childProcess, done);
   });
 
   it('can start Parse Server with auth via CLI', done => {
     const env = { ...process.env };
-    env.NODE_OPTIONS = '--dns-result-order=ipv4first';
+    env.NODE_OPTIONS = '--dns-result-order=ipv4first --trace-deprecation';
     childProcess = spawn(
       binPath,
       ['--databaseURI', databaseURI, './spec/configs/CLIConfigAuth.json'],
       { env }
     );
-    childProcess.stdout.on('data', data => {
-      data = data.toString();
-      if (data.includes('parse-server running on')) {
-        done();
-      }
-    });
-    childProcess.stderr.on('data', data => {
-      data = data.toString();
-      done.fail(data.toString());
-    });
+    handleStdout(childProcess, done, aggregatedData, ['parse-server running on']);
+    handleStderr(childProcess, done);
+    handleError(childProcess, done);
   });
 });
diff --git a/src/vendor/mongodbUrl.js b/src/vendor/mongodbUrl.js
index 2aa422a9cd..becab5745f 100644
--- a/src/vendor/mongodbUrl.js
+++ b/src/vendor/mongodbUrl.js
@@ -5,7 +5,7 @@
 
 'use strict';
 
-const punycode = require('punycode');
+import punycode from 'punycode/punycode.js';
 
 exports.parse = urlParse;
 exports.resolve = urlResolve;