From c0d76bd8914c972bc30cf88c1e8189d6f1f5e99d Mon Sep 17 00:00:00 2001 From: "Adam J. Hines" Date: Sun, 3 Apr 2022 17:30:56 -0600 Subject: [PATCH 1/3] feat(colors): add options for forcing color/no-color This change checks for --color / --no-color as command-line arguments as a mechanism for forcing color in non-TTY situations, or forcing --no-color for TTY. Additionally, this change hooks into the FORCED_COLOR environment variable that is used by the [`supports-color` npm package](https://www.npmjs.com/package/supports-color) that is used by a variety of packages in the node ecosystem. The intention of this PR is to alleviate some of the concerns from Issue #897 by allowing users of turborepo to enable color output on CI runners such as Github Actions which support color output (as mentioned in the [fatih/color documentation](https://github.com/fatih/color/tree/master#github-actions)). With github actions and local builds are both running in color, then the cached output should be consistently in color, compared to the current situation where its a mix of color and non-color output with a remote shared between CI and users building interactively. The FORCED_COLOR environment variable is something that I find myself already setting when running turbo so that the streamed output is in color for the build and testing tools that I am running with it, so it seemed like a natural way to provide this argument without needing to modify the CLI parameters being passed to turbo. In the event multiple methods are being used, the precedence is: * isTTY or not (provided by fatih/color) * $NO_COLOR (if set, then color is disabled) * $FORCED_COLOR ("1", "2", "3", "true" will enable color, "0", or "false" will disable color) * --color/--no-color (the last argument wins if both exist) --- cli/cmd/turbo/main.go | 7 ++++- cli/internal/run/run.go | 4 +-- cli/internal/ui/colors.go | 54 +++++++++++++++++++++++++++++++++++++++ cli/internal/ui/ui.go | 50 +++++++++++++++++++++++++++--------- 4 files changed, 100 insertions(+), 15 deletions(-) create mode 100644 cli/internal/ui/colors.go diff --git a/cli/cmd/turbo/main.go b/cli/cmd/turbo/main.go index 69c994c677f8c..4d6e60467a3dc 100644 --- a/cli/cmd/turbo/main.go +++ b/cli/cmd/turbo/main.go @@ -28,6 +28,7 @@ func main() { traceFile := "" cpuprofileFile := "" argsEnd := 0 + colorMode := uiPkg.GetColorModeFromEnv() for _, arg := range args { switch { case strings.HasPrefix(arg, "--heap="): @@ -36,6 +37,10 @@ func main() { traceFile = arg[len("--trace="):] case strings.HasPrefix(arg, "--cpuprofile="): cpuprofileFile = arg[len("--cpuprofile="):] + case arg == "--color": + colorMode = ui.ColorModeForced + case arg == "--no-color": + colorMode = ui.ColorModeSuppressed default: // Strip any arguments that were handled above args[argsEnd] = arg @@ -44,10 +49,10 @@ func main() { } args = args[:argsEnd] + ui := ui.BuildColoredUi(colorMode); c := cli.NewCLI("turbo", turboVersion) util.InitPrintf() - ui := ui.Default() c.Args = args c.HelpWriter = os.Stdout diff --git a/cli/internal/run/run.go b/cli/internal/run/run.go index 6526ec06926e0..01954ba159c20 100644 --- a/cli/internal/run/run.go +++ b/cli/internal/run/run.go @@ -810,10 +810,10 @@ func replayLogs(logger hclog.Logger, prefixUi cli.Ui, runOptions *RunOptions, lo if outputLogsMode == HashLogs { //Writing to Stdout only the "cache hit, replaying output" line scan.Scan() - prefixUi.Output(ui.StripAnsi(string(scan.Bytes()))) + prefixUi.Output(string(scan.Bytes())) } else { for scan.Scan() { - prefixUi.Output(ui.StripAnsi(string(scan.Bytes()))) //Writing to Stdout + prefixUi.Output(string(scan.Bytes())) //Writing to Stdout } } } diff --git a/cli/internal/ui/colors.go b/cli/internal/ui/colors.go new file mode 100644 index 0000000000000..0fdc2c76e4259 --- /dev/null +++ b/cli/internal/ui/colors.go @@ -0,0 +1,54 @@ +package ui + +import ( + "os" + + "github.com/fatih/color" +) + +type ColorMode int + +const ( + ColorModeUndefined ColorMode = iota + 1 + ColorModeSuppressed + ColorModeForced +) + +func GetColorModeFromEnv() ColorMode { + // The FORCED_COLOR behavior and accepted values are taken from the supports-color NodeJS Package: + // The accepted values as documented are "0" to disable, and "1", "2", or "3" to force-enable color + // at the specified support level (1 = 16 colors, 2 = 256 colors, 3 = 16M colors). + // We don't currently use the level for anything specific, and just treat things as on and off. + // + // Note: while "false" and "true" aren't documented, the library coerces these values to 0 and 1 + // respectively, so that behavior is reproduced here as well. + // https://www.npmjs.com/package/supports-color + + switch forcedColor := os.Getenv("FORCED_COLOR"); { + case forcedColor == "false" || forcedColor == "0": + return ColorModeSuppressed + case forcedColor == "true" || forcedColor == "1" || forcedColor == "2" || forcedColor == "3": + return ColorModeForced + default: + return ColorModeUndefined + } +} + +func applyColorMode(colorMode ColorMode) ColorMode { + switch colorMode { + case ColorModeForced: + color.NoColor = false + case ColorModeSuppressed: + color.NoColor = true + case ColorModeUndefined: + default: + // color.NoColor already gets its default value based on + // isTTY and/or the presence of the NO_COLOR env variable. + } + + if color.NoColor { + return ColorModeSuppressed; + } else { + return ColorModeForced; + } +} diff --git a/cli/internal/ui/ui.go b/cli/internal/ui/ui.go index c4c9852ee0579..b90f25ee26b59 100644 --- a/cli/internal/ui/ui.go +++ b/cli/internal/ui/ui.go @@ -2,6 +2,7 @@ package ui import ( "fmt" + "io" "math" "os" "regexp" @@ -23,13 +24,6 @@ var WARNING_PREFIX = color.New(color.Bold, color.FgYellow, color.ReverseVideo).S var ansiRegex = regexp.MustCompile(ansiEscapeStr) -func StripAnsi(str string) string { - if !IsTTY { - return ansiRegex.ReplaceAllString(str, "") - } - return str -} - // Dim prints out dimmed text func Dim(str string) string { return gray.Sprint(str) @@ -50,9 +44,6 @@ func rgb(i int) (int, int, int) { // Rainbow function returns a formated colorized string ready to print it to the shell/terminal func Rainbow(text string) string { - if !IsTTY { - return text - } var rainbowStr []string for index, value := range text { r, g, b := rgb(index) @@ -63,13 +54,48 @@ func Rainbow(text string) string { return strings.Join(rainbowStr, "") } +type stripAnsiWriter struct { + wrappedWriter io.Writer +} + +func (into *stripAnsiWriter) Write(p []byte) (int, error) { + n, err := into.wrappedWriter.Write(ansiRegex.ReplaceAll(p, []byte{})) + if (err != nil) { + // The number of bytes returned here isn't directly related to the input bytes + // if ansi color codes were being stripped out, but we are counting on Stdout.Write + // not failing under typical operation as well. + return n, err + } + + // Write must return a non-nil error if it returns n < len(p). Consequently, if the + // wrappedWrite.Write call succeeded we will return len(p) as the number of bytes + // written. + return len(p), nil +} + // Default returns the default colored ui func Default() *cli.ColoredUi { + return BuildColoredUi(ColorModeUndefined) +} + +func BuildColoredUi(colorMode ColorMode) *cli.ColoredUi { + colorMode = applyColorMode(colorMode); + + var outWriter, errWriter io.Writer + + if colorMode == ColorModeSuppressed { + outWriter = &stripAnsiWriter{wrappedWriter: os.Stdout} + errWriter = &stripAnsiWriter{wrappedWriter: os.Stderr} + } else { + outWriter = os.Stdout + errWriter = os.Stderr + } + return &cli.ColoredUi{ Ui: &cli.BasicUi{ Reader: os.Stdin, - Writer: os.Stdout, - ErrorWriter: os.Stderr, + Writer: outWriter, + ErrorWriter: errWriter, }, OutputColor: cli.UiColorNone, InfoColor: cli.UiColorNone, From 93d10e5c033ca8141a28582341e085701141fe29 Mon Sep 17 00:00:00 2001 From: "Adam J. Hines" Date: Tue, 5 Apr 2022 16:25:24 -0600 Subject: [PATCH 2/3] chore(ci): enable color in github actions for CI --- .github/workflows/ci-go.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-go.yml b/.github/workflows/ci-go.yml index e83378e208ed5..1c32403e7e82d 100644 --- a/.github/workflows/ci-go.yml +++ b/.github/workflows/ci-go.yml @@ -41,10 +41,10 @@ jobs: run: pnpm install - name: Build & Unit Test - run: pnpm turbo run test --scope=cli + run: pnpm turbo -- run test --scope=cli --color - name: Lint - run: pnpm turbo run lint --scope=cli + run: pnpm turbo -- run lint --scope=cli --color - name: E2E Tests run: | @@ -143,4 +143,6 @@ jobs: - name: Check examples shell: bash + env: + FORCED_COLOR: true run: ./scripts/run-examples.sh From 09a855fbaff260ebb5c56a7772b66e042e66d355 Mon Sep 17 00:00:00 2001 From: "Adam J. Hines" Date: Wed, 6 Apr 2022 08:41:19 -0600 Subject: [PATCH 3/3] docs(color): add docs for --color/--no-color flags --- .../docs/reference/command-line-reference.mdx | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/docs/pages/docs/reference/command-line-reference.mdx b/docs/pages/docs/reference/command-line-reference.mdx index 36437ca8aba12..4809b12de00c9 100644 --- a/docs/pages/docs/reference/command-line-reference.mdx +++ b/docs/pages/docs/reference/command-line-reference.mdx @@ -25,6 +25,46 @@ Boolean options can be enabled as follows: --