[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'
)} Path to the global ${'`.vercel`'} directory
-d, --debug Debug mode [off]
--no-color No color mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN'
)} Login token

View File

@@ -33,6 +33,7 @@ const help = () => {
-h, --help Output usage information
-d, --debug Debug mode [off]
--no-color No color mode [off]
-b, --bad Known bad URL
-g, --good Known good URL
-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
--prod Build a production deployment
-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
${chalk.dim('Examples:')}

View File

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

View File

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

View File

@@ -31,6 +31,7 @@ const help = () => {
-h, --help Output usage information
-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]
-t, --token [token] Specify an Authorization Token
-y, --yes Skip questions when setting up new project using default scope and settings

View File

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

View File

@@ -33,6 +33,7 @@ const help = () => {
-h, --help Output usage information
-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
-A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline(
'FILE'

View File

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

View File

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

View File

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

View File

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

View File

@@ -36,6 +36,7 @@ const help = () => {
'DIR'
)} Path to the global ${'`.vercel`'} directory
-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
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -103,7 +103,12 @@ const main = async () => {
}
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;

View File

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

View File

@@ -1,4 +1,4 @@
export const emojiLabels = {
const emojiLabels = {
notice: '📝',
tip: '💡',
warning: '❗️',
@@ -8,6 +8,8 @@ export const emojiLabels = {
locked: '🔒',
} as const;
const stripEmojiRegex = new RegExp(Object.values(emojiLabels).join('|'), 'gi');
export type EmojiLabel = keyof typeof emojiLabels;
export function emoji(label: EmojiLabel) {
@@ -21,3 +23,9 @@ export function prependEmoji(message: string, emoji?: string): string {
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 type { WritableTTY } from '../../types';
import { errorToString } from '@vercel/error-utils';
import { removeEmoji } from '../emoji';
const IS_TEST = process.env.NODE_ENV === 'test';
export interface OutputOptions {
debug?: boolean;
supportsHyperlink?: boolean;
noColor?: boolean;
}
export interface LogOptions {
@@ -25,6 +27,7 @@ export class Output {
stream: WritableTTY;
debugEnabled: boolean;
supportsHyperlink: boolean;
colorDisabled: boolean;
private spinnerMessage: string;
private _spinner: StopSpinner | null;
@@ -33,6 +36,7 @@ export class Output {
{
debug: debugEnabled = false,
supportsHyperlink = detectSupportsHyperlink(stream),
noColor = false,
}: OutputOptions = {}
) {
this.stream = stream;
@@ -40,6 +44,11 @@ export class Output {
this.supportsHyperlink = supportsHyperlink;
this.spinnerMessage = '';
this._spinner = null;
this.colorDisabled = getNoColor(noColor);
if (this.colorDisabled) {
chalk.level = 0;
}
}
isDebugEnabled = () => {
@@ -47,6 +56,9 @@ export class Output {
};
print = (str: string) => {
if (this.colorDisabled) {
str = removeEmoji(str);
}
this.stopSpinner();
this.stream.write(str);
};
@@ -203,3 +215,14 @@ export class Output {
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[]) {
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) {

View File

@@ -1,4 +1,5 @@
import login from '../../../src/commands/login';
import { emoji } from '../../../src/util/emoji';
import { client } from '../../mocks/client';
import { useUser } from '../../mocks/user';
@@ -45,5 +46,115 @@ describe('login', () => {
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);
});
});
});
});