Repository: denoland/deno
Pull request: fix(runtime): output to stderr with colors if a tty and stdout is piped

Something I always wanted in Deno (took a break for 30 mins from normal activities)

![image](https://github.com/denoland/deno/assets/1609021/05c07ad7-8516-412f-ab90-81005014e0ae)

This also fixes a bug where Deno would output to stderr with colours when piped and stdout was not piped.


Changed files:
filename: cli/worker.rs
patch: @@ -601,7 +601,8 @@ impl CliMainWorkerFactory {
         locale: deno_core::v8::icu::get_language_tag(),
         location: shared.options.location.clone(),
         no_color: !colors::use_color(),
-        is_tty: deno_terminal::is_stdout_tty(),
+        is_stdout_tty: deno_terminal::is_stdout_tty(),
+        is_stderr_tty: deno_terminal::is_stderr_tty(),
         unstable: shared.options.unstable,
         unstable_features,
         user_agent: version::get_user_agent().to_string(),
@@ -811,7 +812,8 @@ fn create_web_worker_callback(
         locale: deno_core::v8::icu::get_language_tag(),
         location: Some(args.main_module.clone()),
         no_color: !colors::use_color(),
-        is_tty: deno_terminal::is_stdout_tty(),
+        is_stdout_tty: deno_terminal::is_stdout_tty(),
+        is_stderr_tty: deno_terminal::is_stderr_tty(),
         unstable: shared.options.unstable,
         unstable_features,
         user_agent: version::get_user_agent().to_string(),

filename: ext/console/01_console.js
patch: @@ -156,14 +156,20 @@ const {
   Uint8Array,
 } = primordials;
 
-let noColor = () => false;
+let noColorStdout = () => false;
+let noColorStderr = () => false;
 
-function setNoColorFn(fn) {
-  noColor = fn;
+function setNoColorFns(stdoutFn, stderrFn) {
+  noColorStdout = stdoutFn;
+  noColorStderr = stderrFn;
 }
 
-function getNoColor() {
-  return noColor();
+function getStdoutNoColor() {
+  return noColorStdout();
+}
+
+function getStderrNoColor() {
+  return noColorStderr();
 }
 
 function assert(cond, msg = "Assertion failed.") {
@@ -2927,6 +2933,7 @@ function cssToAnsi(css, prevCss = null) {
 function inspectArgs(args, inspectOptions = {}) {
   const ctx = {
     ...getDefaultInspectOptions(),
+    colors: inspectOptions.colors ?? !noColorStdout(),
     ...inspectOptions,
   };
   if (inspectOptions.iterableLimit !== undefined) {
@@ -2939,7 +2946,7 @@ function inspectArgs(args, inspectOptions = {}) {
   if (ctx.maxArrayLength === null) ctx.maxArrayLength = Infinity;
   if (ctx.maxStringLength === null) ctx.maxStringLength = Infinity;
 
-  const noColor = getNoColor();
+  const noColor = !ctx.colors;
   const first = args[0];
   let a = 0;
   let string = "";
@@ -3053,12 +3060,12 @@ const countMap = new SafeMap();
 const timerMap = new SafeMap();
 const isConsoleInstance = Symbol("isConsoleInstance");
 
-function getConsoleInspectOptions() {
-  const color = !getNoColor();
+/** @param noColor {boolean} */
+function getConsoleInspectOptions(noColor) {
   return {
     ...getDefaultInspectOptions(),
-    colors: color,
-    stylize: color ? createStylizeWithColor(styles, colors) : stylizeNoColor,
+    colors: !noColor,
+    stylize: noColor ? stylizeNoColor : createStylizeWithColor(styles, colors),
   };
 }
 
@@ -3090,7 +3097,7 @@ class Console {
   log = (...args) => {
     this.#printFunc(
       inspectArgs(args, {
-        ...getConsoleInspectOptions(),
+        ...getConsoleInspectOptions(noColorStdout()),
         indentLevel: this.indentLevel,
       }) + "\n",
       1,
@@ -3100,7 +3107,7 @@ class Console {
   debug = (...args) => {
     this.#printFunc(
       inspectArgs(args, {
-        ...getConsoleInspectOptions(),
+        ...getConsoleInspectOptions(noColorStdout()),
         indentLevel: this.indentLevel,
       }) + "\n",
       0,
@@ -3110,7 +3117,7 @@ class Console {
   info = (...args) => {
     this.#printFunc(
       inspectArgs(args, {
-        ...getConsoleInspectOptions(),
+        ...getConsoleInspectOptions(noColorStdout()),
         indentLevel: this.indentLevel,
       }) + "\n",
       1,
@@ -3119,8 +3126,10 @@ class Console {
 
   dir = (obj = undefined, options = {}) => {
     this.#printFunc(
-      inspectArgs([obj], { ...getConsoleInspectOptions(), ...options }) +
-        "\n",
+      inspectArgs([obj], {
+        ...getConsoleInspectOptions(noColorStdout()),
+        ...options,
+      }) + "\n",
       1,
     );
   };
@@ -3130,7 +3139,7 @@ class Console {
   warn = (...args) => {
     this.#printFunc(
       inspectArgs(args, {
-        ...getConsoleInspectOptions(),
+        ...getConsoleInspectOptions(noColorStderr()),
         indentLevel: this.indentLevel,
       }) + "\n",
       2,
@@ -3140,7 +3149,7 @@ class Console {
   error = (...args) => {
     this.#printFunc(
       inspectArgs(args, {
-        ...getConsoleInspectOptions(),
+        ...getConsoleInspectOptions(noColorStderr()),
         indentLevel: this.indentLevel,
       }) + "\n",
       3,
@@ -3353,7 +3362,10 @@ class Console {
   trace = (...args) => {
     const message = inspectArgs(
       args,
-      { ...getConsoleInspectOptions(), indentLevel: 0 },
+      {
+        ...getConsoleInspectOptions(noColorStderr()),
+        indentLevel: 0,
+      },
     );
     const err = {
       name: "Trace",
@@ -3473,10 +3485,11 @@ export {
   formatNumber,
   formatValue,
   getDefaultInspectOptions,
-  getNoColor,
+  getStderrNoColor,
+  getStdoutNoColor,
   inspect,
   inspectArgs,
   quoteString,
-  setNoColorFn,
+  setNoColorFns,
   styles,
 };

filename: runtime/js/99_main.js
patch: @@ -8,7 +8,8 @@ import { core, internals, primordials } from "ext:core/mod.js";
 const ops = core.ops;
 import {
   op_bootstrap_args,
-  op_bootstrap_is_tty,
+  op_bootstrap_is_stderr_tty,
+  op_bootstrap_is_stdout_tty,
   op_bootstrap_no_color,
   op_bootstrap_pid,
   op_main_module,
@@ -62,10 +63,10 @@ import * as timers from "ext:deno_web/02_timers.js";
 import {
   customInspect,
   getDefaultInspectOptions,
-  getNoColor,
+  getStderrNoColor,
   inspectArgs,
   quoteString,
-  setNoColorFn,
+  setNoColorFns,
 } from "ext:deno_console/01_console.js";
 import * as performance from "ext:deno_web/15_performance.js";
 import * as url from "ext:deno_url/00_url.js";
@@ -379,7 +380,10 @@ function importScripts(...urls) {
 
 const opArgs = memoizeLazy(() => op_bootstrap_args());
 const opPid = memoizeLazy(() => op_bootstrap_pid());
-setNoColorFn(() => op_bootstrap_no_color() || !op_bootstrap_is_tty());
+setNoColorFns(
+  () => op_bootstrap_no_color() || !op_bootstrap_is_stdout_tty(),
+  () => op_bootstrap_no_color() || !op_bootstrap_is_stderr_tty(),
+);
 
 function formatException(error) {
   if (
@@ -390,11 +394,11 @@ function formatException(error) {
   } else if (typeof error == "string") {
     return `Uncaught ${
       inspectArgs([quoteString(error, getDefaultInspectOptions())], {
-        colors: !getNoColor(),
+        colors: !getStderrNoColor(),
       })
     }`;
   } else {
-    return `Uncaught ${inspectArgs([error], { colors: !getNoColor() })}`;
+    return `Uncaught ${inspectArgs([error], { colors: !getStderrNoColor() })}`;
   }
 }
 

filename: runtime/ops/bootstrap.rs
patch: @@ -16,7 +16,8 @@ deno_core::extension!(
     op_bootstrap_language,
     op_bootstrap_log_level,
     op_bootstrap_no_color,
-    op_bootstrap_is_tty,
+    op_bootstrap_is_stdout_tty,
+    op_bootstrap_is_stderr_tty,
     op_bootstrap_unstable_args,
     op_snapshot_options,
   ],
@@ -117,7 +118,13 @@ pub fn op_bootstrap_no_color(state: &mut OpState) -> bool {
 }
 
 #[op2(fast)]
-pub fn op_bootstrap_is_tty(state: &mut OpState) -> bool {
+pub fn op_bootstrap_is_stdout_tty(state: &mut OpState) -> bool {
   let options = state.borrow::<BootstrapOptions>();
-  options.is_tty
+  options.is_stdout_tty
+}
+
+#[op2(fast)]
+pub fn op_bootstrap_is_stderr_tty(state: &mut OpState) -> bool {
+  let options = state.borrow::<BootstrapOptions>();
+  options.is_stderr_tty
 }

filename: runtime/worker_bootstrap.rs
patch: @@ -76,7 +76,8 @@ pub struct BootstrapOptions {
   pub location: Option<ModuleSpecifier>,
   /// Sets `Deno.noColor` in JS runtime.
   pub no_color: bool,
-  pub is_tty: bool,
+  pub is_stdout_tty: bool,
+  pub is_stderr_tty: bool,
   // --unstable flag, deprecated
   pub unstable: bool,
   // --unstable-* flags
@@ -109,7 +110,8 @@ impl Default for BootstrapOptions {
       user_agent,
       cpu_count,
       no_color: !colors::use_color(),
-      is_tty: deno_terminal::is_stdout_tty(),
+      is_stdout_tty: deno_terminal::is_stdout_tty(),
+      is_stderr_tty: deno_terminal::is_stderr_tty(),
       enable_op_summary_metrics: Default::default(),
       enable_testing_features: Default::default(),
       log_level: Default::default(),