2020-05-15 11:19:13 -04:00
|
|
|
/**
|
|
|
|
* @license
|
|
|
|
* Copyright Google LLC All Rights Reserved.
|
|
|
|
*
|
|
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
|
|
* found in the LICENSE file at https://angular.io/license
|
|
|
|
*/
|
|
|
|
|
2020-05-18 18:37:18 -04:00
|
|
|
import chalk from 'chalk';
|
2020-09-11 15:13:36 -04:00
|
|
|
import {writeFileSync} from 'fs';
|
2020-07-29 17:19:15 -04:00
|
|
|
import {createPromptModule, ListChoiceOptions, prompt} from 'inquirer';
|
|
|
|
import * as inquirerAutocomplete from 'inquirer-autocomplete-prompt';
|
2020-08-26 14:38:19 -04:00
|
|
|
import {join} from 'path';
|
|
|
|
import {Arguments} from 'yargs';
|
2020-05-15 11:19:13 -04:00
|
|
|
|
2020-08-26 14:38:19 -04:00
|
|
|
import {getRepoBaseDir} from './config';
|
2020-05-18 18:37:18 -04:00
|
|
|
|
|
|
|
/** Reexport of chalk colors for convenient access. */
|
|
|
|
export const red: typeof chalk = chalk.red;
|
|
|
|
export const green: typeof chalk = chalk.green;
|
|
|
|
export const yellow: typeof chalk = chalk.yellow;
|
2020-08-26 16:49:43 -04:00
|
|
|
export const bold: typeof chalk = chalk.bold;
|
2020-09-09 08:48:36 -04:00
|
|
|
export const blue: typeof chalk = chalk.blue;
|
2020-05-18 18:37:18 -04:00
|
|
|
|
2020-05-15 11:19:13 -04:00
|
|
|
/** Prompts the user with a confirmation question and a specified message. */
|
|
|
|
export async function promptConfirm(message: string, defaultValue = false): Promise<boolean> {
|
|
|
|
return (await prompt<{result: boolean}>({
|
|
|
|
type: 'confirm',
|
|
|
|
name: 'result',
|
|
|
|
message: message,
|
|
|
|
default: defaultValue,
|
|
|
|
}))
|
|
|
|
.result;
|
|
|
|
}
|
2020-05-18 18:37:18 -04:00
|
|
|
|
2020-07-29 17:19:15 -04:00
|
|
|
/** Prompts the user to select an option from a filterable autocomplete list. */
|
|
|
|
export async function promptAutocomplete(
|
|
|
|
message: string, choices: (string|ListChoiceOptions)[]): Promise<string>;
|
|
|
|
/**
|
|
|
|
* Prompts the user to select an option from a filterable autocomplete list, with an option to
|
|
|
|
* choose no value.
|
|
|
|
*/
|
|
|
|
export async function promptAutocomplete(
|
|
|
|
message: string, choices: (string|ListChoiceOptions)[],
|
|
|
|
noChoiceText?: string): Promise<string|false>;
|
|
|
|
export async function promptAutocomplete(
|
|
|
|
message: string, choices: (string|ListChoiceOptions)[],
|
|
|
|
noChoiceText?: string): Promise<string|false> {
|
|
|
|
// Creates a local prompt module with an autocomplete prompt type.
|
|
|
|
const prompt = createPromptModule({}).registerPrompt('autocomplete', inquirerAutocomplete);
|
|
|
|
if (noChoiceText) {
|
|
|
|
choices = [noChoiceText, ...choices];
|
|
|
|
}
|
|
|
|
// `prompt` must be cast as `any` as the autocomplete typings are not available.
|
|
|
|
const result = (await (prompt as any)({
|
|
|
|
type: 'autocomplete',
|
|
|
|
name: 'result',
|
|
|
|
message,
|
|
|
|
source: (_: any, input: string) => {
|
|
|
|
if (!input) {
|
|
|
|
return Promise.resolve(choices);
|
|
|
|
}
|
|
|
|
return Promise.resolve(choices.filter(choice => {
|
|
|
|
if (typeof choice === 'string') {
|
|
|
|
return choice.includes(input);
|
|
|
|
}
|
|
|
|
return choice.name!.includes(input);
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
})).result;
|
|
|
|
if (result === noChoiceText) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Prompts the user for one line of input. */
|
|
|
|
export async function promptInput(message: string): Promise<string> {
|
|
|
|
return (await prompt<{result: string}>({type: 'input', name: 'result', message})).result;
|
|
|
|
}
|
|
|
|
|
2020-05-18 18:37:18 -04:00
|
|
|
/**
|
|
|
|
* Supported levels for logging functions.
|
|
|
|
*
|
|
|
|
* Levels are mapped to numbers to represent a hierarchy of logging levels.
|
|
|
|
*/
|
|
|
|
export enum LOG_LEVELS {
|
|
|
|
SILENT = 0,
|
|
|
|
ERROR = 1,
|
|
|
|
WARN = 2,
|
|
|
|
LOG = 3,
|
|
|
|
INFO = 4,
|
|
|
|
DEBUG = 5,
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Default log level for the tool. */
|
|
|
|
export const DEFAULT_LOG_LEVEL = LOG_LEVELS.INFO;
|
|
|
|
|
|
|
|
/** Write to the console for at INFO logging level */
|
2020-05-20 17:19:10 -04:00
|
|
|
export const info = buildLogLevelFunction(() => console.info, LOG_LEVELS.INFO);
|
2020-05-18 18:37:18 -04:00
|
|
|
|
|
|
|
/** Write to the console for at ERROR logging level */
|
2020-05-20 17:19:10 -04:00
|
|
|
export const error = buildLogLevelFunction(() => console.error, LOG_LEVELS.ERROR);
|
2020-05-18 18:37:18 -04:00
|
|
|
|
|
|
|
/** Write to the console for at DEBUG logging level */
|
2020-05-20 17:19:10 -04:00
|
|
|
export const debug = buildLogLevelFunction(() => console.debug, LOG_LEVELS.DEBUG);
|
2020-05-18 18:37:18 -04:00
|
|
|
|
|
|
|
/** Write to the console for at LOG logging level */
|
2020-05-20 17:19:10 -04:00
|
|
|
// tslint:disable-next-line: no-console
|
|
|
|
export const log = buildLogLevelFunction(() => console.log, LOG_LEVELS.LOG);
|
2020-05-18 18:37:18 -04:00
|
|
|
|
|
|
|
/** Write to the console for at WARN logging level */
|
2020-05-20 17:19:10 -04:00
|
|
|
export const warn = buildLogLevelFunction(() => console.warn, LOG_LEVELS.WARN);
|
|
|
|
|
|
|
|
/** Build an instance of a logging function for the provided level. */
|
|
|
|
function buildLogLevelFunction(loadCommand: () => Function, level: LOG_LEVELS) {
|
|
|
|
/** Write to stdout for the LOG_LEVEL. */
|
|
|
|
const loggingFunction = (...text: string[]) => {
|
|
|
|
runConsoleCommand(loadCommand, level, ...text);
|
|
|
|
};
|
|
|
|
|
|
|
|
/** Start a group at the LOG_LEVEL, optionally starting it as collapsed. */
|
|
|
|
loggingFunction.group = (text: string, collapsed = false) => {
|
|
|
|
const command = collapsed ? console.groupCollapsed : console.group;
|
|
|
|
runConsoleCommand(() => command, level, text);
|
|
|
|
};
|
|
|
|
|
|
|
|
/** End the group at the LOG_LEVEL. */
|
|
|
|
loggingFunction.groupEnd = () => {
|
|
|
|
runConsoleCommand(() => console.groupEnd, level);
|
|
|
|
};
|
|
|
|
|
|
|
|
return loggingFunction;
|
2020-05-18 18:37:18 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Run the console command provided, if the environments logging level greater than the
|
|
|
|
* provided logging level.
|
2020-05-20 17:19:10 -04:00
|
|
|
*
|
|
|
|
* The loadCommand takes in a function which is called to retrieve the console.* function
|
|
|
|
* to allow for jasmine spies to still work in testing. Without this method of retrieval
|
|
|
|
* the console.* function, the function is saved into the closure of the created logging
|
|
|
|
* function before jasmine can spy.
|
2020-05-18 18:37:18 -04:00
|
|
|
*/
|
2020-05-20 17:19:10 -04:00
|
|
|
function runConsoleCommand(loadCommand: () => Function, logLevel: LOG_LEVELS, ...text: string[]) {
|
2020-05-18 18:37:18 -04:00
|
|
|
if (getLogLevel() >= logLevel) {
|
2020-05-20 17:19:10 -04:00
|
|
|
loadCommand()(...text);
|
2020-05-18 18:37:18 -04:00
|
|
|
}
|
2020-08-26 14:38:19 -04:00
|
|
|
printToLogFile(logLevel, ...text);
|
2020-05-18 18:37:18 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieve the log level from environment variables, if the value found
|
|
|
|
* based on the LOG_LEVEL environment variable is undefined, return the default
|
|
|
|
* logging level.
|
|
|
|
*/
|
|
|
|
function getLogLevel() {
|
|
|
|
const logLevelEnvValue: any = (process.env[`LOG_LEVEL`] || '').toUpperCase();
|
|
|
|
const logLevel = LOG_LEVELS[logLevelEnvValue];
|
|
|
|
if (logLevel === undefined) {
|
|
|
|
return DEFAULT_LOG_LEVEL;
|
|
|
|
}
|
|
|
|
return logLevel;
|
|
|
|
}
|
2020-08-26 14:38:19 -04:00
|
|
|
|
|
|
|
/** All text to write to the log file. */
|
|
|
|
let LOGGED_TEXT = '';
|
|
|
|
/** Whether file logging as been enabled. */
|
|
|
|
let FILE_LOGGING_ENABLED = false;
|
|
|
|
/**
|
|
|
|
* The number of columns used in the prepended log level information on each line of the logging
|
|
|
|
* output file.
|
|
|
|
*/
|
|
|
|
const LOG_LEVEL_COLUMNS = 7;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Enable writing the logged outputs to the log file on process exit, sets initial lines from the
|
|
|
|
* command execution, containing information about the timing and command parameters.
|
|
|
|
*
|
|
|
|
* This is expected to be called only once during a command run, and should be called by the
|
|
|
|
* middleware of yargs to enable the file logging before the rest of the command parsing and
|
|
|
|
* response is executed.
|
|
|
|
*/
|
|
|
|
export function captureLogOutputForCommand(argv: Arguments) {
|
|
|
|
if (FILE_LOGGING_ENABLED) {
|
|
|
|
throw Error('`captureLogOutputForCommand` cannot be called multiple times');
|
|
|
|
}
|
|
|
|
/** The date time used for timestamping when the command was invoked. */
|
|
|
|
const now = new Date();
|
|
|
|
/** Header line to separate command runs in log files. */
|
|
|
|
const headerLine = Array(100).fill('#').join('');
|
|
|
|
LOGGED_TEXT += `${headerLine}\nCommand: ${argv.$0} ${argv._.join(' ')}\nRan at: ${now}\n`;
|
|
|
|
|
|
|
|
// On process exit, write the logged output to the appropriate log files
|
|
|
|
process.on('exit', (code: number) => {
|
2021-01-22 12:54:07 -05:00
|
|
|
LOGGED_TEXT += `${headerLine}\n`;
|
|
|
|
LOGGED_TEXT += `Command ran in ${new Date().getTime() - now.getTime()}ms\n`;
|
|
|
|
LOGGED_TEXT += `Exit Code: ${code}\n`;
|
2020-08-26 14:38:19 -04:00
|
|
|
/** Path to the log file location. */
|
|
|
|
const logFilePath = join(getRepoBaseDir(), '.ng-dev.log');
|
|
|
|
|
2020-09-10 11:32:00 -04:00
|
|
|
// Strip ANSI escape codes from log outputs.
|
|
|
|
LOGGED_TEXT = LOGGED_TEXT.replace(/\x1B\[([0-9]{1,3}(;[0-9]{1,2})?)?[mGK]/g, '');
|
|
|
|
|
2020-08-26 14:38:19 -04:00
|
|
|
writeFileSync(logFilePath, LOGGED_TEXT);
|
|
|
|
|
|
|
|
// For failure codes greater than 1, the new logged lines should be written to a specific log
|
|
|
|
// file for the command run failure.
|
|
|
|
if (code > 1) {
|
2021-01-22 12:54:07 -05:00
|
|
|
const logFileName = `.ng-dev.err-${now.getTime()}.log`;
|
|
|
|
console.error(`Exit code: ${code}. Writing full log to ${logFileName}`);
|
|
|
|
writeFileSync(join(getRepoBaseDir(), logFileName), LOGGED_TEXT);
|
2020-08-26 14:38:19 -04:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// Mark file logging as enabled to prevent the function from executing multiple times.
|
|
|
|
FILE_LOGGING_ENABLED = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Write the provided text to the log file, prepending each line with the log level. */
|
|
|
|
function printToLogFile(logLevel: LOG_LEVELS, ...text: string[]) {
|
|
|
|
const logLevelText = `${LOG_LEVELS[logLevel]}:`.padEnd(LOG_LEVEL_COLUMNS);
|
|
|
|
LOGGED_TEXT += text.join(' ').split('\n').map(l => `${logLevelText} ${l}\n`).join('');
|
|
|
|
}
|