[cli] Add --no-color mode (#8826)

Co-authored-by: Sean Massa <EndangeredMassa@gmail.com>
Co-authored-by: Chris Barber <chris.barber@vercel.com>
This commit is contained in:
최지민(Jeemin Choi)
2023-02-07 07:45:17 +09:00
committed by GitHub
parent a585969dd3
commit 76d58673fc
26 changed files with 179 additions and 2 deletions

View File

@@ -32,6 +32,7 @@ const help = () => {
'DIR' 'DIR'
)} Path to the global ${'`.vercel`'} directory )} Path to the global ${'`.vercel`'} directory
-d, --debug Debug mode [off] -d, --debug Debug mode [off]
--no-color No color mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline( -t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN' 'TOKEN'
)} Login token )} Login token

View File

@@ -33,6 +33,7 @@ const help = () => {
-h, --help Output usage information -h, --help Output usage information
-d, --debug Debug mode [off] -d, --debug Debug mode [off]
--no-color No color mode [off]
-b, --bad Known bad URL -b, --bad Known bad URL
-g, --good Known good URL -g, --good Known good URL
-o, --open Automatically open each URL in the browser -o, --open Automatically open each URL in the browser

View File

@@ -118,6 +118,7 @@ const help = () => {
--output [path] Directory where built assets should be written to --output [path] Directory where built assets should be written to
--prod Build a production deployment --prod Build a production deployment
-d, --debug Debug mode [off] -d, --debug Debug mode [off]
--no-color No color mode [off]
-y, --yes Skip the confirmation prompt about pulling environment variables and project settings when not found locally -y, --yes Skip the confirmation prompt about pulling environment variables and project settings when not found locally
${chalk.dim('Examples:')} ${chalk.dim('Examples:')}

View File

@@ -37,6 +37,7 @@ const help = () => {
'DIR' 'DIR'
)} Path to the global ${'`.vercel`'} directory )} Path to the global ${'`.vercel`'} directory
-d, --debug Debug mode [off] -d, --debug Debug mode [off]
--no-color No color mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline( -t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN' 'TOKEN'
)} Login token )} Login token

View File

@@ -53,6 +53,7 @@ export const help = () => `
'DIR' 'DIR'
)} Path to the global ${'`.vercel`'} directory )} Path to the global ${'`.vercel`'} directory
-d, --debug Debug mode [off] -d, --debug Debug mode [off]
--no-color No color mode [off]
-f, --force Force a new deployment even if nothing has changed -f, --force Force a new deployment even if nothing has changed
--with-cache Retain build cache when using "--force" --with-cache Retain build cache when using "--force"
-t ${chalk.underline('TOKEN')}, --token=${chalk.underline( -t ${chalk.underline('TOKEN')}, --token=${chalk.underline(

View File

@@ -31,6 +31,7 @@ const help = () => {
-h, --help Output usage information -h, --help Output usage information
-d, --debug Debug mode [off] -d, --debug Debug mode [off]
--no-color No color mode [off]
-l, --listen [uri] Specify a URI endpoint on which to listen [0.0.0.0:3000] -l, --listen [uri] Specify a URI endpoint on which to listen [0.0.0.0:3000]
-t, --token [token] Specify an Authorization Token -t, --token [token] Specify an Authorization Token
-y, --yes Skip questions when setting up new project using default scope and settings -y, --yes Skip questions when setting up new project using default scope and settings

View File

@@ -33,6 +33,7 @@ const help = () => {
'DIR' 'DIR'
)} Path to the global ${'`.vercel`'} directory )} Path to the global ${'`.vercel`'} directory
-d, --debug Debug mode [off] -d, --debug Debug mode [off]
--no-color No color mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline( -t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN' 'TOKEN'
)} Login token )} Login token

View File

@@ -33,6 +33,7 @@ const help = () => {
-h, --help Output usage information -h, --help Output usage information
-d, --debug Debug mode [off] -d, --debug Debug mode [off]
--no-color No color mode [off]
-f, --force Force a domain on a project and remove it from an existing one -f, --force Force a domain on a project and remove it from an existing one
-A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline( -A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline(
'FILE' 'FILE'

View File

@@ -40,6 +40,7 @@ const help = () => {
'DIR' 'DIR'
)} Path to the global ${'`.vercel`'} directory )} Path to the global ${'`.vercel`'} directory
-d, --debug Debug mode [off] -d, --debug Debug mode [off]
--no-color No color mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline( -t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN' 'TOKEN'
)} Login token )} Login token

View File

@@ -21,6 +21,7 @@ const help = () => {
-h, --help Output usage information -h, --help Output usage information
-d, --debug Debug mode [off] -d, --debug Debug mode [off]
--no-color No color mode [off]
-f, --force Overwrite destination directory if exists [off] -f, --force Overwrite destination directory if exists [off]
${chalk.dim('Examples:')} ${chalk.dim('Examples:')}

View File

@@ -32,6 +32,7 @@ const help = () => {
'TOKEN' 'TOKEN'
)} Login token )} Login token
-d, --debug Debug mode [off] -d, --debug Debug mode [off]
--no-color No color mode [off]
-S, --scope Set a custom scope -S, --scope Set a custom scope
${chalk.dim('Examples:')} ${chalk.dim('Examples:')}

View File

@@ -19,6 +19,7 @@ const help = () => {
'DIR' 'DIR'
)} Path to the global ${'`.vercel`'} directory )} Path to the global ${'`.vercel`'} directory
-d, --debug Debug mode [off] -d, --debug Debug mode [off]
--no-color No color mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline( -t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN' 'TOKEN'
)} Login token )} Login token

View File

@@ -36,6 +36,7 @@ const help = () => {
'DIR' 'DIR'
)} Path to the global ${'`.vercel`'} directory )} Path to the global ${'`.vercel`'} directory
-d, --debug Debug mode [off] -d, --debug Debug mode [off]
--no-color No color mode [off]
-y, --yes Skip questions when setting up new project using default scope and settings -y, --yes Skip questions when setting up new project using default scope and settings
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline( -t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN' 'TOKEN'

View File

@@ -23,6 +23,7 @@ const help = () => {
${chalk.dim('Options:')} ${chalk.dim('Options:')}
-h, --help Output usage information -h, --help Output usage information
--no-color No color mode [off]
-A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline( -A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline(
'FILE' 'FILE'
)} Path to the local ${'`vercel.json`'} file )} Path to the local ${'`vercel.json`'} file

View File

@@ -23,6 +23,7 @@ const help = () => {
'DIR' 'DIR'
)} Path to the global ${'`.vercel`'} directory )} Path to the global ${'`.vercel`'} directory
-d, --debug Debug mode [off] -d, --debug Debug mode [off]
--no-color No color mode [off]
-f, --follow Wait for additional data [off] -f, --follow Wait for additional data [off]
-n ${chalk.bold.underline( -n ${chalk.bold.underline(
'NUMBER' 'NUMBER'

View File

@@ -31,6 +31,7 @@ const help = () => {
'DIR' 'DIR'
)} Path to the global ${'`.vercel`'} directory )} Path to the global ${'`.vercel`'} directory
-d, --debug Debug mode [off] -d, --debug Debug mode [off]
--no-color No color mode [off]
--environment [environment] Deployment environment [development] --environment [environment] Deployment environment [development]
-y, --yes Skip questions when setting up new project using default scope and settings -y, --yes Skip questions when setting up new project using default scope and settings

View File

@@ -39,6 +39,7 @@ const help = () => {
'DIR' 'DIR'
)} Path to the global ${'`.vercel`'} directory )} Path to the global ${'`.vercel`'} directory
-d, --debug Debug mode [off] -d, --debug Debug mode [off]
--no-color No color mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline( -t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN' 'TOKEN'
)} Login token )} Login token

View File

@@ -28,6 +28,7 @@ const help = () => {
'DIR' 'DIR'
)} Path to the global ${'`.vercel`'} directory )} Path to the global ${'`.vercel`'} directory
-d, --debug Debug mode [off] -d, --debug Debug mode [off]
--no-color No color mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline( -t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN' 'TOKEN'
)} Login token )} Login token

View File

@@ -30,6 +30,7 @@ const help = () => {
'DIR' 'DIR'
)} Path to the global ${'`.vercel`'} directory )} Path to the global ${'`.vercel`'} directory
-d, --debug Debug mode [off] -d, --debug Debug mode [off]
--no-color No color mode [off]
-N, --next Show next page of results -N, --next Show next page of results
${chalk.dim('Examples:')} ${chalk.dim('Examples:')}

View File

@@ -19,6 +19,7 @@ const help = () => {
'DIR' 'DIR'
)} Path to the global ${'`.vercel`'} directory )} Path to the global ${'`.vercel`'} directory
-d, --debug Debug mode [off] -d, --debug Debug mode [off]
--no-color No color mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline( -t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN' 'TOKEN'
)} Login token )} Login token

View File

@@ -103,7 +103,12 @@ const main = async () => {
} }
const isDebugging = argv['--debug']; const isDebugging = argv['--debug'];
const output = new Output(process.stderr, { debug: isDebugging }); const isNoColor = argv['--no-color'];
const output = new Output(process.stderr, {
debug: isDebugging,
noColor: isNoColor,
});
debug = output.debug; debug = output.debug;

View File

@@ -8,6 +8,8 @@ const ARG_COMMON = {
'--debug': Boolean, '--debug': Boolean,
'-d': '--debug', '-d': '--debug',
'--no-color': Boolean,
'--token': String, '--token': String,
'-t': '--token', '-t': '--token',

View File

@@ -1,4 +1,4 @@
export const emojiLabels = { const emojiLabels = {
notice: '📝', notice: '📝',
tip: '💡', tip: '💡',
warning: '❗️', warning: '❗️',
@@ -8,6 +8,8 @@ export const emojiLabels = {
locked: '🔒', locked: '🔒',
} as const; } as const;
const stripEmojiRegex = new RegExp(Object.values(emojiLabels).join('|'), 'gi');
export type EmojiLabel = keyof typeof emojiLabels; export type EmojiLabel = keyof typeof emojiLabels;
export function emoji(label: EmojiLabel) { export function emoji(label: EmojiLabel) {
@@ -21,3 +23,9 @@ export function prependEmoji(message: string, emoji?: string): string {
return message; return message;
} }
export function removeEmoji(message: string): string {
const result = message.replace(stripEmojiRegex, '').trimStart();
return result;
}

View File

@@ -5,12 +5,14 @@ import renderLink from './link';
import wait, { StopSpinner } from './wait'; import wait, { StopSpinner } from './wait';
import type { WritableTTY } from '../../types'; import type { WritableTTY } from '../../types';
import { errorToString } from '@vercel/error-utils'; import { errorToString } from '@vercel/error-utils';
import { removeEmoji } from '../emoji';
const IS_TEST = process.env.NODE_ENV === 'test'; const IS_TEST = process.env.NODE_ENV === 'test';
export interface OutputOptions { export interface OutputOptions {
debug?: boolean; debug?: boolean;
supportsHyperlink?: boolean; supportsHyperlink?: boolean;
noColor?: boolean;
} }
export interface LogOptions { export interface LogOptions {
@@ -25,6 +27,7 @@ export class Output {
stream: WritableTTY; stream: WritableTTY;
debugEnabled: boolean; debugEnabled: boolean;
supportsHyperlink: boolean; supportsHyperlink: boolean;
colorDisabled: boolean;
private spinnerMessage: string; private spinnerMessage: string;
private _spinner: StopSpinner | null; private _spinner: StopSpinner | null;
@@ -33,6 +36,7 @@ export class Output {
{ {
debug: debugEnabled = false, debug: debugEnabled = false,
supportsHyperlink = detectSupportsHyperlink(stream), supportsHyperlink = detectSupportsHyperlink(stream),
noColor = false,
}: OutputOptions = {} }: OutputOptions = {}
) { ) {
this.stream = stream; this.stream = stream;
@@ -40,6 +44,11 @@ export class Output {
this.supportsHyperlink = supportsHyperlink; this.supportsHyperlink = supportsHyperlink;
this.spinnerMessage = ''; this.spinnerMessage = '';
this._spinner = null; this._spinner = null;
this.colorDisabled = getNoColor(noColor);
if (this.colorDisabled) {
chalk.level = 0;
}
} }
isDebugEnabled = () => { isDebugEnabled = () => {
@@ -47,6 +56,9 @@ export class Output {
}; };
print = (str: string) => { print = (str: string) => {
if (this.colorDisabled) {
str = removeEmoji(str);
}
this.stopSpinner(); this.stopSpinner();
this.stream.write(str); this.stream.write(str);
}; };
@@ -203,3 +215,14 @@ export class Output {
return ansiEscapes.link(chalk.cyan(text), url); return ansiEscapes.link(chalk.cyan(text), url);
}; };
} }
function getNoColor(noColorArg: boolean | undefined): boolean {
// FORCE_COLOR: the standard supported by chalk https://github.com/chalk/chalk#supportscolor
// NO_COLOR: the standard we want to support https://no-color.org/
// noColorArg: the `--no-color` arg passed to the CLI command
const noColor =
process.env.FORCE_COLOR === '0' ||
process.env.NO_COLOR === '1' ||
noColorArg;
return !!noColor;
}

View File

@@ -132,6 +132,14 @@ export class MockClient extends Client {
setArgv(...argv: string[]) { setArgv(...argv: string[]) {
this.argv = [process.execPath, 'cli.js', ...argv]; this.argv = [process.execPath, 'cli.js', ...argv];
this.output = new Output(this.stderr, {
debug: argv.includes('--debug') || argv.includes('-d'),
noColor: argv.includes('--no-color'),
});
}
resetOutput() {
this.output = new Output(this.stderr);
} }
useScenario(scenario: Scenario) { useScenario(scenario: Scenario) {

View File

@@ -1,4 +1,5 @@
import login from '../../../src/commands/login'; import login from '../../../src/commands/login';
import { emoji } from '../../../src/util/emoji';
import { client } from '../../mocks/client'; import { client } from '../../mocks/client';
import { useUser } from '../../mocks/user'; import { useUser } from '../../mocks/user';
@@ -45,5 +46,115 @@ describe('login', () => {
await expect(exitCodePromise).resolves.toEqual(0); await expect(exitCodePromise).resolves.toEqual(0);
}); });
it('should allow the `--no-color` flag', async () => {
const user = useUser();
client.setArgv('login', '--no-color');
const exitCodePromise = login(client);
await expect(client.stderr).toOutput(`> Log in to Vercel`);
// Move down to "Email" option
client.stdin.write('\x1B[B'); // Down arrow
client.stdin.write('\x1B[B'); // Down arrow
client.stdin.write('\x1B[B'); // Down arrow
client.stdin.write('\r'); // Return key
await expect(client.stderr).toOutput('> Enter your email address:');
client.stdin.write(`${user.email}\n`);
await expect(client.stderr).toOutput(
`Success! Email authentication complete for ${user.email}`
);
await expect(client.stderr).not.toOutput(emoji('tip'));
await expect(exitCodePromise).resolves.toEqual(0);
});
describe('with NO_COLOR="1" env var', () => {
let previousNoColor: string | undefined;
beforeEach(() => {
previousNoColor = process.env.NO_COLOR;
process.env.NO_COLOR = '1';
});
afterEach(() => {
delete process.env.NO_COLOR;
if (previousNoColor) {
process.env.NO_COLOR = previousNoColor;
}
});
it('should remove emoji the `NO_COLOR` env var with 1', async () => {
client.resetOutput();
const user = useUser();
client.setArgv('login');
const exitCodePromise = login(client);
await expect(client.stderr).toOutput(`> Log in to Vercel`);
// Move down to "Email" option
client.stdin.write('\x1B[B'); // Down arrow
client.stdin.write('\x1B[B'); // Down arrow
client.stdin.write('\x1B[B'); // Down arrow
client.stdin.write('\r'); // Return key
await expect(client.stderr).toOutput('> Enter your email address:');
client.stdin.write(`${user.email}\n`);
await expect(client.stderr).toOutput(
`Success! Email authentication complete for ${user.email}`
);
await expect(client.stderr).not.toOutput(emoji('tip'));
await expect(exitCodePromise).resolves.toEqual(0);
});
});
describe('with FORCE_COLOR="0" env var', () => {
let previousForceColor: string | undefined;
beforeEach(() => {
previousForceColor = process.env.FORCE_COLOR;
process.env.FORCE_COLOR = '0';
});
afterEach(() => {
delete process.env.FORCE_COLOR;
if (previousForceColor) {
process.env.FORCE_COLOR = previousForceColor;
}
});
it('should remove emoji the `FORCE_COLOR` env var with 0', async () => {
client.resetOutput();
const user = useUser();
client.setArgv('login');
const exitCodePromise = login(client);
await expect(client.stderr).toOutput(`> Log in to Vercel`);
// Move down to "Email" option
client.stdin.write('\x1B[B'); // Down arrow
client.stdin.write('\x1B[B'); // Down arrow
client.stdin.write('\x1B[B'); // Down arrow
client.stdin.write('\r'); // Return key
await expect(client.stderr).toOutput('> Enter your email address:');
client.stdin.write(`${user.email}\n`);
await expect(client.stderr).toOutput(
`Success! Email authentication complete for ${user.email}`
);
await expect(client.stderr).not.toOutput(emoji('tip'));
await expect(exitCodePromise).resolves.toEqual(0);
});
});
}); });
}); });