diff --git a/nx.json b/nx.json index f064babd75e..bfbb9d572ee 100644 --- a/nx.json +++ b/nx.json @@ -537,7 +537,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 887a8c8f181..8c9e159bb53 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 f25cbe063db..88dc20ab2fd 100644 --- a/packages/nx/src/tasks-runner/task-env.spec.ts +++ b/packages/nx/src/tasks-runner/task-env.spec.ts @@ -6,6 +6,7 @@ import { Task } from '../config/task-graph'; import { getEnvFilesForTask, getEnvVariablesForTask, + getForceColorForChild, loadAndExpandDotEnvFile, } from './task-env'; @@ -271,3 +272,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 e1873d5350e..69f9f2c6052 100644 --- a/packages/nx/src/tasks-runner/task-env.ts +++ b/packages/nx/src/tasks-runner/task-env.ts @@ -6,20 +6,41 @@ import { Task } from '../config/task-graph'; import { workspaceRoot } from '../utils/workspace-root'; 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 7953630efb1..321d8c43c44 100644 --- a/packages/nx/src/tasks-runner/task-orchestrator.ts +++ b/packages/nx/src/tasks-runner/task-orchestrator.ts @@ -49,6 +49,7 @@ import { SharedRunningTask } from './running-tasks/shared-running-task'; import { getEnvVariablesForBatchProcess, getEnvVariablesForTask, + getForceColorForChild, getTaskSpecificEnv, } from './task-env'; import { TaskStatus } from './tasks-runner'; @@ -1036,9 +1037,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, @@ -1347,9 +1346,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,