diff --git a/dev-infra/cli.ts b/dev-infra/cli.ts index a220486865..5901dd9fe1 100644 --- a/dev-infra/cli.ts +++ b/dev-infra/cli.ts @@ -13,8 +13,10 @@ import {buildCommitMessageParser} from './commit-message/cli'; import {buildFormatParser} from './format/cli'; import {buildReleaseParser} from './release/cli'; import {buildPrParser} from './pr/cli'; +import {captureLogOutputForCommand} from './utils/console'; yargs.scriptName('ng-dev') + .middleware(captureLogOutputForCommand) .demandCommand() .recommendCommands() .command('commit-message ', '', buildCommitMessageParser) diff --git a/dev-infra/utils/BUILD.bazel b/dev-infra/utils/BUILD.bazel index 3881ac19b0..8bbddafe10 100644 --- a/dev-infra/utils/BUILD.bazel +++ b/dev-infra/utils/BUILD.bazel @@ -12,14 +12,18 @@ ts_library( "@npm//@octokit/graphql", "@npm//@octokit/rest", "@npm//@octokit/types", + "@npm//@types/fs-extra", "@npm//@types/inquirer", "@npm//@types/node", "@npm//@types/shelljs", + "@npm//@types/yargs", "@npm//chalk", + "@npm//fs-extra", "@npm//inquirer", "@npm//inquirer-autocomplete-prompt", "@npm//shelljs", "@npm//tslib", "@npm//typed-graphqlify", + "@npm//yargs", ], ) diff --git a/dev-infra/utils/console.ts b/dev-infra/utils/console.ts index 625cf9189a..d8676c48c4 100644 --- a/dev-infra/utils/console.ts +++ b/dev-infra/utils/console.ts @@ -7,9 +7,13 @@ */ import chalk from 'chalk'; +import {writeFileSync} from 'fs-extra'; import {createPromptModule, ListChoiceOptions, prompt} from 'inquirer'; import * as inquirerAutocomplete from 'inquirer-autocomplete-prompt'; +import {join} from 'path'; +import {Arguments} from 'yargs'; +import {getRepoBaseDir} from './config'; /** Reexport of chalk colors for convenient access. */ export const red: typeof chalk = chalk.red; @@ -140,6 +144,7 @@ function runConsoleCommand(loadCommand: () => Function, logLevel: LOG_LEVELS, .. if (getLogLevel() >= logLevel) { loadCommand()(...text); } + printToLogFile(logLevel, ...text); } /** @@ -155,3 +160,56 @@ function getLogLevel() { } return logLevel; } + +/** 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) => { + LOGGED_TEXT += `Command ran in ${new Date().getTime() - now.getTime()}ms`; + /** Path to the log file location. */ + const logFilePath = join(getRepoBaseDir(), '.ng-dev.log'); + + 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) { + writeFileSync(join(getRepoBaseDir(), `.ng-dev.err-${now.getTime()}.log`), LOGGED_TEXT); + } + }); + + // 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(''); +}