diff --git a/nx.json b/nx.json index 73e33efc9d0434..ea35b49ebe2054 100644 --- a/nx.json +++ b/nx.json @@ -427,7 +427,8 @@ "NX_PREFIX_OUTPUT", "NX_INFER_ALL_PACKAGE_JSONS", "NX_AI_FILES_USE_LOCAL", - "NX_WINDOWS_PTY_SUPPORT" + "NX_WINDOWS_PTY_SUPPORT", + "NX_ORIGINAL_FORCE_COLOR" ] } } diff --git a/packages/nx/bin/nx.ts b/packages/nx/bin/nx.ts index 557867c55e135b..ad58f0d91b1b5d 100644 --- a/packages/nx/bin/nx.ts +++ b/packages/nx/bin/nx.ts @@ -4,6 +4,7 @@ // See: https://github.com/alexeyraspopov/picocolors/issues/100 if (process.env.FORCE_COLOR === '0') { + process.env.NX_ORIGINAL_FORCE_COLOR = '0'; process.env.NO_COLOR = '1'; delete process.env.FORCE_COLOR; } diff --git a/packages/nx/src/tasks-runner/task-env.spec.ts b/packages/nx/src/tasks-runner/task-env.spec.ts index ae7d25fd77014a..627fb55c01f77e 100644 --- a/packages/nx/src/tasks-runner/task-env.spec.ts +++ b/packages/nx/src/tasks-runner/task-env.spec.ts @@ -1,7 +1,11 @@ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { getEnvFilesForTask, loadAndExpandDotEnvFile } from './task-env'; +import { + getEnvFilesForTask, + getForceColorForChild, + loadAndExpandDotEnvFile, +} from './task-env'; import { Task } from '../config/task-graph'; import { ProjectGraph } from '../config/project-graph'; @@ -207,3 +211,35 @@ describe('getEnvFilesForTask', () => { expect(envFiles).toMatchSnapshot(); }); }); + +describe('getForceColorForChild', () => { + const originalEnv = { ...process.env }; + + afterEach(() => { + process.env = { ...originalEnv }; + }); + + it('should return FORCE_COLOR when it is explicitly set', () => { + process.env.FORCE_COLOR = '1'; + delete process.env.NX_ORIGINAL_FORCE_COLOR; + expect(getForceColorForChild()).toBe('1'); + }); + + it('should return "0" when NX_ORIGINAL_FORCE_COLOR is "0" and FORCE_COLOR was deleted', () => { + delete process.env.FORCE_COLOR; + process.env.NX_ORIGINAL_FORCE_COLOR = '0'; + expect(getForceColorForChild()).toBe('0'); + }); + + it('should default to "true" when neither FORCE_COLOR nor NX_ORIGINAL_FORCE_COLOR is set', () => { + delete process.env.FORCE_COLOR; + delete process.env.NX_ORIGINAL_FORCE_COLOR; + expect(getForceColorForChild()).toBe('true'); + }); + + it('should prefer FORCE_COLOR over NX_ORIGINAL_FORCE_COLOR when both are set', () => { + process.env.FORCE_COLOR = '3'; + process.env.NX_ORIGINAL_FORCE_COLOR = '0'; + expect(getForceColorForChild()).toBe('3'); + }); +}); diff --git a/packages/nx/src/tasks-runner/task-env.ts b/packages/nx/src/tasks-runner/task-env.ts index 13004e213835a9..a28e35f2b3e42d 100644 --- a/packages/nx/src/tasks-runner/task-env.ts +++ b/packages/nx/src/tasks-runner/task-env.ts @@ -6,20 +6,41 @@ import { join } from 'node:path'; import { ProjectGraph } from '../config/project-graph'; import { getEnvPathsForTask } from './task-env-paths'; +/** + * Resolves the FORCE_COLOR value for forked child processes. + * + * When the user sets FORCE_COLOR=0, bin/nx.ts deletes it from process.env + * (workaround for picocolors treating "0" as truthy) and saves the original + * value in NX_ORIGINAL_FORCE_COLOR. Without this check, the undefined + * FORCE_COLOR would default to 'true', re-enabling colors in all children. + */ +export function getForceColorForChild(): string { + if (process.env.FORCE_COLOR !== undefined) { + return process.env.FORCE_COLOR; + } + if (process.env.NX_ORIGINAL_FORCE_COLOR === '0') { + return '0'; + } + return 'true'; +} + export function getEnvVariablesForBatchProcess( skipNxCache: boolean, captureStderr: boolean ): NodeJS.ProcessEnv { - return { + const res = { // User Process Env Variables override Dotenv Variables ...process.env, // Nx Env Variables overrides everything ...getNxEnvVariablesForForkedProcess( - process.env.FORCE_COLOR === undefined ? 'true' : process.env.FORCE_COLOR, + getForceColorForChild(), skipNxCache, captureStderr ), }; + // NX_ORIGINAL_FORCE_COLOR is an internal signal and should not leak into child processes + delete res.NX_ORIGINAL_FORCE_COLOR; + return res; } // The orchestrator now calls this eagerly during the coordinator pre-hash @@ -80,6 +101,9 @@ export function getEnvVariablesForTask( } // we don't reset NX_BASE or NX_HEAD because those are set by the user and should be preserved delete res.NX_SET_CLI; + // NX_ORIGINAL_FORCE_COLOR is an internal signal used by getForceColorForChild() + // and should not leak into child processes + delete res.NX_ORIGINAL_FORCE_COLOR; return res; } diff --git a/packages/nx/src/tasks-runner/task-orchestrator.ts b/packages/nx/src/tasks-runner/task-orchestrator.ts index 7340ad04a969e8..59590a331e33a0 100644 --- a/packages/nx/src/tasks-runner/task-orchestrator.ts +++ b/packages/nx/src/tasks-runner/task-orchestrator.ts @@ -47,6 +47,7 @@ import { RunningTask } from './running-tasks/running-task'; import { getEnvVariablesForBatchProcess, getEnvVariablesForTask, + getForceColorForChild, getTaskSpecificEnv, } from './task-env'; import { TaskStatus } from './tasks-runner'; @@ -965,9 +966,7 @@ export class TaskOrchestrator { ? getEnvVariablesForTask( task, taskSpecificEnv, - process.env.FORCE_COLOR === undefined - ? 'true' - : process.env.FORCE_COLOR, + getForceColorForChild(), this.options.skipNxCache, this.options.captureStderr, null, @@ -1274,9 +1273,7 @@ export class TaskOrchestrator { ? getEnvVariablesForTask( task, taskSpecificEnv, - process.env.FORCE_COLOR === undefined - ? 'true' - : process.env.FORCE_COLOR, + getForceColorForChild(), this.options.skipNxCache, this.options.captureStderr, null,