From 182c1fb7abab119c954d1108395697a61c7da718 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Muhammed=20K=C4=B1l=C4=B1=C3=A7?= Date: Fri, 4 Sep 2026 10:17:11 +0300 Subject: [PATCH 1/2] worker: propagate TTY color support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Propagate parent stdout and stderr TTY state to workers that use default stdio handling. This allows styleText() to detect color support inside workers. Keep captured worker output unstyled because its eventual destination is unknown. Fixes: https://github.com/nodejs/node/issues/65766 Signed-off-by: Muhammed Kılıç --- lib/internal/main/worker_thread.js | 32 ++++++++++++ lib/internal/worker.js | 2 + test/parallel/test-util-styletext-worker.js | 55 +++++++++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 test/parallel/test-util-styletext-worker.js diff --git a/lib/internal/main/worker_thread.js b/lib/internal/main/worker_thread.js index 45e5efddb6f8..f18f58323d7c 100644 --- a/lib/internal/main/worker_thread.js +++ b/lib/internal/main/worker_thread.js @@ -99,6 +99,8 @@ port.on('message', (message) => { filename, hasStdin, publicPort, + stderrIsTTY, + stdoutIsTTY, workerData, webWorkerData, mainThreadPort, @@ -117,6 +119,36 @@ port.on('message', (message) => { require('internal/worker').assignEnvironmentData(environmentData); setupMainThreadPort(mainThreadPort); + if (stdoutIsTTY || stderrIsTTY) { + const { getColorDepth, hasColors } = require('internal/tty'); + const setupTTYProperties = (stream) => { + ObjectDefineProperty(stream, 'isTTY', { + __proto__: null, + configurable: true, + enumerable: true, + value: true, + writable: true, + }); + ObjectDefineProperty(stream, 'getColorDepth', { + __proto__: null, + configurable: true, + enumerable: true, + value: getColorDepth, + writable: true, + }); + ObjectDefineProperty(stream, 'hasColors', { + __proto__: null, + configurable: true, + enumerable: true, + value: hasColors, + writable: true, + }); + }; + + if (stdoutIsTTY) setupTTYProperties(process.stdout); + if (stderrIsTTY) setupTTYProperties(process.stderr); + } + if (webWorkerData !== undefined) { require('internal/webworker') .installDedicatedWorkerGlobalScope(webWorkerData.url, webWorkerData); diff --git a/lib/internal/worker.js b/lib/internal/worker.js index f3f3c23a5abe..81d5bbf84f7f 100644 --- a/lib/internal/worker.js +++ b/lib/internal/worker.js @@ -367,6 +367,8 @@ class Worker extends EventEmitter { webWorkerData: options[kWebWorkerData], environmentData, hasStdin: !!options.stdin, + stdoutIsTTY: !options.stdout && !!process.stdout.isTTY, + stderrIsTTY: !options.stderr && !!process.stderr.isTTY, publicPort: publicPortToWorker, mainThreadPort: mainThreadPortToWorker, }, transferList); diff --git a/test/parallel/test-util-styletext-worker.js b/test/parallel/test-util-styletext-worker.js new file mode 100644 index 000000000000..296734b4e1df --- /dev/null +++ b/test/parallel/test-util-styletext-worker.js @@ -0,0 +1,55 @@ +'use strict'; + +const common = require('../common'); +const assert = require('node:assert'); +const { once } = require('node:events'); +const { Worker } = require('node:worker_threads'); + +const styled = '\u001b[31mhello\u001b[39m'; +const plain = 'hello'; + +const workerCode = ` + const { parentPort } = require('node:worker_threads'); + const { styleText } = require('node:util'); + + parentPort.postMessage(styleText('red', 'hello')); +`; + +async function runWorker(options) { + const worker = new Worker(workerCode, options); + const exit = once(worker, 'exit'); + const [actual] = await once(worker, 'message'); + + assert.deepStrictEqual(await exit, [0]); + return actual; +} + +// Make the parent destination deterministically color-capable. Worker color +// behavior should not depend on whether the test runner itself owns a TTY. +Object.defineProperty(process.stdout, 'isTTY', { + configurable: true, + value: true, +}); +Object.defineProperty(process.stdout, 'getColorDepth', { + configurable: true, + value: () => 8, +}); + +const env = { ...process.env, TERM: 'xterm-256color' }; +delete env.FORCE_COLOR; +delete env.NO_COLOR; +delete env.NODE_DISABLE_COLORS; + +common.mustCall(async () => { + const [pipedOutput, capturedOutput] = await Promise.all([ + // By default, worker output is piped to the parent's color-capable stdout. + runWorker({ eval: true, env }), + + // With stdout: true, the output is captured and its eventual destination is + // unknown, so styleText() should not add ANSI sequences automatically. + runWorker({ eval: true, env, stdout: true }), + ]); + + assert.strictEqual(capturedOutput, plain); + assert.strictEqual(pipedOutput, styled); +})(); From 03440165f4ae572d5b21e061471f9efaa974543f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Muhammed=20K=C4=B1l=C4=B1=C3=A7?= Date: Fri, 4 Sep 2026 10:57:03 +0300 Subject: [PATCH 2/2] test: expand worker styleText coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cover stdout, stderr, captured streams, and non-TTY parent output. Fixes: #65766 Signed-off-by: Muhammed Kılıç --- test/parallel/test-util-styletext-worker.js | 103 +++++++++++++++----- 1 file changed, 76 insertions(+), 27 deletions(-) diff --git a/test/parallel/test-util-styletext-worker.js b/test/parallel/test-util-styletext-worker.js index 296734b4e1df..805250953b39 100644 --- a/test/parallel/test-util-styletext-worker.js +++ b/test/parallel/test-util-styletext-worker.js @@ -9,14 +9,30 @@ const styled = '\u001b[31mhello\u001b[39m'; const plain = 'hello'; const workerCode = ` - const { parentPort } = require('node:worker_threads'); + const { parentPort, workerData } = require('node:worker_threads'); const { styleText } = require('node:util'); - parentPort.postMessage(styleText('red', 'hello')); + const stream = process[workerData.stream]; + parentPort.postMessage({ + hasColorDepth: typeof stream.getColorDepth === 'function', + hasColors: typeof stream.hasColors === 'function', + isTTY: stream.isTTY, + result: styleText('red', 'hello', { stream }), + }); `; -async function runWorker(options) { - const worker = new Worker(workerCode, options); +const env = { ...process.env, TERM: 'xterm-256color' }; +delete env.FORCE_COLOR; +delete env.NO_COLOR; +delete env.NODE_DISABLE_COLORS; + +async function runWorker(stream, options = {}) { + const worker = new Worker(workerCode, { + env, + eval: true, + workerData: { stream }, + ...options, + }); const exit = once(worker, 'exit'); const [actual] = await once(worker, 'message'); @@ -24,32 +40,65 @@ async function runWorker(options) { return actual; } -// Make the parent destination deterministically color-capable. Worker color -// behavior should not depend on whether the test runner itself owns a TTY. -Object.defineProperty(process.stdout, 'isTTY', { - configurable: true, - value: true, -}); -Object.defineProperty(process.stdout, 'getColorDepth', { - configurable: true, - value: () => 8, -}); +function setIsTTY(stream, value) { + Object.defineProperty(stream, 'isTTY', { + configurable: true, + value, + }); +} -const env = { ...process.env, TERM: 'xterm-256color' }; -delete env.FORCE_COLOR; -delete env.NO_COLOR; -delete env.NODE_DISABLE_COLORS; +function restoreIsTTY(stream, descriptor) { + if (descriptor === undefined) { + delete stream.isTTY; + } else { + Object.defineProperty(stream, 'isTTY', descriptor); + } +} + +const stdoutIsTTY = Object.getOwnPropertyDescriptor(process.stdout, 'isTTY'); +const stderrIsTTY = Object.getOwnPropertyDescriptor(process.stderr, 'isTTY'); common.mustCall(async () => { - const [pipedOutput, capturedOutput] = await Promise.all([ - // By default, worker output is piped to the parent's color-capable stdout. - runWorker({ eval: true, env }), + try { + setIsTTY(process.stdout, true); + assert.deepStrictEqual(await runWorker('stdout'), { + hasColorDepth: true, + hasColors: true, + isTTY: true, + result: styled, + }); + + setIsTTY(process.stderr, true); + assert.deepStrictEqual(await runWorker('stderr'), { + hasColorDepth: true, + hasColors: true, + isTTY: true, + result: styled, + }); + + assert.deepStrictEqual(await runWorker('stdout', { stdout: true }), { + hasColorDepth: false, + hasColors: false, + isTTY: undefined, + result: plain, + }); - // With stdout: true, the output is captured and its eventual destination is - // unknown, so styleText() should not add ANSI sequences automatically. - runWorker({ eval: true, env, stdout: true }), - ]); + assert.deepStrictEqual(await runWorker('stderr', { stderr: true }), { + hasColorDepth: false, + hasColors: false, + isTTY: undefined, + result: plain, + }); - assert.strictEqual(capturedOutput, plain); - assert.strictEqual(pipedOutput, styled); + setIsTTY(process.stdout, false); + assert.deepStrictEqual(await runWorker('stdout'), { + hasColorDepth: false, + hasColors: false, + isTTY: undefined, + result: plain, + }); + } finally { + restoreIsTTY(process.stdout, stdoutIsTTY); + restoreIsTTY(process.stderr, stderrIsTTY); + } })();