Skip to content

Dev worker loses ANSI color & terminal width (isTTY unset) — propagate FORCE_COLOR/COLUMNS to worker env #37

Description

@pi0x

Environment

  • env-runner: 0.1.16
  • Runner: node-worker (NodeWorkerEnvRunner, node:worker_threads)

Summary

Anything that logs from inside a dev worker loses ANSI color (and cannot detect terminal width), even when the host process is an interactive TTY.

The runner starts the worker with the host env inherited verbatim and nothing else:

// dist/_chunks/node-worker-runner.mjs
const worker = new Worker(this._workerEntry, {
  env: { ...process.env },
  workerData: { name: this._name, ...this._data },
});

In a node:worker_threads worker, process.stdout is a pipe to the host — so process.stdout.isTTY and process.stdout.columns are undefined. Worker console.log output still reaches the terminal, but the standard color/width detection everyone uses (isTTY) reports "not a terminal", so ANSI is stripped and width-aware output can't size itself.

Net effect: framework logs printed from the host process are colored; anything printed from application/runtime code running in the worker is not.

Reproduction

// worker-tty.mjs
import { Worker, isMainThread } from "node:worker_threads";
if (isMainThread) {
  console.log("[main]  ", process.stdout.isTTY, process.stdout.columns);
  new Worker(new URL(import.meta.url), { env: { ...process.env } }); // same as env-runner
} else {
  console.log("[worker]", process.stdout.isTTY, process.stdout.columns, process.env.FORCE_COLOR);
}

Run from a real terminal:

[main]   true 173
[worker] undefined undefined undefined

The env is inherited, so FORCE_COLOR set on the host does reach the worker — it's just never set.

Proposed fix

When spawning a worker, propagate the host's terminal capabilities into the worker env (respecting existing FORCE_COLOR / NO_COLOR), the way tinypool/vitest, execa, npm, etc. do:

const env = { ...process.env };
if (!("FORCE_COLOR" in env) && !env.NO_COLOR && process.stdout.isTTY) {
  env.FORCE_COLOR = "1";
}
if (!("COLUMNS" in env) && process.stdout.columns) {
  env.COLUMNS = String(process.stdout.columns);
}
const worker = new Worker(this._workerEntry, { env, workerData: { ... } });

Then worker-side code using the standard NO_COLOR / FORCE_COLORisTTY cascade colorizes correctly, and width-aware output can read COLUMNS.

Notes:

  • Same reasoning applies to the child-process runners (Deno) — anything where the child's stdout isn't the host TTY.
  • COLUMNS is a static snapshot; propagating resize is a nice-to-have but not required for the color fix.

Context

Surfaced via Nitro's experimental tracingLogger (a per-request span waterfall) which renders colored ANSI + adapts to terminal width — both are disabled in nitro dev purely because the worker reports no TTY. Nitro-side it now honors FORCE_COLOR/NO_COLOR/COLUMNS, so once the runner propagates them it "just works".

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions