Skip to content

Commit 3eb3745

Browse files
Fix Node.js v25 logging prefix and modernize logger (#4049)
On Node.js v25, the log prefix in the terminal stopped working - instead of seeing something like: ``` [2026-03-05 23:00:00.000] [LOG] [app] Starting MagicMirror: v2.35.0 ``` the output was: ``` [2026-03-05 23:00:00.000] :pre() Starting MagicMirror: v2.35.0 ``` Reported in #4048. ## Why did it break? The logger used the `console-stamp` package to format log output. One part of that formatting used `styleText("grey", ...)` to color the caller prefix gray. Node.js v25 dropped `"grey"` as a valid color name (only `"gray"` with an "a" is accepted now). This caused `styleText` to throw an error internally - and `console-stamp` silently swallowed that error and fell back to returning its raw `:pre()` format string as the prefix. Not ideal. ## What's in this PR? **1. The actual fix** - `"grey"` → `"gray"`. **2. Cleaner stack trace approach** - the previous code set `Error.prepareStackTrace` *after* creating the `Error`, which is fragile and was starting to behave differently across Node versions. Replaced with straightforward string parsing of `new Error().stack`. **3. Removed the `console-stamp` dependency** - all formatting is now done with plain Node.js built-ins (`node:util` `styleText`). Same visual result, no external dependency. **4. Simplified the module wrapper** - the logger was wrapped in a UMD pattern, which is meant for environments like AMD/RequireJS. MagicMirror only runs in two places: Node.js and the browser. Replaced with a simple check (`typeof module !== "undefined"`), which is much easier to follow.
1 parent ab3108f commit 3eb3745

File tree

3 files changed

+128
-150
lines changed

3 files changed

+128
-150
lines changed

js/logger.js

Lines changed: 103 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,131 +1,115 @@
1-
// This logger is very simple, but needs to be extended.
2-
(function (root, factory) {
3-
if (typeof exports === "object") {
1+
// Logger for MagicMirror² — works both in Node.js (CommonJS) and the browser (global).
2+
(function () {
3+
if (typeof module !== "undefined") {
44
if (process.env.mmTestMode !== "true") {
55
const { styleText } = require("node:util");
66

7-
// add timestamps in front of log messages
8-
require("console-stamp")(console, {
9-
format: ":date(yyyy-mm-dd HH:MM:ss.l) :label(7) :pre() :msg",
10-
tokens: {
11-
pre: () => {
12-
const err = new Error();
13-
Error.prepareStackTrace = (_, stack) => stack;
14-
const stack = err.stack;
15-
Error.prepareStackTrace = undefined;
16-
try {
17-
for (const line of stack) {
18-
const file = line.getFileName();
19-
if (file && !file.includes("node:") && !file.includes("js/logger.js") && !file.includes("node_modules")) {
20-
const filename = file.replace(/.*\/(.*).js/, "$1");
21-
const filepath = file.replace(/.*\/(.*)\/.*.js/, "$1");
22-
if (filepath === "js") {
23-
return styleText("grey", `[${filename}]`);
24-
} else {
25-
return styleText("grey", `[${filepath}]`);
26-
}
27-
}
28-
}
29-
} catch (err) {
30-
return styleText("grey", "[unknown]");
31-
}
32-
},
33-
label: (arg) => {
34-
const { method, defaultTokens } = arg;
35-
let label = defaultTokens.label(arg);
36-
switch (method) {
37-
case "error":
38-
label = styleText("red", label);
39-
break;
40-
case "warn":
41-
label = styleText("yellow", label);
42-
break;
43-
case "debug":
44-
label = styleText("bgBlue", label);
45-
break;
46-
case "info":
47-
label = styleText("blue", label);
48-
break;
49-
}
50-
return label;
51-
},
52-
msg: (arg) => {
53-
const { method, defaultTokens } = arg;
54-
let msg = defaultTokens.msg(arg);
55-
switch (method) {
56-
case "error":
57-
msg = styleText("red", msg);
58-
break;
59-
case "warn":
60-
msg = styleText("yellow", msg);
61-
break;
62-
case "info":
63-
msg = styleText("blue", msg);
64-
break;
7+
const LABEL_COLORS = { error: "red", warn: "yellow", debug: "bgBlue", info: "blue" };
8+
const MSG_COLORS = { error: "red", warn: "yellow", info: "blue" };
9+
10+
const formatTimestamp = () => {
11+
const d = new Date();
12+
const pad2 = (n) => String(n).padStart(2, "0");
13+
const pad3 = (n) => String(n).padStart(3, "0");
14+
const date = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
15+
const time = `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}.${pad3(d.getMilliseconds())}`;
16+
return `[${date} ${time}]`;
17+
};
18+
19+
const getCallerPrefix = () => {
20+
try {
21+
const lines = new Error().stack.split("\n");
22+
for (const line of lines) {
23+
if (line.includes("node:") || line.includes("js/logger.js") || line.includes("node_modules")) continue;
24+
const match = line.match(/\((.+?\.js):\d+:\d+\)/) || line.match(/at\s+(.+?\.js):\d+:\d+/);
25+
if (match) {
26+
const file = match[1];
27+
const baseName = file.replace(/.*\/(.*)\.js/, "$1");
28+
const parentDir = file.replace(/.*\/(.*)\/.*\.js/, "$1");
29+
return styleText("gray", parentDir === "js" ? `[${baseName}]` : `[${parentDir}]`);
6530
}
66-
return msg;
6731
}
68-
}
69-
});
32+
} catch (err) { /* ignore */ }
33+
return styleText("gray", "[unknown]");
34+
};
35+
36+
// Patch console methods to prepend timestamp, level label, and caller prefix.
37+
for (const method of ["debug", "log", "info", "warn", "error"]) {
38+
const original = console[method].bind(console);
39+
const labelRaw = `[${method.toUpperCase()}]`.padEnd(7);
40+
const label = LABEL_COLORS[method] ? styleText(LABEL_COLORS[method], labelRaw) : labelRaw;
41+
console[method] = (...args) => {
42+
const prefix = `${formatTimestamp()} ${label} ${getCallerPrefix()}`;
43+
const msgColor = MSG_COLORS[method];
44+
if (msgColor && args.length > 0 && typeof args[0] === "string") {
45+
original(prefix, styleText(msgColor, args[0]), ...args.slice(1));
46+
} else {
47+
original(prefix, ...args);
48+
}
49+
};
50+
}
7051
}
71-
// Node, CommonJS-like
72-
module.exports = factory(root.config);
73-
} else {
74-
// Browser globals (root is window)
75-
root.Log = factory(root.config);
76-
}
77-
}(this, function (config) {
78-
let logLevel;
79-
let enableLog;
80-
if (typeof exports === "object") {
81-
// in nodejs and not running in test mode
82-
enableLog = process.env.mmTestMode !== "true";
52+
// Node, CommonJS
53+
module.exports = makeLogger();
8354
} else {
84-
// in browser and not running with jsdom
85-
enableLog = typeof window === "object" && window.name !== "jsdom";
55+
// Browser globals
56+
window.Log = makeLogger();
8657
}
8758

88-
if (enableLog) {
89-
logLevel = {
90-
debug: Function.prototype.bind.call(console.debug, console),
91-
log: Function.prototype.bind.call(console.log, console),
92-
info: Function.prototype.bind.call(console.info, console),
93-
warn: Function.prototype.bind.call(console.warn, console),
94-
error: Function.prototype.bind.call(console.error, console),
95-
group: Function.prototype.bind.call(console.group, console),
96-
groupCollapsed: Function.prototype.bind.call(console.groupCollapsed, console),
97-
groupEnd: Function.prototype.bind.call(console.groupEnd, console),
98-
time: Function.prototype.bind.call(console.time, console),
99-
timeEnd: Function.prototype.bind.call(console.timeEnd, console),
100-
timeStamp: console.timeStamp ? Function.prototype.bind.call(console.timeStamp, console) : function () {}
101-
};
59+
/**
60+
* Creates the logger object. Logging is disabled when running in test mode
61+
* (Node.js) or inside jsdom (browser).
62+
* @returns {object} The logger object with log level methods.
63+
*/
64+
function makeLogger () {
65+
const enableLog = typeof module !== "undefined"
66+
? process.env.mmTestMode !== "true"
67+
: typeof window === "object" && window.name !== "jsdom";
10268

103-
logLevel.setLogLevel = function (newLevel) {
104-
if (newLevel) {
105-
Object.keys(logLevel).forEach(function (key) {
106-
if (!newLevel.includes(key.toLocaleUpperCase())) {
107-
logLevel[key] = function () {};
108-
}
109-
});
110-
}
111-
};
112-
} else {
113-
logLevel = {
114-
debug () {},
115-
log () {},
116-
info () {},
117-
warn () {},
118-
error () {},
119-
group () {},
120-
groupCollapsed () {},
121-
groupEnd () {},
122-
time () {},
123-
timeEnd () {},
124-
timeStamp () {}
125-
};
69+
let logLevel;
12670

127-
logLevel.setLogLevel = function () {};
128-
}
71+
if (enableLog) {
72+
logLevel = {
73+
debug: Function.prototype.bind.call(console.debug, console),
74+
log: Function.prototype.bind.call(console.log, console),
75+
info: Function.prototype.bind.call(console.info, console),
76+
warn: Function.prototype.bind.call(console.warn, console),
77+
error: Function.prototype.bind.call(console.error, console),
78+
group: Function.prototype.bind.call(console.group, console),
79+
groupCollapsed: Function.prototype.bind.call(console.groupCollapsed, console),
80+
groupEnd: Function.prototype.bind.call(console.groupEnd, console),
81+
time: Function.prototype.bind.call(console.time, console),
82+
timeEnd: Function.prototype.bind.call(console.timeEnd, console),
83+
timeStamp: console.timeStamp ? Function.prototype.bind.call(console.timeStamp, console) : function () {}
84+
};
85+
86+
logLevel.setLogLevel = function (newLevel) {
87+
if (newLevel) {
88+
Object.keys(logLevel).forEach(function (key) {
89+
if (!newLevel.includes(key.toLocaleUpperCase())) {
90+
logLevel[key] = function () {};
91+
}
92+
});
93+
}
94+
};
95+
} else {
96+
logLevel = {
97+
debug () {},
98+
log () {},
99+
info () {},
100+
warn () {},
101+
error () {},
102+
group () {},
103+
groupCollapsed () {},
104+
groupEnd () {},
105+
time () {},
106+
timeEnd () {},
107+
timeStamp () {}
108+
};
129109

130-
return logLevel;
131-
}));
110+
logLevel.setLogLevel = function () {};
111+
}
112+
113+
return logLevel;
114+
}
115+
}());

0 commit comments

Comments
 (0)