Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion nx.json
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,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"
]
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/nx/bin/nx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
38 changes: 37 additions & 1 deletion packages/nx/src/tasks-runner/task-env.spec.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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');
});
});
28 changes: 26 additions & 2 deletions packages/nx/src/tasks-runner/task-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}

Expand Down
9 changes: 3 additions & 6 deletions packages/nx/src/tasks-runner/task-orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { RunningTask } from './running-tasks/running-task';
import {
getEnvVariablesForBatchProcess,
getEnvVariablesForTask,
getForceColorForChild,
getTaskSpecificEnv,
} from './task-env';
import { TaskStatus } from './tasks-runner';
Expand Down Expand Up @@ -955,9 +956,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,
Expand Down Expand Up @@ -1264,9 +1263,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,
Expand Down
Loading