diff --git a/backend/jest.config.js b/backend/jest.config.js index ff65705e..9e0853f7 100644 --- a/backend/jest.config.js +++ b/backend/jest.config.js @@ -13,5 +13,9 @@ module.exports = { testMatch: [ '**/__tests__/**/*.test.js' ], - setupFilesAfterEnv: ['/jest.setup.js'] + setupFilesAfterEnv: ['/jest.setup.js'], + // sanitize-html's htmlparser2 12 is ESM-only; see jest.sanitizeHtml.js. + moduleNameMapper: { + '^sanitize-html$': '/jest.sanitizeHtml.js' + } }; diff --git a/backend/jest.sanitizeHtml.js b/backend/jest.sanitizeHtml.js new file mode 100644 index 00000000..65766b3a --- /dev/null +++ b/backend/jest.sanitizeHtml.js @@ -0,0 +1,18 @@ +/** + * sanitize-html 2.17.6+ depends on htmlparser2 12, which ships ESM only. + * Node 22.12+ loads it fine through require(esm); Jest 29's CommonJS module + * registry cannot evaluate an ESM file and fails every suite that imports a + * route or service using the sanitiser. Rather than bolting a Babel + * transform onto node_modules for one dependency, hand this single module to + * Node's own loader. + * + * process.getBuiltinModule (Node 22.3+) is the real core `module` even inside + * Jest — a plain require('module') here returns Jest's wrapper, whose + * createRequire() hands back an empty object for this package. createRequire() + * on the real one resolves from backend/node_modules exactly like production. + * + * Wired in via moduleNameMapper in jest.config.js. The module is stateless, + * so sharing one instance across test files changes nothing; it just cannot + * be jest.mock()ed, and nothing mocks it. + */ +module.exports = process.getBuiltinModule('module').createRequire(__filename)('sanitize-html'); diff --git a/backend/package-lock.json b/backend/package-lock.json index 2d8faaf3..df35fc8b 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -52,7 +52,7 @@ "postcss": "8.5.23", "qrcode": "^1.5.4", "react-i18next": "^15.6.0", - "sanitize-html": "2.17.5", + "sanitize-html": "2.17.7", "sharp": "0.35.3", "sqlite3": "^5.1.6", "swagger-jsdoc": "^6.2.8", @@ -71,7 +71,7 @@ "supertest": "^6.3.3" }, "engines": { - "node": "^20.19.0 || >=22" + "node": ">=22.12.0" } }, "node_modules/@apidevtools/json-schema-ref-parser": { @@ -11050,18 +11050,122 @@ "license": "MIT" }, "node_modules/sanitize-html": { - "version": "2.17.5", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.5.tgz", - "integrity": "sha512-ZmU1joGRrvoyctKIiuwUxqR6moLoU2Wk+2bMccN6f7UwhAmwYDvWziqPxRDDN2Qip62NqnIrVrT9akbL6Wretg==", + "version": "2.17.7", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.7.tgz", + "integrity": "sha512-PGtEkc9cbnedU3s9TmzDbpsZ8w086g/0Q8k8/oIO1NLNU3i5k9yn835CrjJSajp1KMmkisbO1qPXxNKO3welAg==", "license": "MIT", "dependencies": { "deepmerge": "^4.2.2", "escape-string-regexp": "^4.0.0", - "htmlparser2": "^10.1.0", + "htmlparser2": "^12.0.0", "is-plain-object": "^5.0.0", "launder": "^1.7.1", "parse-srcset": "^1.0.2", "postcss": "^8.3.11" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/sanitize-html/node_modules/dom-serializer": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-3.1.1.tgz", + "integrity": "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==", + "license": "MIT", + "dependencies": { + "domelementtype": "^3.0.0", + "domhandler": "^6.0.0", + "entities": "^8.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/sanitize-html/node_modules/domelementtype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-3.0.0.tgz", + "integrity": "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/sanitize-html/node_modules/domhandler": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-6.0.1.tgz", + "integrity": "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^3.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/sanitize-html/node_modules/domutils": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-4.0.2.tgz", + "integrity": "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^3.0.0", + "domelementtype": "^3.0.0", + "domhandler": "^6.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/sanitize-html/node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/sanitize-html/node_modules/htmlparser2": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-12.0.0.tgz", + "integrity": "sha512-Tz7u1i95/g2x2jz81+x0FBVhBhY5aRTvD3tXXdFaljuNdzDLJ8UGNRrTcj2cgQvAg3iW/h77Fz15nLW0L0CrZw==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^3.0.0", + "domhandler": "^6.0.0", + "domutils": "^4.0.2", + "entities": "^8.0.0" + }, + "engines": { + "node": ">=20.19.0" } }, "node_modules/selderee": { diff --git a/backend/package.json b/backend/package.json index 80f46dbe..c1349e73 100644 --- a/backend/package.json +++ b/backend/package.json @@ -4,7 +4,7 @@ "description": "Backend for PicPeak event photo sharing platform", "main": "server.js", "engines": { - "node": "^20.19.0 || >=22" + "node": ">=22.12.0" }, "scripts": { "start": "node server.js", @@ -62,7 +62,7 @@ "postcss": "8.5.23", "qrcode": "^1.5.4", "react-i18next": "^15.6.0", - "sanitize-html": "2.17.5", + "sanitize-html": "2.17.7", "sharp": "0.35.3", "sqlite3": "^5.1.6", "swagger-jsdoc": "^6.2.8", diff --git a/scripts/picpeak-setup.sh b/scripts/picpeak-setup.sh index b7e64e5b..93bbfff4 100755 --- a/scripts/picpeak-setup.sh +++ b/scripts/picpeak-setup.sh @@ -14,8 +14,8 @@ IFS=$'\n\t' readonly SCRIPT_VERSION="2.1.0" readonly APP_NAME="PicPeak" readonly REPO_URL="https://github.com/PicPeak/picpeak.git" -readonly NODE_VERSION="20" -readonly NODE_MIN_VERSION="20.19.0" # backend engines: ^20.19.0 || >=22 (sharp 0.35, html-to-text 10) +readonly NODE_VERSION="22" +readonly NODE_MIN_VERSION="22.12.0" # backend engines: >=22.12.0 (sanitize-html 2.17.7) readonly MIN_RAM_DOCKER=2048 readonly MIN_RAM_NATIVE=1024 readonly MIN_DISK_GB=2 @@ -822,6 +822,13 @@ EOF # Native Installation ################################################################################ +# True when a Node.js version satisfies the backend's engines range (>=22.12.0). +node_version_supported() { + local ver="$1" + [[ "$ver" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || return 1 + [[ "$(printf '%s\n' "$NODE_MIN_VERSION" "$ver" | sort -V | head -1)" == "$NODE_MIN_VERSION" ]] +} + install_nodejs() { # --update dispatches here before main() runs detect_os, so detect on demand if [[ -z "$PACKAGE_MANAGER" ]]; then @@ -830,8 +837,8 @@ install_nodejs() { local node_ver node_ver=$(command_exists node && node -v | cut -d'v' -f2 || echo "0") - # backend engines range is ^20.19.0 || >=22 (Node 21 is excluded by the glob/minimatch family) - if [[ "$(printf '%s\n' "$NODE_MIN_VERSION" "$node_ver" | sort -V | head -1)" == "$NODE_MIN_VERSION" && "${node_ver%%.*}" != "21" ]]; then + # Match sanitize-html's declared Node minimum, including strict npm installs. + if node_version_supported "$node_ver"; then log_success "Node.js $(node -v) is already installed" return fi @@ -849,10 +856,10 @@ install_nodejs() { ;; esac - # Package managers won't downgrade a newer Node (e.g. 21), so re-verify before continuing + # Re-verify in case the package manager did not replace an unsupported Node. node_ver=$(command_exists node && node -v | cut -d'v' -f2 || echo "0") - if [[ "$(printf '%s\n' "$NODE_MIN_VERSION" "$node_ver" | sort -V | head -1)" != "$NODE_MIN_VERSION" || "${node_ver%%.*}" == "21" ]]; then - die "Node.js v$node_ver does not satisfy the backend requirement (^$NODE_MIN_VERSION || >=22); remove the current Node.js, install a supported version, then re-run this script" + if ! node_version_supported "$node_ver"; then + die "Node.js v$node_ver does not satisfy the backend requirement (>=$NODE_MIN_VERSION); remove the current Node.js, install a supported version, then re-run this script" fi log_success "Node.js installed: $(node -v)" }