From 5ea70b3c1946b05a287fe3b7bbc6e87a01b7d17f Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Fri, 6 Mar 2026 00:28:12 +0100 Subject: [PATCH 1/4] refactor: replace Error.prepareStackTrace with string stack parsing in logger The previous approach set Error.prepareStackTrace after creating the Error object, which is fragile and relies on lazy evaluation of err.stack. Replace with direct string parsing of new Error().stack using a regex, which is simpler and more robust across Node.js versions. --- js/logger.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/js/logger.js b/js/logger.js index a31b22a0ad..14ca84f060 100644 --- a/js/logger.js +++ b/js/logger.js @@ -9,14 +9,13 @@ format: ":date(yyyy-mm-dd HH:MM:ss.l) :label(7) :pre() :msg", tokens: { pre: () => { - const err = new Error(); - Error.prepareStackTrace = (_, stack) => stack; - const stack = err.stack; - Error.prepareStackTrace = undefined; try { - for (const line of stack) { - const file = line.getFileName(); - if (file && !file.includes("node:") && !file.includes("js/logger.js") && !file.includes("node_modules")) { + const lines = new Error().stack.split("\n"); + for (const line of lines) { + if (line.includes("node:") || line.includes("js/logger.js") || line.includes("node_modules")) continue; + const match = line.match(/\((.+?\.js):\d+:\d+\)/) || line.match(/at\s+(.+?\.js):\d+:\d+/); + if (match) { + const file = match[1]; const filename = file.replace(/.*\/(.*).js/, "$1"); const filepath = file.replace(/.*\/(.*)\/.*.js/, "$1"); if (filepath === "js") { From 0e7f415f916136877b42c8f71eabf60886514350 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Fri, 6 Mar 2026 00:28:25 +0100 Subject: [PATCH 2/4] fix: use "gray" instead of "grey" in styleText for Node.js v25 compat Node.js v25 removed "grey" as a valid color name for styleText(). Only "gray" is accepted. This caused styleText() to throw inside the pre token callback, which console-stamp silently caught and returned the raw ":pre()" format string as the prefix. Fixes #4048 --- js/logger.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/js/logger.js b/js/logger.js index 14ca84f060..e2ff982f17 100644 --- a/js/logger.js +++ b/js/logger.js @@ -19,14 +19,14 @@ const filename = file.replace(/.*\/(.*).js/, "$1"); const filepath = file.replace(/.*\/(.*)\/.*.js/, "$1"); if (filepath === "js") { - return styleText("grey", `[${filename}]`); + return styleText("gray", `[${filename}]`); } else { - return styleText("grey", `[${filepath}]`); + return styleText("gray", `[${filepath}]`); } } } } catch (err) { - return styleText("grey", "[unknown]"); + return styleText("gray", "[unknown]"); } }, label: (arg) => { From 9f30fcf496dd1f755bfa6724d1c3dd8cc04c3834 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Fri, 6 Mar 2026 00:39:36 +0100 Subject: [PATCH 3/4] refactor: simplify logger by removing UMD pattern The UMD (Universal Module Definition) wrapper was unnecessary overhead. The logger only ever needs to run in two environments: Node.js (CJS) and the browser (global). Replace with a plain IIFE that checks typeof module directly. Also removes the unused 'config' parameter that was passed through the UMD factory but never referenced inside it. --- js/logger.js | 117 ++++++++++++++++++++++++++------------------------- 1 file changed, 60 insertions(+), 57 deletions(-) diff --git a/js/logger.js b/js/logger.js index e2ff982f17..5c1ce8fcfc 100644 --- a/js/logger.js +++ b/js/logger.js @@ -1,6 +1,6 @@ // This logger is very simple, but needs to be extended. -(function (root, factory) { - if (typeof exports === "object") { +(function () { + if (typeof module !== "undefined") { if (process.env.mmTestMode !== "true") { const { styleText } = require("node:util"); @@ -67,64 +67,67 @@ } }); } - // Node, CommonJS-like - module.exports = factory(root.config); + // Node, CommonJS + module.exports = makeLogger(); } else { - // Browser globals (root is window) - root.Log = factory(root.config); - } -}(this, function (config) { - let logLevel; - let enableLog; - if (typeof exports === "object") { - // in nodejs and not running in test mode - enableLog = process.env.mmTestMode !== "true"; - } else { - // in browser and not running with jsdom - enableLog = typeof window === "object" && window.name !== "jsdom"; + // Browser globals + window.Log = makeLogger(); } - if (enableLog) { - logLevel = { - debug: Function.prototype.bind.call(console.debug, console), - log: Function.prototype.bind.call(console.log, console), - info: Function.prototype.bind.call(console.info, console), - warn: Function.prototype.bind.call(console.warn, console), - error: Function.prototype.bind.call(console.error, console), - group: Function.prototype.bind.call(console.group, console), - groupCollapsed: Function.prototype.bind.call(console.groupCollapsed, console), - groupEnd: Function.prototype.bind.call(console.groupEnd, console), - time: Function.prototype.bind.call(console.time, console), - timeEnd: Function.prototype.bind.call(console.timeEnd, console), - timeStamp: console.timeStamp ? Function.prototype.bind.call(console.timeStamp, console) : function () {} - }; + /** + * Creates the logger object. Logging is disabled when running in test mode + * (Node.js) or inside jsdom (browser). + * @returns {object} The logger object with log level methods. + */ + function makeLogger () { + const enableLog = typeof module !== "undefined" + ? process.env.mmTestMode !== "true" + : typeof window === "object" && window.name !== "jsdom"; - logLevel.setLogLevel = function (newLevel) { - if (newLevel) { - Object.keys(logLevel).forEach(function (key) { - if (!newLevel.includes(key.toLocaleUpperCase())) { - logLevel[key] = function () {}; - } - }); - } - }; - } else { - logLevel = { - debug () {}, - log () {}, - info () {}, - warn () {}, - error () {}, - group () {}, - groupCollapsed () {}, - groupEnd () {}, - time () {}, - timeEnd () {}, - timeStamp () {} - }; + let logLevel; - logLevel.setLogLevel = function () {}; - } + if (enableLog) { + logLevel = { + debug: Function.prototype.bind.call(console.debug, console), + log: Function.prototype.bind.call(console.log, console), + info: Function.prototype.bind.call(console.info, console), + warn: Function.prototype.bind.call(console.warn, console), + error: Function.prototype.bind.call(console.error, console), + group: Function.prototype.bind.call(console.group, console), + groupCollapsed: Function.prototype.bind.call(console.groupCollapsed, console), + groupEnd: Function.prototype.bind.call(console.groupEnd, console), + time: Function.prototype.bind.call(console.time, console), + timeEnd: Function.prototype.bind.call(console.timeEnd, console), + timeStamp: console.timeStamp ? Function.prototype.bind.call(console.timeStamp, console) : function () {} + }; - return logLevel; -})); + logLevel.setLogLevel = function (newLevel) { + if (newLevel) { + Object.keys(logLevel).forEach(function (key) { + if (!newLevel.includes(key.toLocaleUpperCase())) { + logLevel[key] = function () {}; + } + }); + } + }; + } else { + logLevel = { + debug () {}, + log () {}, + info () {}, + warn () {}, + error () {}, + group () {}, + groupCollapsed () {}, + groupEnd () {}, + time () {}, + timeEnd () {}, + timeStamp () {} + }; + + logLevel.setLogLevel = function () {}; + } + + return logLevel; + } +}()); From c1b2bd92e1fc5d5a5fea7c95fa4f6200944e16c9 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Fri, 6 Mar 2026 01:08:51 +0100 Subject: [PATCH 4/4] refactor: replace console-stamp with native console patching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the console-stamp dependency and replicate its behavior directly using Node.js built-ins only (node:util styleText). Also clean up the new implementation: - Fix regex bug: unescaped dot in .js patterns (. → \.) - Rename confusing variables: filename/filepath → baseName/parentDir - Split formatTimestamp into readable date/time variables - Update outdated top comment --- js/logger.js | 104 +++++++++++++++++++--------------------------- package-lock.json | 55 +++++++++++------------- package.json | 1 - 3 files changed, 68 insertions(+), 92 deletions(-) diff --git a/js/logger.js b/js/logger.js index 5c1ce8fcfc..ccd74dbb99 100644 --- a/js/logger.js +++ b/js/logger.js @@ -1,71 +1,53 @@ -// This logger is very simple, but needs to be extended. +// Logger for MagicMirror² — works both in Node.js (CommonJS) and the browser (global). (function () { if (typeof module !== "undefined") { if (process.env.mmTestMode !== "true") { const { styleText } = require("node:util"); - // add timestamps in front of log messages - require("console-stamp")(console, { - format: ":date(yyyy-mm-dd HH:MM:ss.l) :label(7) :pre() :msg", - tokens: { - pre: () => { - try { - const lines = new Error().stack.split("\n"); - for (const line of lines) { - if (line.includes("node:") || line.includes("js/logger.js") || line.includes("node_modules")) continue; - const match = line.match(/\((.+?\.js):\d+:\d+\)/) || line.match(/at\s+(.+?\.js):\d+:\d+/); - if (match) { - const file = match[1]; - const filename = file.replace(/.*\/(.*).js/, "$1"); - const filepath = file.replace(/.*\/(.*)\/.*.js/, "$1"); - if (filepath === "js") { - return styleText("gray", `[${filename}]`); - } else { - return styleText("gray", `[${filepath}]`); - } - } - } - } catch (err) { - return styleText("gray", "[unknown]"); - } - }, - label: (arg) => { - const { method, defaultTokens } = arg; - let label = defaultTokens.label(arg); - switch (method) { - case "error": - label = styleText("red", label); - break; - case "warn": - label = styleText("yellow", label); - break; - case "debug": - label = styleText("bgBlue", label); - break; - case "info": - label = styleText("blue", label); - break; - } - return label; - }, - msg: (arg) => { - const { method, defaultTokens } = arg; - let msg = defaultTokens.msg(arg); - switch (method) { - case "error": - msg = styleText("red", msg); - break; - case "warn": - msg = styleText("yellow", msg); - break; - case "info": - msg = styleText("blue", msg); - break; + const LABEL_COLORS = { error: "red", warn: "yellow", debug: "bgBlue", info: "blue" }; + const MSG_COLORS = { error: "red", warn: "yellow", info: "blue" }; + + const formatTimestamp = () => { + const d = new Date(); + const pad2 = (n) => String(n).padStart(2, "0"); + const pad3 = (n) => String(n).padStart(3, "0"); + const date = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`; + const time = `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}.${pad3(d.getMilliseconds())}`; + return `[${date} ${time}]`; + }; + + const getCallerPrefix = () => { + try { + const lines = new Error().stack.split("\n"); + for (const line of lines) { + if (line.includes("node:") || line.includes("js/logger.js") || line.includes("node_modules")) continue; + const match = line.match(/\((.+?\.js):\d+:\d+\)/) || line.match(/at\s+(.+?\.js):\d+:\d+/); + if (match) { + const file = match[1]; + const baseName = file.replace(/.*\/(.*)\.js/, "$1"); + const parentDir = file.replace(/.*\/(.*)\/.*\.js/, "$1"); + return styleText("gray", parentDir === "js" ? `[${baseName}]` : `[${parentDir}]`); } - return msg; } - } - }); + } catch (err) { /* ignore */ } + return styleText("gray", "[unknown]"); + }; + + // Patch console methods to prepend timestamp, level label, and caller prefix. + for (const method of ["debug", "log", "info", "warn", "error"]) { + const original = console[method].bind(console); + const labelRaw = `[${method.toUpperCase()}]`.padEnd(7); + const label = LABEL_COLORS[method] ? styleText(LABEL_COLORS[method], labelRaw) : labelRaw; + console[method] = (...args) => { + const prefix = `${formatTimestamp()} ${label} ${getCallerPrefix()}`; + const msgColor = MSG_COLORS[method]; + if (msgColor && args.length > 0 && typeof args[0] === "string") { + original(prefix, styleText(msgColor, args[0]), ...args.slice(1)); + } else { + original(prefix, ...args); + } + }; + } } // Node, CommonJS module.exports = makeLogger(); diff --git a/package-lock.json b/package-lock.json index fd92837595..735a7a6729 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,6 @@ "@fortawesome/fontawesome-free": "^7.2.0", "ajv": "^8.18.0", "animate.css": "^4.1.1", - "console-stamp": "^3.1.2", "croner": "^10.0.1", "eslint": "^9.39.3", "express": "^5.2.1", @@ -249,6 +248,7 @@ "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@keyv/serialize": "^1.1.1" } @@ -407,6 +407,7 @@ "integrity": "sha512-Tdfx4eH2uS+gv9V9NCr3Rz+c7RSS6ntXp3Blliud18ibRUlRxO9dTaOjG4iv4x0nAmMeedP1ORkEpeXSkh2QiQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=20" } @@ -488,7 +489,8 @@ "resolved": "https://registry.npmjs.org/@cspell/dict-css/-/dict-css-4.0.19.tgz", "integrity": "sha512-VYHtPnZt/Zd/ATbW3rtexWpBnHUohUrQOHff/2JBhsVgxOrksAxJnLAO43Q1ayLJBJUUwNVo+RU0sx0aaysZfg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@cspell/dict-dart": { "version": "2.3.2", @@ -628,14 +630,16 @@ "resolved": "https://registry.npmjs.org/@cspell/dict-html/-/dict-html-4.0.14.tgz", "integrity": "sha512-2bf7n+kS92g+cMKV0wr9o/Oq9n8JzU7CcrB96gIh2GHgnF+0xDOqO2W/1KeFAqOfqosoOVE48t+4dnEMkkoJ2Q==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@cspell/dict-html-symbol-entities": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@cspell/dict-html-symbol-entities/-/dict-html-symbol-entities-4.0.5.tgz", "integrity": "sha512-429alTD4cE0FIwpMucvSN35Ld87HCyuM8mF731KU5Rm4Je2SG6hmVx7nkBsLyrmH3sQukTcr1GaiZsiEg8svPA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@cspell/dict-java": { "version": "5.0.12", @@ -833,7 +837,8 @@ "resolved": "https://registry.npmjs.org/@cspell/dict-typescript/-/dict-typescript-3.2.3.tgz", "integrity": "sha512-zXh1wYsNljQZfWWdSPYwQhpwiuW0KPW1dSd8idjMRvSD0aSvWWHoWlrMsmZeRl4qM4QCEAjua8+cjflm41cQBg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@cspell/dict-vue": { "version": "3.0.5", @@ -991,6 +996,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" }, @@ -1031,6 +1037,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" } @@ -3708,6 +3715,7 @@ "integrity": "sha512-CGJ25bc8fRi8Lod/3GHSvXRKi7nBo3kxh0ApW4yCjmrWmRmlT53B5E08XRSZRliygG0aVNxLrBEqPYdz/KcCtQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/utils": "4.0.18", "fflate": "^0.8.2", @@ -3762,6 +3770,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4344,6 +4353,7 @@ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "license": "MIT", + "peer": true, "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -4637,19 +4647,6 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, - "node_modules/console-stamp": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/console-stamp/-/console-stamp-3.1.2.tgz", - "integrity": "sha512-ab66x3NxOTxPuq71dI6gXEiw2X6ql4Le5gZz0bm7FW3FSCB00eztra/oQUuCoCGlsyKOxtULnHwphzMrRtzMBg==", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.2", - "dateformat": "^4.6.3" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/content-disposition": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", @@ -5064,15 +5061,6 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/dateformat": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", - "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/dayjs": { "version": "1.11.15", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.15.tgz", @@ -5703,6 +5691,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz", "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7557,6 +7546,7 @@ "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@acemir/cssom": "^0.9.31", "@asamuzakjp/dom-selector": "^6.8.1", @@ -7630,7 +7620,6 @@ "integrity": "sha512-75EA7EWZExL/j+MDKQrRbdzcRI2HOkRlmUw8fZJc1ioqFEOvBsq7Rt+A6yCxOt9w/TYNpkt52gC6nm/g5tFIng==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "acorn": "^8.5.0", "eslint-visitor-keys": "^5.0.0", @@ -7649,7 +7638,6 @@ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" }, @@ -7962,6 +7950,7 @@ "integrity": "sha512-DzzmbqfMW3EzHsunP66x556oZDzjcdjjlL2bHG4PubwnL58ZPAfz07px4GqteZkoCGnBYi779Y2mg7+vgNCwbw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "globby": "16.1.0", "js-yaml": "4.1.1", @@ -9680,6 +9669,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -9722,6 +9712,7 @@ "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -9752,6 +9743,7 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -11010,6 +11002,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-parser-algorithms": "^4.0.0", @@ -11622,7 +11615,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11692,6 +11684,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "napi-postinstall": "^0.3.0" }, @@ -11793,6 +11786,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -11883,6 +11877,7 @@ "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", diff --git a/package.json b/package.json index 8bc5c4b30b..17cf13765b 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,6 @@ "@fortawesome/fontawesome-free": "^7.2.0", "ajv": "^8.18.0", "animate.css": "^4.1.1", - "console-stamp": "^3.1.2", "croner": "^10.0.1", "eslint": "^9.39.3", "express": "^5.2.1",