code:
import ansiStyles from 'ansi-styles';
import isFullwidthCodePoint from 'is-fullwidth-code-point';

// \x1b and \x9b
const ESCAPES = new Set([27, 155]);

const CODE_POINT_0 = '0'.codePointAt(0);
const CODE_POINT_9 = '9'.codePointAt(0);

const endCodesSet = new Set();
const endCodesMap = new Map();
for (const [start, end] of ansiStyles.codes) {
	endCodesSet.add(ansiStyles.color.ansi(end));
	endCodesMap.set(ansiStyles.color.ansi(start), ansiStyles.color.ansi(end));
}

function getEndCode(code) {
	if (endCodesSet.has(code)) {
		return code;
	}

	if (endCodesMap.has(code)) {
		return endCodesMap.get(code);
	}

	code = code.slice(2);
	if (code.includes(';')) {
		code = code[0] + '0';
	}

	const returnValue = ansiStyles.codes.get(Number.parseInt(code, 10));
	if (returnValue) {
		return ansiStyles.color.ansi(returnValue);
	}

	return ansiStyles.reset.open;
}

function findNumberIndex(string) {
	for (let index = 0; index < string.length; index++) {
		const codePoint = string.codePointAt(index);
		if (codePoint >= CODE_POINT_0 && codePoint <= CODE_POINT_9) {
			return index;
		}
	}

	return -1;
}

function parseAnsiCode(string, offset) {
	string = string.slice(offset, offset + 19);
	const startIndex = findNumberIndex(string);
	if (startIndex !== -1) {
		let endIndex = string.indexOf('m', startIndex);
		if (endIndex === -1) {
			endIndex = string.length;
		}

		return string.slice(0, endIndex + 1);
	}
}

function tokenize(string, endCharacter = Number.POSITIVE_INFINITY) {
	const returnValue = [];

	let index = 0;
	let visibleCount = 0;
	while (index < string.length) {
		const codePoint = string.codePointAt(index);

		if (ESCAPES.has(codePoint)) {
			const code = parseAnsiCode(string, index);
			if (code) {
				returnValue.push({
					type: 'ansi',
					code,
					endCode: getEndCode(code),
				});
				index += code.length;
				continue;
			}
		}

		const isFullWidth = isFullwidthCodePoint(codePoint);
		const character = String.fromCodePoint(codePoint);

		returnValue.push({
			type: 'character',
			value: character,
			isFullWidth,
		});

		index += character.length;
		visibleCount += isFullWidth ? 2 : character.length;

		if (visibleCount >= endCharacter) {
			break;
		}
	}

	return returnValue;
}

function reduceAnsiCodes(codes) {
	let returnValue = [];

	for (const code of codes) {
		if (code.code === ansiStyles.reset.open) {
			// Reset code, disable all codes
			returnValue = [];
		} else if (endCodesSet.has(code.code)) {
			// This is an end code, disable all matching start codes
			returnValue = returnValue.filter(returnValueCode => returnValueCode.endCode !== code.code);
		} else {
			// This is a start code. Disable all styles this "overrides", then enable it
			returnValue = returnValue.filter(returnValueCode => returnValueCode.endCode !== code.endCode);
			returnValue.push(code);
		}
	}

	return returnValue;
}

function undoAnsiCodes(codes) {
	const reduced = reduceAnsiCodes(codes);
	const endCodes = reduced.map(({endCode}) => endCode);
	return endCodes.reverse().join('');
}

export default function sliceAnsi(string, start, end) {
	const tokens = tokenize(string, end);
	let activeCodes = [];
	let position = 0;
	let returnValue = '';
	let include = false;

	for (const token of tokens) {
		if (end !== undefined && position >= end) {
			break;
		}

		if (token.type === 'ansi') {
			activeCodes.push(token);
			if (include) {
				returnValue += token.code;
			}
		} else {
			// Character
			if (!include && position >= start) {
				include = true;
				// Simplify active codes
				activeCodes = reduceAnsiCodes(activeCodes);
				returnValue = activeCodes.map(({code}) => code).join('');
			}

			if (include) {
				returnValue += token.value;
			}

			position += token.isFullWidth ? 2 : token.value.length;
		}
	}

	// Disable active codes at the end
	returnValue += undoAnsiCodes(activeCodes);
	return returnValue;
}


slice-ansi

description: Slice a string with ANSI escape codes

license: MIT

repository: chalk/slice-ansi

type: module

test: xo && ava && tsc index.d.ts

keywords: slice string ansi styles color colour colors terminal console cli tty escape formatting rgb 256 shell xterm log logging command-line text

license: mit


code:
# slice-ansi [![XO: Linted](https://img.shields.io/badge/xo-linted-blue.svg)](https://github.com/xojs/xo)

> Slice a string with [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors_and_Styles)

## Install

```sh
npm install slice-ansi
```

## Usage

```js
import chalk from 'chalk';
import sliceAnsi from 'slice-ansi';

const string = 'The quick brown ' + chalk.red('fox jumped over ') +
	'the lazy ' + chalk.green('dog and then ran away with the unicorn.');

console.log(sliceAnsi(string, 20, 30));
```

## API

### sliceAnsi(string, startSlice, endSlice?)

#### string

Type: `string`

String with ANSI escape codes. Like one styled by [`chalk`](https://github.com/chalk/chalk).

#### startSlice

Type: `number`

Zero-based index at which to start the slice.

#### endSlice

Type: `number`

Zero-based index at which to end the slice.

## Related

- [wrap-ansi](https://github.com/chalk/wrap-ansi) - Wordwrap a string with ANSI escape codes
- [cli-truncate](https://github.com/sindresorhus/cli-truncate) - Truncate a string to a specific width in the terminal
- [chalk](https://github.com/chalk/chalk) - Terminal string styling done right

## Maintainers

- [Sindre Sorhus](https://github.com/sindresorhus)
- [Josh Junon](https://github.com/qix-)


code:
export type Options = {
	/**
	Count [ambiguous width characters](https://www.unicode.org/reports/tr11/#Ambiguous) as having narrow width (count of 1) instead of wide width (count of 2).

	@default true

	> Ambiguous characters behave like wide or narrow characters depending on the context (language tag, script identification, associated font, source of data, or explicit markup; all can provide the context). __If the context cannot be established reliably, they should be treated as narrow characters by default.__
	> - http://www.unicode.org/reports/tr11/
	*/
	readonly ambiguousIsNarrow?: boolean;

	/**
	Whether [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code) should be counted.

	@default false
	*/
	readonly countAnsiEscapeCodes?: boolean;
};

/**
Get the visual width of a string - the number of columns required to display it.

Some Unicode characters are [fullwidth](https://en.wikipedia.org/wiki/Halfwidth_and_fullwidth_forms) and use double the normal width. [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code) are stripped and doesn't affect the width.

@example
```
import stringWidth from 'string-width';

stringWidth('a');
//=> 1

stringWidth('古');
//=> 2

stringWidth('\u001B[1m古\u001B[22m');
//=> 2
```
*/
export default function stringWidth(string: string, options?: Options): number;


code:
import stripAnsi from 'strip-ansi';
import {eastAsianWidth} from 'get-east-asian-width';
import emojiRegex from 'emoji-regex';

const segmenter = new Intl.Segmenter();

const defaultIgnorableCodePointRegex = /^\p{Default_Ignorable_Code_Point}$/u;

export default function stringWidth(string, options = {}) {
	if (typeof string !== 'string' || string.length === 0) {
		return 0;
	}

	const {
		ambiguousIsNarrow = true,
		countAnsiEscapeCodes = false,
	} = options;

	if (!countAnsiEscapeCodes) {
		string = stripAnsi(string);
	}

	if (string.length === 0) {
		return 0;
	}

	let width = 0;
	const eastAsianWidthOptions = {ambiguousAsWide: !ambiguousIsNarrow};

	for (const {segment: character} of segmenter.segment(string)) {
		const codePoint = character.codePointAt(0);

		// Ignore control characters
		if (codePoint <= 0x1F || (codePoint >= 0x7F && codePoint <= 0x9F)) {
			continue;
		}

		// Ignore zero-width characters
		if (
			(codePoint >= 0x20_0B && codePoint <= 0x20_0F) // Zero-width space, non-joiner, joiner, left-to-right mark, right-to-left mark
			|| codePoint === 0xFE_FF // Zero-width no-break space
		) {
			continue;
		}

		// Ignore combining characters
		if (
			(codePoint >= 0x3_00 && codePoint <= 0x3_6F) // Combining diacritical marks
			|| (codePoint >= 0x1A_B0 && codePoint <= 0x1A_FF) // Combining diacritical marks extended
			|| (codePoint >= 0x1D_C0 && codePoint <= 0x1D_FF) // Combining diacritical marks supplement
			|| (codePoint >= 0x20_D0 && codePoint <= 0x20_FF) // Combining diacritical marks for symbols
			|| (codePoint >= 0xFE_20 && codePoint <= 0xFE_2F) // Combining half marks
		) {
			continue;
		}

		// Ignore surrogate pairs
		if (codePoint >= 0xD8_00 && codePoint <= 0xDF_FF) {
			continue;
		}

		// Ignore variation selectors
		if (codePoint >= 0xFE_00 && codePoint <= 0xFE_0F) {
			continue;
		}

		// This covers some of the above cases, but we still keep them for performance reasons.
		if (defaultIgnorableCodePointRegex.test(character)) {
			continue;
		}

		// TODO: Use `/\p{RGI_Emoji}/v` when targeting Node.js 20.
		if (emojiRegex().test(character)) {
			width += 2;
			continue;
		}

		width += eastAsianWidth(codePoint, eastAsianWidthOptions);
	}

	return width;
}


string-width

description: Get the visual width of a string - the number of columns required to display it

repository: sindresorhus/string-width

test: xo && ava && tsd

keywords: string character unicode width visual column columns fullwidth full-width full ansi escape codes cli command-line terminal console cjk chinese japanese korean fixed-width east-asian-width


code:
# string-width

> Get the visual width of a string - the number of columns required to display it

Some Unicode characters are [fullwidth](https://en.wikipedia.org/wiki/Halfwidth_and_fullwidth_forms) and use double the normal width. [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code) are stripped and doesn't affect the width.

Useful to be able to measure the actual width of command-line output.

## Install

```sh
npm install string-width
```

## Usage

```js
import stringWidth from 'string-width';

stringWidth('a');
//=> 1

stringWidth('古');
//=> 2

stringWidth('\u001B[1m古\u001B[22m');
//=> 2
```

## API

### stringWidth(string, options?)

#### string

Type: `string`

The string to be counted.

#### options

Type: `object`

##### ambiguousIsNarrow

Type: `boolean`\
Default: `true`

Count [ambiguous width characters](https://www.unicode.org/reports/tr11/#Ambiguous) as having narrow width (count of 1) instead of wide width (count of 2).

> Ambiguous characters behave like wide or narrow characters depending on the context (language tag, script identification, associated font, source of data, or explicit markup; all can provide the context). **If the context cannot be established reliably, they should be treated as narrow characters by default.**
> - http://www.unicode.org/reports/tr11/

##### countAnsiEscapeCodes

Type: `boolean`\
Default: `false`

Whether [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code) should be counted.

## Related

- [string-width-cli](https://github.com/sindresorhus/string-width-cli) - CLI for this module
- [string-length](https://github.com/sindresorhus/string-length) - Get the real length of a string
- [widest-line](https://github.com/sindresorhus/widest-line) - Get the visual width of the widest line in a string
- [get-east-asian-width](https://github.com/sindresorhus/get-east-asian-width) - Determine the East Asian Width of a Unicode character


code:
/**
Strip [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code) from a string.

@example
```
import stripAnsi from 'strip-ansi';

stripAnsi('\u001B[4mUnicorn\u001B[0m');
//=> 'Unicorn'

stripAnsi('\u001B]8;;https://github.com\u0007Click\u001B]8;;\u0007');
//=> 'Click'
```
*/
export default function stripAnsi(string: string): string;


code:
import ansiRegex from 'ansi-regex';

const regex = ansiRegex();

export default function stripAnsi(string) {
	if (typeof string !== 'string') {
		throw new TypeError(`Expected a \`string\`, got \`${typeof string}\``);
	}

	// Even though the regex is global, we don't need to reset the `.lastIndex`
	// because unlike `.exec()` and `.test()`, `.replace()` does it automatically
	// and doing it manually has a performance penalty.
	return string.replace(regex, '');
}


strip-ansi

description: Strip ANSI escape codes from a string

repository: chalk/strip-ansi

keywords: strip trim remove ansi styles color colour colors terminal console string tty escape formatting rgb 256 shell xterm log logging command-line text


code:
# strip-ansi

> Strip [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code) from a string

## Install

```
$ npm install strip-ansi
```

## Usage

```js
import stripAnsi from 'strip-ansi';

stripAnsi('\u001B[4mUnicorn\u001B[0m');
//=> 'Unicorn'

stripAnsi('\u001B]8;;https://github.com\u0007Click\u001B]8;;\u0007');
//=> 'Click'
```

## strip-ansi for enterprise

Available as part of the Tidelift Subscription.

The maintainers of strip-ansi and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. [Learn more.](https://tidelift.com/subscription/pkg/npm-strip-ansi?utm_source=npm-strip-ansi&utm_medium=referral&utm_campaign=enterprise&utm_term=repo)

## Related

- [strip-ansi-cli](https://github.com/chalk/strip-ansi-cli) - CLI for this module
- [strip-ansi-stream](https://github.com/chalk/strip-ansi-stream) - Streaming version of this module
- [has-ansi](https://github.com/chalk/has-ansi) - Check if a string has ANSI escape codes
- [ansi-regex](https://github.com/chalk/ansi-regex) - Regular expression for matching ANSI escape codes
- [chalk](https://github.com/chalk/chalk) - Terminal string styling done right

## Maintainers

- [Sindre Sorhus](https://github.com/sindresorhus)
- [Josh Junon](https://github.com/qix-)


code:
export type Options = {
	/**
	By default the wrap is soft, meaning long words may extend past the column width. Setting this to `true` will make it hard wrap at the column width.

	@default false
	*/
	readonly hard?: boolean;

	/**
	By default, an attempt is made to split words at spaces, ensuring that they don't extend past the configured columns. If wordWrap is `false`, each column will instead be completely filled splitting words as necessary.

	@default true
	*/
	readonly wordWrap?: boolean;

	/**
	Whitespace on all lines is removed by default. Set this option to `false` if you don't want to trim.

	@default true
	*/
	readonly trim?: boolean;
};

/**
Wrap words to the specified column width.

@param string - A string with ANSI escape codes, like one styled by [`chalk`](https://github.com/chalk/chalk). Newline characters will be normalized to `\n`.
@param columns - The number of columns to wrap the text to.

@example
```
import chalk from 'chalk';
import wrapAnsi from 'wrap-ansi';

const input = 'The quick brown ' + chalk.red('fox jumped over ') +
	'the lazy ' + chalk.green('dog and then ran away with the unicorn.');

console.log(wrapAnsi(input, 20));
```
*/
export default function wrapAnsi(string: string, columns: number, options?: Options): string;


code:
import stringWidth from 'string-width';
import stripAnsi from 'strip-ansi';
import ansiStyles from 'ansi-styles';

const ESCAPES = new Set([
	'\u001B',
	'\u009B',
]);

const END_CODE = 39;
const ANSI_ESCAPE_BELL = '\u0007';
const ANSI_CSI = '[';
const ANSI_OSC = ']';
const ANSI_SGR_TERMINATOR = 'm';
const ANSI_ESCAPE_LINK = `${ANSI_OSC}8;;`;

const wrapAnsiCode = code => `${ESCAPES.values().next().value}${ANSI_CSI}${code}${ANSI_SGR_TERMINATOR}`;
const wrapAnsiHyperlink = url => `${ESCAPES.values().next().value}${ANSI_ESCAPE_LINK}${url}${ANSI_ESCAPE_BELL}`;

// Calculate the length of words split on ' ', ignoring
// the extra characters added by ansi escape codes
const wordLengths = string => string.split(' ').map(character => stringWidth(character));

// Wrap a long word across multiple rows
// Ansi escape codes do not count towards length
const wrapWord = (rows, word, columns) => {
	const characters = [...word];

	let isInsideEscape = false;
	let isInsideLinkEscape = false;
	let visible = stringWidth(stripAnsi(rows.at(-1)));

	for (const [index, character] of characters.entries()) {
		const characterLength = stringWidth(character);

		if (visible + characterLength <= columns) {
			rows[rows.length - 1] += character;
		} else {
			rows.push(character);
			visible = 0;
		}

		if (ESCAPES.has(character)) {
			isInsideEscape = true;

			const ansiEscapeLinkCandidate = characters.slice(index + 1, index + 1 + ANSI_ESCAPE_LINK.length).join('');
			isInsideLinkEscape = ansiEscapeLinkCandidate === ANSI_ESCAPE_LINK;
		}

		if (isInsideEscape) {
			if (isInsideLinkEscape) {
				if (character === ANSI_ESCAPE_BELL) {
					isInsideEscape = false;
					isInsideLinkEscape = false;
				}
			} else if (character === ANSI_SGR_TERMINATOR) {
				isInsideEscape = false;
			}

			continue;
		}

		visible += characterLength;

		if (visible === columns && index < characters.length - 1) {
			rows.push('');
			visible = 0;
		}
	}

	// It's possible that the last row we copy over is only
	// ansi escape characters, handle this edge-case
	if (!visible && rows.at(-1).length > 0 && rows.length > 1) {
		rows[rows.length - 2] += rows.pop();
	}
};

// Trims spaces from a string ignoring invisible sequences
const stringVisibleTrimSpacesRight = string => {
	const words = string.split(' ');
	let last = words.length;

	while (last > 0) {
		if (stringWidth(words[last - 1]) > 0) {
			break;
		}

		last--;
	}

	if (last === words.length) {
		return string;
	}

	return words.slice(0, last).join(' ') + words.slice(last).join('');
};

// The wrap-ansi module can be invoked in either 'hard' or 'soft' wrap mode.
//
// 'hard' will never allow a string to take up more than columns characters.
//
// 'soft' allows long words to expand past the column length.
const exec = (string, columns, options = {}) => {
	if (options.trim !== false && string.trim() === '') {
		return '';
	}

	let returnValue = '';
	let escapeCode;
	let escapeUrl;

	const lengths = wordLengths(string);
	let rows = [''];

	for (const [index, word] of string.split(' ').entries()) {
		if (options.trim !== false) {
			rows[rows.length - 1] = rows.at(-1).trimStart();
		}

		let rowLength = stringWidth(rows.at(-1));

		if (index !== 0) {
			if (rowLength >= columns && (options.wordWrap === false || options.trim === false)) {
				// If we start with a new word but the current row length equals the length of the columns, add a new row
				rows.push('');
				rowLength = 0;
			}

			if (rowLength > 0 || options.trim === false) {
				rows[rows.length - 1] += ' ';
				rowLength++;
			}
		}

		// In 'hard' wrap mode, the length of a line is never allowed to extend past 'columns'
		if (options.hard && lengths[index] > columns) {
			const remainingColumns = (columns - rowLength);
			const breaksStartingThisLine = 1 + Math.floor((lengths[index] - remainingColumns - 1) / columns);
			const breaksStartingNextLine = Math.floor((lengths[index] - 1) / columns);
			if (breaksStartingNextLine < breaksStartingThisLine) {
				rows.push('');
			}

			wrapWord(rows, word, columns);
			continue;
		}

		if (rowLength + lengths[index] > columns && rowLength > 0 && lengths[index] > 0) {
			if (options.wordWrap === false && rowLength < columns) {
				wrapWord(rows, word, columns);
				continue;
			}

			rows.push('');
		}

		if (rowLength + lengths[index] > columns && options.wordWrap === false) {
			wrapWord(rows, word, columns);
			continue;
		}

		rows[rows.length - 1] += word;
	}

	if (options.trim !== false) {
		rows = rows.map(row => stringVisibleTrimSpacesRight(row));
	}

	const preString = rows.join('\n');
	const pre = [...preString];

	// We need to keep a separate index as `String#slice()` works on Unicode code units, while `pre` is an array of codepoints.
	let preStringIndex = 0;

	for (const [index, character] of pre.entries()) {
		returnValue += character;

		if (ESCAPES.has(character)) {
			const {groups} = new RegExp(`(?:\\${ANSI_CSI}(?<code>\\d+)m|\\${ANSI_ESCAPE_LINK}(?<uri>.*)${ANSI_ESCAPE_BELL})`).exec(preString.slice(preStringIndex)) || {groups: {}};
			if (groups.code !== undefined) {
				const code = Number.parseFloat(groups.code);
				escapeCode = code === END_CODE ? undefined : code;
			} else if (groups.uri !== undefined) {
				escapeUrl = groups.uri.length === 0 ? undefined : groups.uri;
			}
		}

		const code = ansiStyles.codes.get(Number(escapeCode));

		if (pre[index + 1] === '\n') {
			if (escapeUrl) {
				returnValue += wrapAnsiHyperlink('');
			}

			if (escapeCode && code) {
				returnValue += wrapAnsiCode(code);
			}
		} else if (character === '\n') {
			if (escapeCode && code) {
				returnValue += wrapAnsiCode(escapeCode);
			}

			if (escapeUrl) {
				returnValue += wrapAnsiHyperlink(escapeUrl);
			}
		}

		preStringIndex += character.length;
	}

	return returnValue;
};

// For each newline, invoke the method separately
export default function wrapAnsi(string, columns, options) {
	return String(string)
		.normalize()
		.replaceAll('\r\n', '\n')
		.split('\n')
		.map(line => exec(line, columns, options))
		.join('\n');
}


wrap-ansi

description: Wordwrap a string with ANSI escape codes

repository: chalk/wrap-ansi

test: xo && nyc ava && tsd

keywords: wrap break wordwrap wordbreak linewrap ansi styles color colour colors terminal console cli string tty escape formatting rgb 256 shell xterm log logging command-line text


code:
# wrap-ansi

> Wordwrap a string with [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors_and_Styles)

## Install

```sh
npm install wrap-ansi
```

## Usage

```js
import chalk from 'chalk';
import wrapAnsi from 'wrap-ansi';

const input = 'The quick brown ' + chalk.red('fox jumped over ') +
	'the lazy ' + chalk.green('dog and then ran away with the unicorn.');

console.log(wrapAnsi(input, 20));
```

<img width="331" src="screenshot.png">

## API

### wrapAnsi(string, columns, options?)

Wrap words to the specified column width.

#### string

Type: `string`

A string with ANSI escape codes, like one styled by [`chalk`](https://github.com/chalk/chalk).

Newline characters will be normalized to `\n`.

#### columns

Type: `number`

The number of columns to wrap the text to.

#### options

Type: `object`

##### hard

Type: `boolean`\
Default: `false`

By default the wrap is soft, meaning long words may extend past the column width. Setting this to `true` will make it hard wrap at the column width.

##### wordWrap

Type: `boolean`\
Default: `true`

By default, an attempt is made to split words at spaces, ensuring that they don't extend past the configured columns. If wordWrap is `false`, each column will instead be completely filled splitting words as necessary.

##### trim

Type: `boolean`\
Default: `true`

Whitespace on all lines is removed by default. Set this option to `false` if you don't want to trim.

## Related

- [slice-ansi](https://github.com/chalk/slice-ansi) - Slice a string with ANSI escape codes
- [cli-truncate](https://github.com/sindresorhus/cli-truncate) - Truncate a string to a specific width in the terminal
- [chalk](https://github.com/chalk/chalk) - Terminal string styling done right
- [jsesc](https://github.com/mathiasbynens/jsesc) - Generate ASCII-only output from Unicode strings. Useful for creating test fixtures.


log-update

description: Log by overwriting the previous output in the terminal. Useful for rendering progress bars, animations, etc.

repository: sindresorhus/log-update

keywords: log logger logging cli terminal term console shell update refresh overwrite output stdout progress bar animation

@typescript-eslint/no-unsafe-argument: off


code:
# log-update

> Log by overwriting the previous output in the terminal.\
> Useful for rendering progress bars, animations, etc.

![](screenshot.gif)

## Install

```sh
npm install log-update
```

## Usage

```js
import logUpdate from 'log-update';

const frames = ['-', '\\', '|', '/'];
let index = 0;

setInterval(() => {
	const frame = frames[index = ++index % frames.length];

	logUpdate(
`
        ♥♥
   ${frame} unicorns ${frame}
        ♥♥
`
	);
}, 80);
```

## API

### logUpdate(text…)

Log to stdout.

### logUpdate.clear()

Clear the logged output.

### logUpdate.done()

Persist the logged output.

Useful if you want to start a new log session below the current one.

### logUpdateStderr(text…)

Log to stderr.

### logUpdateStderr.clear()
### logUpdateStderr.done()

### createLogUpdate(stream, options?)

Get a `logUpdate` method that logs to the specified stream.

#### options

Type: `object`

##### showCursor

Type: `boolean`\
Default: `false`

Show the cursor. This can be useful when a CLI accepts input from a user.

```js
import {createLogUpdate} from 'log-update';

// Write output but don't hide the cursor
const log = createLogUpdate(process.stdout, {
	showCursor: true
});
```

## Examples

- [listr](https://github.com/SamVerschueren/listr) - Uses this module to render an interactive task list
- [ora](https://github.com/sindresorhus/ora) - Uses this module to render awesome spinners
- [speed-test](https://github.com/sindresorhus/speed-test) - Uses this module to render a [spinner](https://github.com/sindresorhus/elegant-spinner)


code:
# log4js-node Changelog

## [6.9.1](https://github.com/log4js-node/log4js-node/milestone/94)

- [fix(7922e82): regex for stacktrace](https://github.com/log4js-node/log4js-node/pull/1378) - thanks [@lamweili](https://github.com/lamweili)
  - addresses [#1377](https://github.com/log4js-node/log4js-node/issues/1377) which has a regression since [6.8.0](https://github.com/log4js-node/log4js-node/milestone/92) from [#1363](https://github.com/log4js-node/log4js-node/pull/1363) at commit [7922e82](https://github.com/log4js-node/log4js-node/commit/7922e8257806811666c1d4cf6774b4426276ea16)

## [6.9.0](https://github.com/log4js-node/log4js-node/milestone/93)

- [feat: support for idempotent logging on browser](https://github.com/log4js-node/log4js-node/pull/1374) - thanks [@aellerton](https://github.com/aellerton)
  - addresses [#968](https://github.com/log4js-node/log4js-node/issues/968), [#1270](https://github.com/log4js-node/log4js-node/issues/1270), [#1288](https://github.com/log4js-node/log4js-node/issues/1288), [#1372](https://github.com/log4js-node/log4js-node/issues/1372)
- [docs: added that `log4js.getLogger()` may call `log4js.configure()`](https://github.com/log4js-node/log4js-node/pull/1375) - thanks [@lamweili](https://github.com/lamweili)

## [6.8.0](https://github.com/log4js-node/log4js-node/milestone/92)

- [feat: added `log4js.isConfigured()` API](https://github.com/log4js-node/log4js-node/pull/1364) - thanks [@lamweili](https://github.com/lamweili)
  - [docs: added `log4js.isConfigured()`](https://github.com/log4js-node/log4js-node/pull/1369) - thanks [@lamweili](https://github.com/lamweili)
- [feat(layout): support a specifier on %m](https://github.com/log4js-node/log4js-node/pull/1346) - thanks [@lamweili](https://github.com/lamweili)
- [fix: tilde expansion for windows](https://github.com/log4js-node/log4js-node/pull/1363) - thanks [@lamweili](https://github.com/lamweili)
- [docs: updated typescript usage](https://github.com/log4js-node/log4js-node/pull/1361) - thanks [@lamweili](https://github.com/lamweili)
- [test: improved test for fileAppender](https://github.com/log4js-node/log4js-node/pull/1365) - thanks [@lamweili](https://github.com/lamweili)
- [ci: generate coverage report in both text and html](https://github.com/log4js-node/log4js-node/pull/1368) - thanks [@lamweili](https://github.com/lamweili)
- [ci: replaced deprecated github set-output](https://github.com/log4js-node/log4js-node/pull/1351) - thanks [@lamweili](https://github.com/lamweili)
- [chore(deps): updated dependencies](https://github.com/log4js-node/log4js-node/pull/1367) - thanks [@lamweili](https://github.com/lamweili)
  - chore(deps): bump streamroller from 3.1.3 to 3.1.5
  - chore(deps): updated package-lock.json
- [chore(deps-dev): updated dependencies](https://github.com/log4js-node/log4js-node/pull/1366) - thanks [@lamweili](https://github.com/lamweili)
  - chore(deps-dev): bump @commitlint/cli from 17.3.0 to 17.4.4
  - chore(deps-dev): bump @commitlint/config-conventional from 17.3.0 to 17.4.4
  - chore(deps-dev): bump eslint from 8.28.0 to 8.34.0
  - chore(deps-dev): bump eslint-config-prettier from 8.5.0 to 8.6.0
  - chore(deps-dev): bump eslint-import-resolver-node from 0.3.6 to 0.3.7
  - chore(deps-dev): bump eslint-plugin-import from 2.26.0 to 2.27.5
  - chore(deps-dev): bump fs-extra from 10.1.0 to 11.1.0
  - chore(deps-dev): bump husky from 8.0.2 to 8.0.3
  - chore(deps-dev): bump prettier from 2.8.0 to 2.8.4
  - chore(deps-dev): bump tap from 16.3.2 to 16.3.4
  - chore(deps-dev): bump typescript from 4.9.3 to 4.9.5
  - chore(deps-dev): updated package-lock.json
- [chore(deps-dev): bump json5 from 1.0.1 to 1.0.2](https://github.com/log4js-node/log4js-node/pull/1357) - thanks [@Dependabot](https://github.com/dependabot)

## [6.7.1](https://github.com/log4js-node/log4js-node/milestone/91)

- [type: updated Configuration.levels type to allow for custom log levels](https://github.com/log4js-node/log4js-node/pull/1348) - thanks [@lamweili](https://github.com/lamweili)
- [docs: fixed typo in `layouts.md`](https://github.com/log4js-node/log4js-node/pull/1338) - thanks [@dtslvr](https://github.com/dtslvr)
- [chore(deps-dev): updated dependencies](https://github.com/log4js-node/log4js-node/pull/1349) - thanks [@lamweili](https://github.com/lamweili)
  - chore(deps-dev): bump @commitlint/cli from 17.1.2 to 17.3.0
  - chore(deps-dev): bump @commitlint/config-conventional from 17.1.0 to 17.3.0
  - chore(deps-dev): bump eslint from 8.24.0 to 8.28.0
  - chore(deps-dev): bump husky from 8.0.1 to 8.0.2
  - chore(deps-dev): bump prettier from 2.7.1 to 2.8.0
  - chore(deps-dev): bump tap from 16.3.0 to 16.3.2
  - chore(deps-dev): bump typescript from 4.8.4 to 4.9.3
  - chore(deps-dev): updated package-lock.json

## [6.7.0](https://github.com/log4js-node/log4js-node/milestone/90)

- [feat(log4js): if cb is passed to shutdown(), it must be a function or it will throw error immediately](https://github.com/log4js-node/log4js-node/pull/1334) - thanks [@lamweili](https://github.com/lamweili)
- [feat: patternLayout function name, class name and function alias](https://github.com/log4js-node/log4js-node/pull/1316) - thanks [@l0ner](https://github.com/l0ner)
  - [refactor(#1316): code flow and readability (%C:class, %M:function, %A:alias, %F:full - in order)](https://github.com/log4js-node/log4js-node/pull/1317) - thanks [@lamweili](https://github.com/lamweili)
- [feat: add ability to use passed in Errors for callstacks and adjust how deeply you want to look for information](https://github.com/log4js-node/log4js-node/pull/1269) - thanks [@ZachHaber](https://github.com/ZachHaber)
- [fix(LoggingEvent): serde for NaN, Infinity, -Infinity, undefined](https://github.com/log4js-node/log4js-node/pull/1332) - thanks [@lamweili](https://github.com/lamweili)
- [fix: make shutdown return value consistent](https://github.com/log4js-node/log4js-node/pull/1082) - thanks [@richardhinkamp](https://github.com/richardhinkamp)
  - [refactor(#1082): removed return value for `log4js.shutdown()`](https://github.com/log4js-node/log4js-node/pull/1319) - thanks [@lamweili](https://github.com/lamweili)
- [test: adjusted timeout interval for OS operations](https://github.com/log4js-node/log4js-node/pull/1318) - thanks [@lamweili](https://github.com/lamweili)
- [refactor(LoggingEvent): loop through location keys instead of hard-coding one-by-one](https://github.com/log4js-node/log4js-node/pull/1333) - thanks [@lamweili](https://github.com/lamweili)
- [docs(noLogFilter): fix heading (`Category Filter` --> `No-Log Filter`)](https://github.com/log4js-node/log4js-node/pull/1322) - thanks [@gkalpak](https://github.com/gkalpak)
- [docs(migration-guide): fix typo (`have been remove` --> `have been removed`)](https://github.com/log4js-node/log4js-node/pull/1321) - thanks [@gkalpak](https://github.com/gkalpak)
- [docs: added quotes for `just-errors` JSON property key ](https://github.com/log4js-node/log4js-node/pull/1312) - thanks [@lamweili](https://github.com/lamweili)
- [ci: skip prettier on older Node.js (8.x, 10.x, 12.x)](https://github.com/log4js-node/log4js-node/pull/1328) - thanks [@lamweili](https://github.com/lamweili)
- [ci: manually downgrade dev dependencies for older versions](https://github.com/log4js-node/log4js-node/pull/1326) - thanks [@lamweili](https://github.com/lamweili)
- [ci: removed scheduled job from codeql and separated npm audit](https://github.com/log4js-node/log4js-node/pull/1325) - thanks [@lamweili](https://github.com/lamweili)
- [chore(deps): bump typescript from 4.8.3 to 4.8.4](https://github.com/log4js-node/log4js-node/pull/1330) - thanks [@lamweili](https://github.com/lamweili)
- [chore(deps-dev): updated dependencies](https://github.com/log4js-node/log4js-node/pull/1329) - thanks [@lamweili](https://github.com/lamweili)
  - chore(deps-dev): bump nyc from 14.1.1 to 15.1.0
  - chore(deps-dev): bump tap from 14.11.0 to 16.3.0
  - chore(deps-dev): bump eslint-plugin-prettier from 3.4.1 to 4.2.1
  - chore(deps-dev): bump prettier from 1.19.1 to 2.7.1
  - chore(deps-dev): updated package-lock.json
- [chore(deps): updated dependencies](https://github.com/log4js-node/log4js-node/pull/1327) - thanks [@lamweili](https://github.com/lamweili)
  - chore(deps-dev): bump @commitlint/cli from 17.0.3 to 17.1.2
  - chore(deps-dev): bump @commitlint/config-conventional from 17.0.3 to 17.1.0
  - chore(deps-dev): bump eslint from 6.8.0 to 8.24.0
  - chore(deps-dev): bump eslint-config-airbnb-base from 14.2.1 to 15.0.0
  - chore(deps-dev): bump eslint-config-prettier from 6.15.0 to 8.5.0
  - chore(deps-dev): bump fs-extra from 8.1.0 to 10.1.0
  - chore(deps-dev): bump typescript from 4.7.4 to 4.8.3
  - chore(deps): bump date-format from 4.0.13 to 4.0.14
  - chore(deps): bump flatted from 3.2.6 to 3.2.7
  - chore(deps): bump streamroller from 3.1.2 to 3.1.3
  - chore(deps): updated package-lock.json

## [6.6.1](https://github.com/log4js-node/log4js-node/milestone/89)

- [fix: connectlogger nolog function](https://github.com/log4js-node/log4js-node/pull/1285) - thanks [@eyoboue](https://github.com/eyoboue)
- [type: corrected AppenderModule interface and Recording interface](https://github.com/log4js-node/log4js-node/pull/1304) - thanks [@lamweili](https://github.com/lamweili)
- test: extended timeout interval for OS operations - thanks [@lamweili](https://github.com/lamweili)
  - test: [#1306](https://github.com/log4js-node/log4js-node/pull/1306)
  - test: [#1297](https://github.com/log4js-node/log4js-node/pull/1297)
- [test: support older Node.js versions](https://github.com/log4js-node/log4js-node/pull/1295) - thanks [@lamweili](https://github.com/lamweili)
- [ci: added tests for Node.js 8.x](https://github.com/log4js-node/log4js-node/pull/1303) - thanks [@lamweili](https://github.com/lamweili)
- [ci: added tests for Node.js 10.x, 18.x](https://github.com/log4js-node/log4js-node/pull/1301) - thanks [@lamweili](https://github.com/lamweili)
- [ci: updated codeql from v1 to v2](https://github.com/log4js-node/log4js-node/pull/1302) - thanks [@lamweili](https://github.com/lamweili)
- [chore(deps-dev): bump lodash from 4.17.19 to 4.17.21](https://github.com/log4js-node/log4js-node/pull/1309) - thanks [@Dependabot](https://github.com/dependabot)
- [chore(deps-dev): bump path-parse from 1.0.6 to 1.0.7](https://github.com/log4js-node/log4js-node/pull/1308) - thanks [@Dependabot](https://github.com/dependabot)
- [chore(deps-dev): downgraded nyc from 15.1.0 to 14.1.1](https://github.com/log4js-node/log4js-node/pull/1305) - thanks [@lamweili](https://github.com/lamweili)
- [chore(deps): updated dependencies](https://github.com/log4js-node/log4js-node/pull/1296) - thanks [@lamweili](https://github.com/lamweili)
  - chore(deps): bump date-format from 4.0.11 to 4.0.13
  - chore(deps): bump flatted from 3.2.5 to 3.2.6
  - chore(deps): bump streamroller from 3.1.1 to 3.1.2
  - chore(deps-dev): bump @commitlint/cli from 17.0.2 to 17.0.3
  - chore(deps-dev): bump @commitlint/config-conventional from 17.0.2 to 17.0.3
  - [chore(deps-dev): bump eslint from 8.16.0 to 8.20.0](https://github.com/log4js-node/log4js-node/pull/1300)
  - chore(deps-dev): bump eslint-plugin-prettier from 4.0.0 to 4.2.1
  - chore(deps-dev): bump prettier from 2.6.0 to 2.7.1
  - chore(deps-dev): bump tap from 16.2.0 to 16.3.0
  - chore(deps-dev): bump typescript from 4.7.2 to 4.7.4
  - chore(deps): updated package-lock.json

## [6.6.0](https://github.com/log4js-node/log4js-node/milestone/87)

- [feat: adding function(req, res) support to connectLogger nolog](https://github.com/log4js-node/log4js-node/pull/1279) - thanks [@eyoboue](https://github.com/eyoboue)
- [fix: ability to load CJS appenders (through .cjs extension) for ESM packages](https://github.com/log4js-node/log4js-node/pull/1280) - thanks [@lamweili](https://github.com/lamweili)
- [type: consistent typing for Logger](https://github.com/log4js-node/log4js-node/pull/1276) - thanks [@taozi0818](https://github.com/taozi0818)
- [type: Make Appender Type extensible from other modules and the user](https://github.com/log4js-node/log4js-node/pull/1267) - thanks [@ZachHaber](https://github.com/ZachHaber)
- [refactor: clearer logic for invalid level and LOG synonym](https://github.com/log4js-node/log4js-node/pull/1264) - thanks [@lamweili](https://github.com/lamweili)
- [style: ran prettier and requires prettier for ci](https://github.com/log4js-node/log4js-node/pull/1271) - thanks [@ZachHaber](https://github.com/ZachHaber)
- [docs: renamed peteriman to lamweili in changelog](https://github.com/log4js-node/log4js-node/pull/1272) - thanks [@lamweili](https://github.com/lamweili)
- [ci: replaced validate-commit-msg, fixed husky config, removed codecov](https://github.com/log4js-node/log4js-node/pull/1274) - thanks [@ZachHaber](https://github.com/ZachHaber)
- [chore(deps): updated dependencies](https://github.com/log4js-node/log4js-node/pull/1266) - thanks [@lamweili](https://github.com/lamweili)
  - chore(deps-dev): bump typescript from 4.6.4 to 4.7.2
  - chore(deps): bump date-format from 4.0.10 to 4.0.11
  - chore(deps): updated package-lock.json

## [6.5.2](https://github.com/log4js-node/log4js-node/milestone/86)

- [type: add LogEvent.serialise](https://github.com/log4js-node/log4js-node/pull/1260) - thanks [@marrowleaves](https://github.com/marrowleaves)

## [6.5.1](https://github.com/log4js-node/log4js-node/milestone/85)

- [fix: fs.appendFileSync should use flag instead of flags](https://github.com/log4js-node/log4js-node/pull/1257) - thanks [@lamweili](https://github.com/lamweili)
- [chore(deps): updated dependencies](https://github.com/log4js-node/log4js-node/pull/1258) - thanks [@lamweili](https://github.com/lamweili)
  - chore(deps): bump streamroller from 3.1.0 to 3.1.1
  - chore(deps): updated package-lock.json

## [6.5.0](https://github.com/log4js-node/log4js-node/milestone/84)

- [feat: logger.log() to be synonym of logger.info()](https://github.com/log4js-node/log4js-node/pull/1254) - thanks [@lamweili](https://github.com/lamweili)
- [feat: tilde expansion for filename](https://github.com/log4js-node/log4js-node/pull/1252) - thanks [@lamweili](https://github.com/lamweili)
- [fix: better file validation](https://github.com/log4js-node/log4js-node/pull/1251) - thanks [@lamweili](https://github.com/lamweili)
- [fix: fallback for logger.log outputs nothing](https://github.com/log4js-node/log4js-node/pull/1247) - thanks [@lamweili](https://github.com/lamweili)
- [docs: updated fileAppender maxLogSize documentation](https://github.com/log4js-node/log4js-node/pull/1248) - thanks [@lamweili](https://github.com/lamweili)
- [ci: enforced 100% test coverage tests](https://github.com/log4js-node/log4js-node/pull/1253) - thanks [@lamweili](https://github.com/lamweili)
- [chore(deps): updated dependencies](https://github.com/log4js-node/log4js-node/pull/1256) - thanks [@lamweili](https://github.com/lamweili)
  - chore(deps-dev): bump eslint from 8.15.0 to 8.16.0
  - chore(deps): bump streamroller from 3.0.9 to 3.1.0
  - chore(deps): updated package-lock.json

## [6.4.7](https://github.com/log4js-node/log4js-node/milestone/83)

- [fix: dateFileAppender unable to use units in maxLogSize](https://github.com/log4js-node/log4js-node/pull/1243) - thanks [@lamweili](https://github.com/lamweili)
- [type: added fileNameSep for FileAppender and DateFileAppender](https://github.com/log4js-node/log4js-node/pull/1241) - thanks [@lamweili](https://github.com/lamweili)
- [docs: updated usage of units for maxLogSize](https://github.com/log4js-node/log4js-node/pull/1242) - thanks [@lamweili](https://github.com/lamweili)
- [docs: updated comments in typescript def](https://github.com/log4js-node/log4js-node/pull/1240) - thanks [@lamweili](https://github.com/lamweili)
- [chore(deps): updated dependencies](https://github.com/log4js-node/log4js-node/pull/1244) - thanks [@lamweili](https://github.com/lamweili)
  - chore(deps-dev): bump eslint from 8.14.0 to 8.15.0
  - chore(deps-dev): bump husky from 7.0.4 to 8.0.1
  - chore(deps-dev): bump tap from 16.1.0 to 16.2.0
  - chore(deps-dev): bump typescript from 4.6.3 to 4.6.4
  - chore(deps): bump date-format from 4.0.9 to 4.0.10
  - chore(deps): bump streamroller from 3.0.8 to 3.0.9
  - chore(deps): updated package-lock.json
- [chore(deps): updated dependencies](https://github.com/log4js-node/log4js-node/pull/1238) - thanks [@lamweili](https://github.com/lamweili)
  - chore(deps-dev): bump tap from 16.0.1 to 16.1.0
  - chore(deps-dev): updated package-lock.json

## [6.4.6](https://github.com/log4js-node/log4js-node/milestone/82)

- [chore(deps): updated dependencies](https://github.com/log4js-node/log4js-node/pull/1236) - thanks [@lamweili](https://github.com/lamweili)
  - chore(deps-dev): bump eslint from 8.13.0 to 8.14.0
  - chore(deps): bump date-format from 4.0.7 to 4.0.9
  - chore(deps): bump streamroller from 3.0.7 to 3.0.8
    - fix: [#1216](https://github.com/log4js-node/log4js-node/issues/1216) where promise rejection is not handled ([streamroller@3.0.8 changelog](https://github.com/log4js-node/streamroller/blob/master/CHANGELOG.md))
  - chore(deps): updated package-lock.json
- [chore(deps): updated dependencies](https://github.com/log4js-node/log4js-node/pull/1234) - thanks [@lamweili](https://github.com/lamweili)
  - chore(deps): bump fs-extra from 10.0.1 to 10.1.0
  - chore(deps): updated package-lock.json

## [6.4.5](https://github.com/log4js-node/log4js-node/milestone/81)

- [fix: deserialise for enableCallStack features: filename, lineNumber, columnNumber, callStack](https://github.com/log4js-node/log4js-node/pull/1230) - thanks [@lamweili](https://github.com/lamweili)
- [fix: fileDepth for ESM](https://github.com/log4js-node/log4js-node/pull/1224) - thanks [@lamweili](https://github.com/lamweili)
- [refactor: replace deprecated String.prototype.substr()](https://github.com/log4js-node/log4js-node/pull/1223) - thanks [@CommanderRoot](https://github.com/CommanderRoot)
- [type: LogEvent types](https://github.com/log4js-node/log4js-node/pull/1231) - thanks [@lamweili](https://github.com/lamweili)
- [docs: updated typescript usage](https://github.com/log4js-node/log4js-node/pull/1229) - thanks [@lamweili](https://github.com/lamweili)
- [chore(deps): updated dependencies](https://github.com/log4js-node/log4js-node/pull/1232) - thanks [@lamweili](https://github.com/lamweili)
  - chore(deps): bump date-format from 4.0.6 to 4.0.7
  - chore(deps): bump streamroller from 3.0.6 to 3.0.7
    - fix: [#1225](https://github.com/log4js-node/log4js-node/issues/1225) where fs-extra throws error when fs.realpath.native is undefined ([streamroller@3.0.7 changelog](https://github.com/log4js-node/streamroller/blob/master/CHANGELOG.md))
  - chore(deps): updated package-lock.json
- [chore(deps): updated dependencies](https://github.com/log4js-node/log4js-node/pull/1228) - thanks [@lamweili](https://github.com/lamweili)
  - chore(deps-dev): bump eslint from 8.11.0 to 8.13.0
  - chore(deps-dev): bump eslint-plugin-import from 2.25.4 to 2.26.0
  - chore(deps-dev): bump tap from 16.0.0 to 16.0.1
  - chore(deps-dev): bump typescript from 4.6.2 to 4.6.3
  - chore(deps-dev): updated package-lock.json
- [chore(deps-dev): bump minimist from 1.2.5 to 1.2.6](https://github.com/log4js-node/log4js-node/pull/1227) - thanks [@Dependabot](https://github.com/dependabot)

## [6.4.4](https://github.com/log4js-node/log4js-node/milestone/80)

- [fix: set logger.level on runtime will no longer wrongly reset useCallStack](https://github.com/log4js-node/log4js-node/pull/1217) - thanks [@lamweili](https://github.com/lamweili)
- [docs: updated docs for broken links and inaccessible pages](https://github.com/log4js-node/log4js-node/pull/1219) - thanks [@lamweili](https://github.com/lamweili)
- [docs: broken link to gelf appender](https://github.com/log4js-node/log4js-node/pull/1218) - thanks [@mattalexx](https://github.com/mattalexx)
- [docs: updated docs for appenders module loading](https://github.com/log4js-node/log4js-node/pull/985) - thanks [@leonimurilo](https://github.com/leonimurilo)
- [chore(deps): updated dependencies](https://github.com/log4js-node/log4js-node/pull/1221) - thanks [@lamweili](https://github.com/lamweili)
  - chore(deps): bump streamroller from 3.0.5 to 3.0.6
  - chore(deps): bump debug from 4.3.3 to 4.3.4
  - chore(deps): bump date-format from 4.0.5 to 4.0.6
  - chore(deps-dev): bump prettier from 2.5.1 to 2.6.0
  - chore(deps): updated package-lock.json

## [6.4.3](https://github.com/log4js-node/log4js-node/milestone/79)

- [fix: added filename validation](https://github.com/log4js-node/log4js-node/pull/1201) - thanks [@lamweili](https://github.com/lamweili)
- [refactor: do not initialise default appenders as it will be done again by configure()](https://github.com/log4js-node/log4js-node/pull/1210) - thanks [@lamweili](https://github.com/lamweili)
- [refactor: defensive coding for cluster=null if require('cluster') fails in try-catch ](https://github.com/log4js-node/log4js-node/pull/1199) - thanks [@lamweili](https://github.com/lamweili)
- [refactor: removed redundant logic in tcp-serverAppender](https://github.com/log4js-node/log4js-node/pull/1198) - thanks [@lamweili](https://github.com/lamweili)
- [refactor: removed redundant logic in multiprocessAppender](https://github.com/log4js-node/log4js-node/pull/1197) - thanks [@lamweili](https://github.com/lamweili)
- test: 100% test coverage - thanks [@lamweili](https://github.com/lamweili)
  - test: part 1 of 3: [#1200](https://github.com/log4js-node/log4js-node/pull/1200)
  - test: part 2 of 3: [#1204](https://github.com/log4js-node/log4js-node/pull/1204)
  - test: part 3 of 3: [#1205](https://github.com/log4js-node/log4js-node/pull/1205)
  - [test: improved test cases](https://github.com/log4js-node/log4js-node/pull/1211)
- [docs: updated README.md with badges](https://github.com/log4js-node/log4js-node/pull/1209) - thanks [@lamweili](https://github.com/lamweili)
- [docs: added docs for istanbul ignore](https://github.com/log4js-node/log4js-node/pull/1208) - thanks [@lamweili](https://github.com/lamweili)
- [docs: updated logger api docs](https://github.com/log4js-node/log4js-node/pull/1203) - thanks [@lamweili](https://github.com/lamweili)
- [docs: updated file and fileSync appender docs](https://github.com/log4js-node/log4js-node/pull/1202) - thanks [@lamweili](https://github.com/lamweili)
- [chore(lint): improve eslint rules](https://github.com/log4js-node/log4js-node/pull/1206) - thanks [@lamweili](https://github.com/lamweili)
- [chore(deps): updated dependencies](https://github.com/log4js-node/log4js-node/pull/1207) - thanks [@lamweili](https://github.com/lamweili)
  - chore(deps-dev): bump eslint from 8.10.0 to 8.11.0
  - chore(deps-dev): bump eslint-config-airbnb-base from 13.2.0 to 15.0.0
  - chore(deps-dev): bump eslint-config-prettier from 8.4.0 to 8.5.0
  - chore(deps-dev): bump tap from 15.1.6 to 16.0.0
  - chore(deps): bump date-format from 4.0.4 to 4.0.5
  - chore(deps): bump streamroller from 3.0.4 to 3.0.5
  - chore(deps): updated package-lock.json

## [6.4.2](https://github.com/log4js-node/log4js-node/milestone/78)

- [fix: fileSync appender to create directory recursively](https://github.com/log4js-node/log4js-node/pull/1191) - thanks [@lamweili](https://github.com/lamweili)
- [fix: serialise() for NaN, Infinity, -Infinity and undefined](https://github.com/log4js-node/log4js-node/pull/1188) - thanks [@lamweili](https://github.com/lamweili)
- [fix: connectLogger not logging on close](https://github.com/log4js-node/log4js-node/pull/1179) - thanks [@lamweili](https://github.com/lamweili)
- [refactor: defensive coding](https://github.com/log4js-node/log4js-node/pull/1183) - thanks [@lamweili](https://github.com/lamweili)
- [type: fixed Logger constructor](https://github.com/log4js-node/log4js-node/pull/1177) - thanks [@lamweili](https://github.com/lamweili)
- [test: improve test coverage](https://github.com/log4js-node/log4js-node/pull/1184) - thanks [@lamweili](https://github.com/lamweili)
- [test: refactor and replaced tap deprecation in preparation for tap v15](https://github.com/log4js-node/log4js-node/pull/1172) - thanks [@lamweili](https://github.com/lamweili)
- [test: added e2e test for multiprocess Appender](https://github.com/log4js-node/log4js-node/pull/1170) - thanks [@nicojs](https://github.com/nicojs)
- [docs: updated file appender docs](https://github.com/log4js-node/log4js-node/pull/1182) - thanks [@lamweili](https://github.com/lamweili)
- [docs: updated dateFile appender docs](https://github.com/log4js-node/log4js-node/pull/1181) - thanks [@lamweili](https://github.com/lamweili)
- [docs: corrected typo in sample code for multiFile appender](https://github.com/log4js-node/log4js-node/pull/1180) - thanks [@lamweili](https://github.com/lamweili)
- [chore(deps): updated deps-dev](https://github.com/log4js-node/log4js-node/pull/1194) - thanks [@lamweili](https://github.com/lamweili)
  - chore(deps): bump date-format from 4.0.3 to 4.0.4
  - chore(deps): bump streamroller from 3.0.2 to 3.0.4
    - fix: [#1189](https://github.com/log4js-node/log4js-node/issues/1189) for an compatibility issue with directory creation for NodeJS < 10.12.0 ([streamroller@3.0.3 changelog](https://github.com/log4js-node/streamroller/blob/master/CHANGELOG.md))
  - chore(deps-dev): bump eslint from 8.8.0 to 8.10.0
  - chore(deps-dev): bump eslint-config-prettier from 8.3.0 to 8.4.0
  - chore(deps-dev): bump fs-extra from 10.0.0 to 10.0.1
  - chore(deps-dev): bump typescript from 4.5.5 to 4.6.2
- [chore(deps): updated deps-dev](https://github.com/log4js-node/log4js-node/pull/1185) - thanks [@lamweili](https://github.com/lamweili)
  - chore(deps): bump flatted from 3.2.4 to 3.2.5
  - chore(deps-dev): bump eslint from 8.7.0 to 8.8.0
- [chore(deps): updated package-lock.json](https://github.com/log4js-node/log4js-node/pull/1174) - thanks [@lamweili](https://github.com/lamweili)
- [chore(deps-dev): bump tap from 14.10.7 to 15.1.6](https://github.com/log4js-node/log4js-node/pull/1173) - thanks [@lamweili](https://github.com/lamweili)

## [6.4.1](https://github.com/log4js-node/log4js-node/milestone/77)

- [fix: startup multiprocess even when no direct appenders](https://github.com/log4js-node/log4js-node/pull/1162) - thanks [@nicojs](https://github.com/nicojs)
  - [refactor: fixed eslint warnings](https://github.com/log4js-node/log4js-node/pull/1165) - thanks [@lamweili](https://github.com/lamweili)
- [refactor: additional alias for date patterns](https://github.com/log4js-node/log4js-node/pull/1163) - thanks [@lamweili](https://github.com/lamweili)
- [refactor: added emitWarning for deprecation](https://github.com/log4js-node/log4js-node/pull/1164) - thanks [@lamweili](https://github.com/lamweili)
- [type: Fixed wrong types from 6.4.0 regression](https://github.com/log4js-node/log4js-node/pull/1158) - thanks [@glasser](https://github.com/glasser)
- [docs: changed author to contributors in package.json](https://github.com/log4js-node/log4js-node/pull/1153) - thanks [@lamweili](https://github.com/lamweili)
- [chore(deps): bump node-fetch from 2.6.6 to 2.6.7](https://github.com/log4js-node/log4js-node/pull/1167) - thanks [@Dependabot](https://github.com/dependabot)
- [chore(deps-dev): bump typescript from 4.5.4 to 4.5.5](https://github.com/log4js-node/log4js-node/pull/1166) - thanks [@lamweili](https://github.com/lamweili)

## [6.4.0](https://github.com/log4js-node/log4js-node/milestone/76) - BREAKING CHANGE 💥

New default file permissions may cause external applications unable to read logs.
A [manual code/configuration change](https://github.com/log4js-node/log4js-node/pull/1141#issuecomment-1076224470) is required.

- [feat: added warnings when log() is used with invalid levels before fallbacking to INFO](https://github.com/log4js-node/log4js-node/pull/1062) - thanks [@abernh](https://github.com/abernh)
- [feat: exposed Recording](https://github.com/log4js-node/log4js-node/pull/1103) - thanks [@polo-language](https://github.com/polo-language)
- [fix: default file permission to be 0o600 instead of 0o644](https://github.com/log4js-node/log4js-node/pull/1141) - thanks [ranjit-git](https://www.huntr.dev/users/ranjit-git) and [@lamweili](https://github.com/lamweili)
  - [docs: updated fileSync.md and misc comments](https://github.com/log4js-node/log4js-node/pull/1148) - thanks [@lamweili](https://github.com/lamweili)
- [fix: file descriptor leak if repeated configure()](https://github.com/log4js-node/log4js-node/pull/1113) - thanks [@lamweili](https://github.com/lamweili)
- [fix: MaxListenersExceededWarning from NodeJS](https://github.com/log4js-node/log4js-node/pull/1110) - thanks [@lamweili](https://github.com/lamweili)
  - [test: added assertion for increase of SIGHUP listeners on log4js.configure()](https://github.com/log4js-node/log4js-node/pull/1142) - thanks [@lamweili](https://github.com/lamweili)
- [fix: missing TCP appender with Webpack and Typescript](https://github.com/log4js-node/log4js-node/pull/1028) - thanks [@techmunk](https://github.com/techmunk)
- [fix: dateFile appender exiting NodeJS on error](https://github.com/log4js-node/log4js-node/pull/1097) - thanks [@4eb0da](https://github.com/4eb0da)
  - [refactor: using writer.writable instead of alive for checking](https://github.com/log4js-node/log4js-node/pull/1144) - thanks [@lamweili](https://github.com/lamweili)
- [fix: TCP appender exiting NodeJS on error](https://github.com/log4js-node/log4js-node/pull/1089) - thanks [@jhonatanTeixeira](https://github.com/jhonatanTeixeira)
- [fix: multiprocess appender exiting NodeJS on error](https://github.com/log4js-node/log4js-node/pull/529) - thanks [@harlentan](https://github.com/harlentan)
- [test: update fakeFS.read as graceful-fs uses it](https://github.com/log4js-node/log4js-node/pull/1127) - thanks [@lamweili](https://github.com/lamweili)
- [test: update fakeFS.realpath as fs-extra uses it](https://github.com/log4js-node/log4js-node/pull/1128) - thanks [@lamweili](https://github.com/lamweili)
- test: added tap.tearDown() to clean up test files
  - test: [#1143](https://github.com/log4js-node/log4js-node/pull/1143) - thanks [@lamweili](https://github.com/lamweili)
  - test: [#1022](https://github.com/log4js-node/log4js-node/pull/1022) - thanks [@abetomo](https://github.com/abetomo)
- [type: improved @types for AppenderModule](https://github.com/log4js-node/log4js-node/pull/1079) - thanks [@nicobao](https://github.com/nicobao)
- [type: Updated fileSync appender types](https://github.com/log4js-node/log4js-node/pull/1116) - thanks [@lamweili](https://github.com/lamweili)
- [type: Removed erroneous type in file appender](https://github.com/log4js-node/log4js-node/pull/1031) - thanks [@vdmtrv](https://github.com/vdmtrv)
- [type: Updated Logger.log type](https://github.com/log4js-node/log4js-node/pull/1115) - thanks [@ZLundqvist](https://github.com/ZLundqvist)
- [type: Updated Logger.\_log type](https://github.com/log4js-node/log4js-node/pull/1117) - thanks [@lamweili](https://github.com/lamweili)
- [type: Updated Logger.level type](https://github.com/log4js-node/log4js-node/pull/1118) - thanks [@lamweili](https://github.com/lamweili)
- [type: Updated Levels.getLevel type](https://github.com/log4js-node/log4js-node/pull/1072) - thanks [@saulzhong](https://github.com/saulzhong)
- [chore(deps): bump streamroller from 3.0.1 to 3.0.2](https://github.com/log4js-node/log4js-node/pull/1147) - thanks [@lamweili](https://github.com/lamweili)
- [chore(deps): bump date-format from 4.0.2 to 4.0.3](https://github.com/log4js-node/log4js-node/pull/1146) - thanks [@lamweili](https://github.com/lamweili)
- [chore(deps-dev): bump eslint from from 8.6.0 to 8.7.0](https://github.com/log4js-node/log4js-node/pull/1145) - thanks [@lamweili](https://github.com/lamweili)
- [chore(deps-dev): bump nyc from 14.1.1 to 15.1.0](https://github.com/log4js-node/log4js-node/pull/1140) - thanks [@lamweili](https://github.com/lamweili)
- [chore(deps-dev): bump eslint from 5.16.0 to 8.6.0](https://github.com/log4js-node/log4js-node/pull/1138) - thanks [@lamweili](https://github.com/lamweili)
- [chore(deps): bump flatted from 2.0.2 to 3.2.4](https://github.com/log4js-node/log4js-node/pull/1137) - thanks [@lamweili](https://github.com/lamweili)
- [chore(deps-dev): bump fs-extra from 8.1.0 to 10.0.0](https://github.com/log4js-node/log4js-node/pull/1136) - thanks [@lamweili](https://github.com/lamweili)
- [chore(deps): bump streamroller from 2.2.4 to 3.0.1](https://github.com/log4js-node/log4js-node/pull/1135) - thanks [@lamweili](https://github.com/lamweili)
  - [fix: compressed file ignores dateFile appender "mode"](https://github.com/log4js-node/streamroller/pull/65) - thanks [@rnd-debug](https://github.com/rnd-debug)
  - fix: [#1039](https://github.com/log4js-node/log4js-node/issues/1039) where there is an additional separator in filename ([streamroller@3.0.0 changelog](https://github.com/log4js-node/streamroller/blob/master/CHANGELOG.md))
  - fix: [#1035](https://github.com/log4js-node/log4js-node/issues/1035), [#1080](https://github.com/log4js-node/log4js-node/issues/1080) for daysToKeep naming confusion ([streamroller@3.0.0 changelog](https://github.com/log4js-node/streamroller/blob/master/CHANGELOG.md))
  - [refactor: migrated from daysToKeep to numBackups due to streamroller@^3.0.0](https://github.com/log4js-node/log4js-node/pull/1149) - thanks [@lamweili](https://github.com/lamweili)
  - [feat: allows for zero backups](https://github.com/log4js-node/log4js-node/pull/1151) - thanks [@lamweili](https://github.com/lamweili)
- [chore(deps): bump date-format from 3.0.0 to 4.0.2](https://github.com/log4js-node/log4js-node/pull/1134) - thanks [@lamweili](https://github.com/lamweili)
- [chore(deps): updated dependencies](https://github.com/log4js-node/log4js-node/pull/1130) - thanks [@lamweili](https://github.com/lamweili)
  - chore(deps-dev): bump eslint-config-prettier from 6.15.0 to 8.3.0
  - chore(deps-dev): bump eslint-plugin-prettier from 3.4.1 to 4.0.0
  - chore(deps-dev): bump husky from 3.1.0 to 7.0.4
  - chore(deps-dev): bump prettier from 1.19.0 to 2.5.1
  - chore(deps-dev): bump typescript from 3.9.10 to 4.5.4
- [chore(deps-dev): bump eslint-config-prettier from 6.15.0 to 8.3.0](https://github.com/log4js-node/log4js-node/pull/1129) - thanks [@lamweili](https://github.com/lamweili)
- [chore(deps): updated dependencies](https://github.com/log4js-node/log4js-node/pull/1121) - thanks [@lamweili](https://github.com/lamweili)
  - chore(deps-dev): bump codecov from 3.6.1 to 3.8.3
  - chore(deps-dev): bump eslint-config-prettier from 6.5.0 to 6.15.0
  - chore(deps-dev): bump eslint-import-resolver-node from 0.3.2 to 0.3.6
  - chore(deps-dev): bump eslint-plugin-import" from 2.18.2 to 2.25.4
  - chore(deps-dev): bump eslint-plugin-prettier from 3.1.1 to 3.4.1
  - chore(deps-dev): bump husky from 3.0.9 to 3.1.0
  - chore(deps-dev): bump prettier from 1.18.2 to 1.19.1
  - chore(deps-dev): bump typescript from 3.7.2 to 3.9.10
- [chore(deps): bump path-parse from 1.0.6 to 1.0.7](https://github.com/log4js-node/log4js-node/pull/1120) - thanks [@Dependabot](https://github.com/dependabot)
- [chore(deps): bump glob-parent from 5.1.1 to 5.1.2](https://github.com/log4js-node/log4js-node/pull/1084) - thanks [@Dependabot](https://github.com/dependabot)
- [chore(deps): bump hosted-git-info from 2.7.1 to 2.8.9](https://github.com/log4js-node/log4js-node/pull/1076) - thanks [@Dependabot](https://github.com/dependabot)
- [chore(deps): bump lodash from 4.17.14 to 4.17.21](https://github.com/log4js-node/log4js-node/pull/1075) - thanks [@Dependabot](https://github.com/dependabot)
- [chore(deps): bump y18n from 4.0.0 to 4.0.1](https://github.com/log4js-node/log4js-node/pull/1070) - thanks [@Dependabot](https://github.com/dependabot)
- [chore(deps): bump node-fetch from 2.6.0 to 2.6.1](https://github.com/log4js-node/log4js-node/pull/1047) - thanks [@Dependabot](https://github.com/dependabot)
- [chore(deps): bump yargs-parser from 13.1.1 to 13.1.2](https://github.com/log4js-node/log4js-node/pull/1045) - thanks [@Dependabot](https://github.com/dependabot)
- [chore(deps-dev): bump codecov from 3.6.5 to 3.7.1](https://github.com/log4js-node/log4js-node/pull/1033) - thanks [@Dependabot](https://github.com/dependabot)

## [6.3.0](https://github.com/log4js-node/log4js-node/milestone/75)

- [Add option to file appender to remove ANSI colours](https://github.com/log4js-node/log4js-node/pull/1001) - thanks [@BlueCocoa](https://github.com/BlueCocoa)
- [Do not create appender if no categories use it](https://github.com/log4js-node/log4js-node/pull/1002) - thanks [@rnd-debug](https://github.com/rnd-debug)
- [Docs: better categories inheritance description](https://github.com/log4js-node/log4js-node/pull/1003) - thanks [@rnd-debug](https://github.com/rnd-debug)
- [Better jsdoc docs](https://github.com/log4js-node/log4js-node/pull/1004) - thanks [@wataash](https://github.com/wataash)
- [Typescript: access category field in Logger](https://github.com/log4js-node/log4js-node/pull/1006) - thanks [@rtvd](https://github.com/rtvd)
- [Docs: influxdb appender](https://github.com/log4js-node/log4js-node/pull/1014) - thanks [@rnd-debug](https://github.com/rnd-debug)
- [Support for fileSync appender in webpack](https://github.com/log4js-node/log4js-node/pull/1015) - thanks [@lauren-li](https://github.com/lauren-li)
- [Docs: UDP appender](https://github.com/log4js-node/log4js-node/pull/1018) - thanks [@iassasin](https://github.com/iassasin)
- [Style: spaces and tabs](https://github.com/log4js-node/log4js-node/pull/1016) - thanks [@abetomo](https://github.com/abetomo)

## [6.2.1](https://github.com/log4js-node/log4js-node/milestone/74)

- [Update streamroller to 2.2.4 to fix incorrect filename matching during log rotation](https://github.com/log4js-node/log4js-node/pull/996)

## [6.2.0](https://github.com/log4js-node/log4js-node/milestone/73)

- [Add custom message end token to TCP appender](https://github.com/log4js-node/log4js-node/pull/994) - thanks [@rnd-debug](https://github.com/rnd-debug)
- [Update acorn (dev dep of a dep)](https://github.com/log4js-node/log4js-node/pull/992) - thanks Github Robots.

## [6.1.2](https://github.com/log4js-node/log4js-node/milestone/72)

- [Handle out-of-order appender loading](https://github.com/log4js-node/log4js-node/pull/986) - thanks [@mvastola](https://github.com/mvastola)

## [6.1.1](https://github.com/log4js-node/log4js-node/milestone/71)

- [Add guards for undefined shutdown callback](https://github.com/log4js-node/log4js-node/pull/972) - thanks [@aaron-edwards](https://github.com/aaron-edwards)
- [Ignore .bob files](https://github.com/log4js-node/log4js-node/pull/975) - thanks [@cesine](https://github.com/cesine)
- [Add mark method to type definitions](https://github.com/log4js-node/log4js-node/pull/984) - thanks [@techmunk](https://github.com/techmunk)

## [6.1.0](https://github.com/log4js-node/log4js-node/milestone/70)

- [Add pause event to dateFile appender](https://github.com/log4js-node/log4js-node/pull/965) - thanks [@shayantabatabaee](https://github.com/shayantabatabaee)
- [Add pause event to file appender](https://github.com/log4js-node/log4js-node/pull/938) - thanks [@shayantabatabaee](https://github.com/shayantabatabaee)
- [Add pause/resume event to docs](https://github.com/log4js-node/log4js-node/pull/966)

## [6.0.0](https://github.com/log4js-node/log4js-node/milestone/69)

- [Update streamroller to fix unhandled promise rejection](https://github.com/log4js-node/log4js-node/pull/962)
- [Updated date-format library](https://github.com/log4js-node/log4js-node/pull/960)

## [5.3.0](https://github.com/log4js-node/log4js-node/milestone/68)

- [Padding and truncation changes](https://github.com/log4js-node/log4js-node/pull/956)

## [5.2.2](https://github.com/log4js-node/log4js-node/milestone/67)

- [Update streamroller to fix overwriting old files when using date rolling](https://github.com/log4js-node/log4js-node/pull/951)

## [5.2.1](https://github.com/log4js-node/log4js-node/milestone/66)

- [Update streamroller to fix numToKeep not working with dateFile pattern that is all digits](https://github.com/log4js-node/log4js-node/pull/949)

## [5.2.0](https://github.com/log4js-node/log4js-node/milestone/65)

- [Update streamroller to 2.2.0 (copy and truncate when file is busy)](https://github.com/log4js-node/log4js-node/pull/948)

## [5.1.0](https://github.com/log4js-node/log4js-node/milestone/64)

- [Update streamroller to 2.1.0 (windows fixes)](https://github.com/log4js-node/log4js-node/pull/933)

## [5.0.0](https://github.com/log4js-node/log4js-node/milestone/61)

- [Update streamroller to 2.0.0 (remove support for node v6)](https://github.com/log4js-node/log4js-node/pull/922)
- [Update dependencies (mostly dev deps)](https://github.com/log4js-node/log4js-node/pull/923)
- [Fix error when cluster not available](https://github.com/log4js-node/log4js-node/pull/930)
- [Test coverage improvements](https://github.com/log4js-node/log4js-node/pull/925)

## [4.5.1](https://github.com/log4js-node/log4js-node/milestone/63)

- [Update streamroller 1.0.5 -> 1.0.6 (to fix overwriting old backup log files)](https://github.com/log4js-node/log4js-node/pull/918)
- [Dependency update: lodash 4.17.4 (dependency of a dependency, not log4js)](https://github.com/log4js-node/log4js-node/pull/917) - thanks Github Automated Security Thing.
- [Dependency update: lodash 4.4.0 -> 4.5.0 (dependency of a dependency, not log4js)](https://github.com/log4js-node/log4js-node/pull/915) - thanks Github Automated Security Thing.

## [4.5.0](https://github.com/log4js-node/log4js-node/milestone/62)

- [Override call stack parsing](https://github.com/log4js-node/log4js-node/pull/914) - thanks [@rommni](https://github.com/rommni)
- [patternLayout filename depth token](https://github.com/log4js-node/log4js-node/pull/913) - thanks [@rommni](https://github.com/rommni)

## [4.4.0](https://github.com/log4js-node/log4js-node/milestone/60)

- [Add option to pass appender module in config](https://github.com/log4js-node/log4js-node/pull/833) - thanks [@kaxelson](https://github.com/kaxelson)
- [Added docs for passing appender module](https://github.com/log4js-node/log4js-node/pull/904)
- [Updated dependencies](https://github.com/log4js-node/log4js-node/pull/900)

## [4.3.2](https://github.com/log4js-node/log4js-node/milestone/59)

- [Types for enableCallStack](https://github.com/log4js-node/log4js-node/pull/897) - thanks [@citrusjunoss](https://github.com/citrusjunoss)

## [4.3.1](https://github.com/log4js-node/log4js-node/milestone/58)

- [Fix for maxLogSize in dateFile appender](https://github.com/log4js-node/log4js-node/pull/889)

## [4.3.0](https://github.com/log4js-node/log4js-node/milestone/57)

- [Feature: line number support](https://github.com/log4js-node/log4js-node/pull/879) - thanks [@victor0801x](https://github.com/victor0801x)
- [Fix for missing core appenders in webpack](https://github.com/log4js-node/log4js-node/pull/882)

## [4.2.0](https://github.com/log4js-node/log4js-node/milestone/56)

- [Feature: add appender and level inheritance](https://github.com/log4js-node/log4js-node/pull/863) - thanks [@pharapiak](https://github.com/pharapiak)
- [Feature: add response to context for connectLogger](https://github.com/log4js-node/log4js-node/pull/862) - thanks [@leak4mk0](https://github.com/leak4mk0)
- [Fix for broken sighup handler](https://github.com/log4js-node/log4js-node/pull/873)
- [Add missing types for Level](https://github.com/log4js-node/log4js-node/pull/872) - thanks [@Ivkaa](https://github.com/Ivkaa)
- [Typescript fixes for connect logger context](https://github.com/log4js-node/log4js-node/pull/876) - thanks [@leak4mk0](https://github.com/leak4mk0)
- [Upgrade to streamroller-1.0.5 to fix log rotation bug](https://github.com/log4js-node/log4js-node/pull/878)

## [4.1.1](https://github.com/log4js-node/log4js-node/milestone/55)

- [Various test fixes for node v12](https://github.com/log4js-node/log4js-node/pull/870)
- [Fix layout problem in node v12](https://github.com/log4js-node/log4js-node/pull/860) - thanks [@bjornstar](https://github.com/bjornstar)
- [Add missing types for addLevels](https://github.com/log4js-node/log4js-node/pull/867) - thanks [@Ivkaa](https://github.com/Ivkaa)
- [Allow any return type for layout function](https://github.com/log4js-node/log4js-node/pull/845) - thanks [@xinbenlv](https://github.com/xinbenlv)

## [4.1.0](https://github.com/log4js-node/log4js-node/milestone/54)

- Updated streamroller to 1.0.4, to fix a bug where the inital size of an existing file was ignored when appending
- [Updated streamroller to 1.0.3](https://github.com/log4js-node/log4js-node/pull/841), to fix a crash bug if the date pattern was all digits.
- [Updated dependencies](https://github.com/log4js-node/log4js-node/pull/840)

## Previous versions

Change information for older versions can be found by looking at the [milestones](https://github.com/log4js-node/log4js-node/milestones) in github.


code:
# log4js-node [![CodeQL](https://github.com/log4js-node/log4js-node/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/log4js-node/log4js-node/actions/workflows/codeql-analysis.yml) [![Node.js CI](https://github.com/log4js-node/log4js-node/actions/workflows/node.js.yml/badge.svg)](https://github.com/log4js-node/log4js-node/actions/workflows/node.js.yml)

[![NPM](https://nodei.co/npm/log4js.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/log4js/)

This is a conversion of the [log4js](https://github.com/stritti/log4js)
framework to work with [node](http://nodejs.org). I started out just stripping out the browser-specific code and tidying up some of the javascript to work better in node. It grew from there. Although it's got a similar name to the Java library [log4j](https://logging.apache.org/log4j/2.x/), thinking that it will behave the same way will only bring you sorrow and confusion.

The full documentation is available [here](https://log4js-node.github.io/log4js-node/).

[Changes in version 3.x](https://log4js-node.github.io/log4js-node/v3-changes.md)

There have been a few changes between log4js 1.x and 2.x (and 0.x too). You should probably read this [migration guide](https://log4js-node.github.io/log4js-node/migration-guide.html) if things aren't working.

Out of the box it supports the following features:

- coloured console logging to stdout or stderr
- file appender, with configurable log rolling based on file size or date
- a logger for connect/express servers
- configurable log message layout/patterns
- different log levels for different log categories (make some parts of your app log as DEBUG, others only ERRORS, etc.)

Optional appenders are available:

- [SMTP](https://github.com/log4js-node/smtp)
- [GELF](https://github.com/log4js-node/gelf)
- [Loggly](https://github.com/log4js-node/loggly)
- Logstash ([UDP](https://github.com/log4js-node/logstashUDP) and [HTTP](https://github.com/log4js-node/logstashHTTP))
- logFaces ([UDP](https://github.com/log4js-node/logFaces-UDP) and [HTTP](https://github.com/log4js-node/logFaces-HTTP))
- [RabbitMQ](https://github.com/log4js-node/rabbitmq)
- [Redis](https://github.com/log4js-node/redis)
- [Hipchat](https://github.com/log4js-node/hipchat)
- [Slack](https://github.com/log4js-node/slack)
- [mailgun](https://github.com/log4js-node/mailgun)
- [InfluxDB](https://github.com/rnd-debug/log4js-influxdb-appender)

## Getting help

Having problems? Jump on the [slack](https://join.slack.com/t/log4js-node/shared_invite/enQtODkzMDQ3MzExMDczLWUzZmY0MmI0YWI1ZjFhODY0YjI0YmU1N2U5ZTRkOTYyYzg3MjY5NWI4M2FjZThjYjdiOGM0NjU2NzBmYTJjOGI) channel, or create an issue. If you want to help out with the development, the slack channel is a good place to go as well.

## installation

```bash
npm install log4js
```

## usage

Minimalist version:

```javascript
var log4js = require("log4js");
var logger = log4js.getLogger();
logger.level = "debug";
logger.debug("Some debug messages");
```

By default, log4js will not output any logs (so that it can safely be used in libraries). The `level` for the `default` category is set to `OFF`. To enable logs, set the level (as in the example). This will then output to stdout with the coloured layout (thanks to [masylum](http://github.com/masylum)), so for the above you would see:

```bash
[2010-01-17 11:43:37.987] [DEBUG] [default] - Some debug messages
```

See example.js for a full example, but here's a snippet (also in `examples/fromreadme.js`):

```javascript
const log4js = require("log4js");
log4js.configure({
  appenders: { cheese: { type: "file", filename: "cheese.log" } },
  categories: { default: { appenders: ["cheese"], level: "error" } },
});

const logger = log4js.getLogger("cheese");
logger.trace("Entering cheese testing");
logger.debug("Got cheese.");
logger.info("Cheese is Comté.");
logger.warn("Cheese is quite smelly.");
logger.error("Cheese is too ripe!");
logger.fatal("Cheese was breeding ground for listeria.");
```

Output (in `cheese.log`):

```bash
[2010-01-17 11:43:37.987] [ERROR] cheese - Cheese is too ripe!
[2010-01-17 11:43:37.990] [FATAL] cheese - Cheese was breeding ground for listeria.
```

## Note for library makers

If you're writing a library and would like to include support for log4js, without introducing a dependency headache for your users, take a look at [log4js-api](https://github.com/log4js-node/log4js-api).

## Documentation

Available [here](https://log4js-node.github.io/log4js-node/).

There's also [an example application](https://github.com/log4js-node/log4js-example).

## TypeScript

```ts
import * as log4js from "log4js";
log4js.configure({
  appenders: { cheese: { type: "file", filename: "cheese.log" } },
  categories: { default: { appenders: ["cheese"], level: "error" } },
});

const logger = log4js.getLogger();
logger.level = "debug";
logger.debug("Some debug messages");
```

## Contributing

We're always looking for people to help out. Jump on [slack](https://join.slack.com/t/log4js-node/shared_invite/enQtODkzMDQ3MzExMDczLWUzZmY0MmI0YWI1ZjFhODY0YjI0YmU1N2U5ZTRkOTYyYzg3MjY5NWI4M2FjZThjYjdiOGM0NjU2NzBmYTJjOGI) and discuss what you want to do. Also, take a look at the [rules](https://log4js-node.github.io/log4js-node/contrib-guidelines.html) before submitting a pull request.

## License

The original log4js was distributed under the Apache 2.0 License, and so is this. I've tried to
keep the original copyright and author credits in place, except in sections that I have rewritten
extensively.


code:
# Security Policy

## Supported Versions

We're aiming to only support the latest major version of log4js. Older than that is usually _very_ old.

| Version | Supported          |
| ------- | ------------------ |
| 6.x     | :white_check_mark: |
| < 6.0   | :x:                |

## Reporting a Vulnerability

Report vulnerabilities via email to:

- Gareth Jones <gareth.nomiddlename@gmail.com>
- Lam Wei Li <lam_wei_li@hotmail.com>

Please put "[log4js:security]" in the subject line. We will aim to respond within a day or two.


code:
/* eslint max-classes-per-file: ["error", 2] */
/* eslint no-underscore-dangle: ["error", { "allow": ["_getLocationKeys"] }] */

const flatted = require('flatted');
const levels = require('./levels');

class SerDe {
  constructor() {
    const deserialise = {
      __LOG4JS_undefined__: undefined,
      __LOG4JS_NaN__: Number('abc'),
      __LOG4JS_Infinity__: 1 / 0,
      '__LOG4JS_-Infinity__': -1 / 0,
    };
    this.deMap = deserialise;
    this.serMap = {};
    Object.keys(this.deMap).forEach((key) => {
      const value = this.deMap[key];
      this.serMap[value] = key;
    });
  }

  canSerialise(key) {
    if (typeof key === 'string') return false;
    return key in this.serMap;
  }

  serialise(key) {
    if (this.canSerialise(key)) return this.serMap[key];
    return key;
  }

  canDeserialise(key) {
    return key in this.deMap;
  }

  deserialise(key) {
    if (this.canDeserialise(key)) return this.deMap[key];
    return key;
  }
}
const serde = new SerDe();

/**
 * @name LoggingEvent
 * @namespace Log4js
 */
class LoggingEvent {
  /**
   * Models a logging event.
   * @constructor
   * @param {string} categoryName name of category
   * @param {Log4js.Level} level level of message
   * @param {Array} data objects to log
   * @param {Error} [error]
   * @author Seth Chisamore
   */
  constructor(categoryName, level, data, context, location, error) {
    this.startTime = new Date();
    this.categoryName = categoryName;
    this.data = data;
    this.level = level;
    this.context = Object.assign({}, context); // eslint-disable-line prefer-object-spread
    this.pid = process.pid;
    this.error = error;

    if (typeof location !== 'undefined') {
      if (!location || typeof location !== 'object' || Array.isArray(location))
        throw new TypeError(
          'Invalid location type passed to LoggingEvent constructor'
        );

      this.constructor._getLocationKeys().forEach((key) => {
        if (typeof location[key] !== 'undefined') this[key] = location[key];
      });
    }
  }

  /** @private */
  static _getLocationKeys() {
    return [
      'fileName',
      'lineNumber',
      'columnNumber',
      'callStack',
      'className',
      'functionName',
      'functionAlias',
      'callerName',
    ];
  }

  serialise() {
    return flatted.stringify(this, (key, value) => {
      // JSON.stringify(new Error('test')) returns {}, which is not really useful for us.
      // The following allows us to serialize errors (semi) correctly.
      if (value instanceof Error) {
        // eslint-disable-next-line prefer-object-spread
        value = Object.assign(
          { message: value.message, stack: value.stack },
          value
        );
      }
      // JSON.stringify({a: Number('abc'), b: 1/0, c: -1/0}) returns {a: null, b: null, c: null}.
      // The following allows us to serialize to NaN, Infinity and -Infinity correctly.
      // JSON.stringify([undefined]) returns [null].
      // The following allows us to serialize to undefined correctly.
      return serde.serialise(value);
    });
  }

  static deserialise(serialised) {
    let event;
    try {
      const rehydratedEvent = flatted.parse(serialised, (key, value) => {
        if (value && value.message && value.stack) {
          const fakeError = new Error(value);
          Object.keys(value).forEach((k) => {
            fakeError[k] = value[k];
          });
          value = fakeError;
        }
        return serde.deserialise(value);
      });
      this._getLocationKeys().forEach((key) => {
        if (typeof rehydratedEvent[key] !== 'undefined') {
          if (!rehydratedEvent.location) rehydratedEvent.location = {};
          rehydratedEvent.location[key] = rehydratedEvent[key];
        }
      });
      event = new LoggingEvent(
        rehydratedEvent.categoryName,
        levels.getLevel(rehydratedEvent.level.levelStr),
        rehydratedEvent.data,
        rehydratedEvent.context,
        rehydratedEvent.location,
        rehydratedEvent.error
      );
      event.startTime = new Date(rehydratedEvent.startTime);
      event.pid = rehydratedEvent.pid;
      if (rehydratedEvent.cluster) {
        event.cluster = rehydratedEvent.cluster;
      }
    } catch (e) {
      event = new LoggingEvent('log4js', levels.ERROR, [
        'Unable to parse log:',
        serialised,
        'because: ',
        e,
      ]);
    }

    return event;
  }
}

module.exports = LoggingEvent;


code:
function maxFileSizeUnitTransform(maxLogSize) {
  if (typeof maxLogSize === 'number' && Number.isInteger(maxLogSize)) {
    return maxLogSize;
  }

  const units = {
    K: 1024,
    M: 1024 * 1024,
    G: 1024 * 1024 * 1024,
  };
  const validUnit = Object.keys(units);
  const unit = maxLogSize.slice(-1).toLocaleUpperCase();
  const value = maxLogSize.slice(0, -1).trim();

  if (validUnit.indexOf(unit) < 0 || !Number.isInteger(Number(value))) {
    throw Error(`maxLogSize: "${maxLogSize}" is invalid`);
  } else {
    return value * units[unit];
  }
}

function adapter(configAdapter, config) {
  const newConfig = Object.assign({}, config); // eslint-disable-line prefer-object-spread
  Object.keys(configAdapter).forEach((key) => {
    if (newConfig[key]) {
      newConfig[key] = configAdapter[key](config[key]);
    }
  });
  return newConfig;
}

function fileAppenderAdapter(config) {
  const configAdapter = {
    maxLogSize: maxFileSizeUnitTransform,
  };
  return adapter(configAdapter, config);
}

const adapters = {
  dateFile: fileAppenderAdapter,
  file: fileAppenderAdapter,
  fileSync: fileAppenderAdapter,
};

module.exports.modifyConfig = (config) =>
  adapters[config.type] ? adapters[config.type](config) : config;


code:
const debug = require('debug')('log4js:categoryFilter');

function categoryFilter(excludes, appender) {
  if (typeof excludes === 'string') excludes = [excludes];
  return (logEvent) => {
    debug(`Checking ${logEvent.categoryName} against ${excludes}`);
    if (excludes.indexOf(logEvent.categoryName) === -1) {
      debug('Not excluded, sending to appender');
      appender(logEvent);
    }
  };
}

function configure(config, layouts, findAppender) {
  const appender = findAppender(config.appender);
  return categoryFilter(config.exclude, appender);
}

module.exports.configure = configure;


code:
// eslint-disable-next-line no-console
const consoleLog = console.log.bind(console);

function consoleAppender(layout, timezoneOffset) {
  return (loggingEvent) => {
    consoleLog(layout(loggingEvent, timezoneOffset));
  };
}

function configure(config, layouts) {
  let layout = layouts.colouredLayout;
  if (config.layout) {
    layout = layouts.layout(config.layout.type, config.layout);
  }
  return consoleAppender(layout, config.timezoneOffset);
}

module.exports.configure = configure;


code:
const streams = require('streamroller');
const os = require('os');

const eol = os.EOL;

function openTheStream(filename, pattern, options) {
  const stream = new streams.DateRollingFileStream(filename, pattern, options);
  stream.on('error', (err) => {
    // eslint-disable-next-line no-console
    console.error(
      'log4js.dateFileAppender - Writing to file %s, error happened ',
      filename,
      err
    );
  });
  stream.on('drain', () => {
    process.emit('log4js:pause', false);
  });
  return stream;
}

/**
 * File appender that rolls files according to a date pattern.
 * @param filename base filename.
 * @param pattern the format that will be added to the end of filename when rolling,
 *          also used to check when to roll files - defaults to '.yyyy-MM-dd'
 * @param layout layout function for log messages - defaults to basicLayout
 * @param options - options to be passed to the underlying stream
 * @param timezoneOffset - optional timezone offset in minutes (default system local)
 */
function appender(filename, pattern, layout, options, timezoneOffset) {
  // the options for file appender use maxLogSize, but the docs say any file appender
  // options should work for dateFile as well.
  options.maxSize = options.maxLogSize;

  const writer = openTheStream(filename, pattern, options);

  const app = function (logEvent) {
    if (!writer.writable) {
      return;
    }
    if (!writer.write(layout(logEvent, timezoneOffset) + eol, 'utf8')) {
      process.emit('log4js:pause', true);
    }
  };

  app.shutdown = function (complete) {
    writer.end('', 'utf-8', complete);
  };

  return app;
}

function configure(config, layouts) {
  let layout = layouts.basicLayout;
  if (config.layout) {
    layout = layouts.layout(config.layout.type, config.layout);
  }

  if (!config.alwaysIncludePattern) {
    config.alwaysIncludePattern = false;
  }

  // security default (instead of relying on streamroller default)
  config.mode = config.mode || 0o600;

  return appender(
    config.filename,
    config.pattern,
    layout,
    config,
    config.timezoneOffset
  );
}

module.exports.configure = configure;


code:
const debug = require('debug')('log4js:file');
const path = require('path');
const streams = require('streamroller');
const os = require('os');

const eol = os.EOL;

let mainSighupListenerStarted = false;
const sighupListeners = new Set();
function mainSighupHandler() {
  sighupListeners.forEach((app) => {
    app.sighupHandler();
  });
}

/**
 * File Appender writing the logs to a text file. Supports rolling of logs by size.
 *
 * @param file the file log messages will be written to
 * @param layout a function that takes a logEvent and returns a string
 *   (defaults to basicLayout).
 * @param logSize - the maximum size (in bytes) for a log file,
 *   if not provided then logs won't be rotated.
 * @param numBackups - the number of log files to keep after logSize
 *   has been reached (default 5)
 * @param options - options to be passed to the underlying stream
 * @param timezoneOffset - optional timezone offset in minutes (default system local)
 */
function fileAppender(
  file,
  layout,
  logSize,
  numBackups,
  options,
  timezoneOffset
) {
  if (typeof file !== 'string' || file.length === 0) {
    throw new Error(`Invalid filename: ${file}`);
  } else if (file.endsWith(path.sep)) {
    throw new Error(`Filename is a directory: ${file}`);
  } else if (file.indexOf(`~${path.sep}`) === 0) {
    // handle ~ expansion: https://github.com/nodejs/node/issues/684
    // exclude ~ and ~filename as these can be valid files
    file = file.replace('~', os.homedir());
  }
  file = path.normalize(file);
  numBackups = !numBackups && numBackups !== 0 ? 5 : numBackups;

  debug(
    'Creating file appender (',
    file,
    ', ',
    logSize,
    ', ',
    numBackups,
    ', ',
    options,
    ', ',
    timezoneOffset,
    ')'
  );

  function openTheStream(filePath, fileSize, numFiles, opt) {
    const stream = new streams.RollingFileStream(
      filePath,
      fileSize,
      numFiles,
      opt
    );
    stream.on('error', (err) => {
      // eslint-disable-next-line no-console
      console.error(
        'log4js.fileAppender - Writing to file %s, error happened ',
        filePath,
        err
      );
    });
    stream.on('drain', () => {
      process.emit('log4js:pause', false);
    });
    return stream;
  }

  let writer = openTheStream(file, logSize, numBackups, options);

  const app = function (loggingEvent) {
    if (!writer.writable) {
      return;
    }
    if (options.removeColor === true) {
      // eslint-disable-next-line no-control-regex
      const regex = /\x1b[[0-9;]*m/g;
      loggingEvent.data = loggingEvent.data.map((d) => {
        if (typeof d === 'string') return d.replace(regex, '');
        return d;
      });
    }
    if (!writer.write(layout(loggingEvent, timezoneOffset) + eol, 'utf8')) {
      process.emit('log4js:pause', true);
    }
  };

  app.reopen = function () {
    writer.end(() => {
      writer = openTheStream(file, logSize, numBackups, options);
    });
  };

  app.sighupHandler = function () {
    debug('SIGHUP handler called.');
    app.reopen();
  };

  app.shutdown = function (complete) {
    sighupListeners.delete(app);
    if (sighupListeners.size === 0 && mainSighupListenerStarted) {
      process.removeListener('SIGHUP', mainSighupHandler);
      mainSighupListenerStarted = false;
    }
    writer.end('', 'utf-8', complete);
  };

  // On SIGHUP, close and reopen all files. This allows this appender to work with
  // logrotate. Note that if you are using logrotate, you should not set
  // `logSize`.
  sighupListeners.add(app);
  if (!mainSighupListenerStarted) {
    process.on('SIGHUP', mainSighupHandler);
    mainSighupListenerStarted = true;
  }

  return app;
}

function configure(config, layouts) {
  let layout = layouts.basicLayout;
  if (config.layout) {
    layout = layouts.layout(config.layout.type, config.layout);
  }

  // security default (instead of relying on streamroller default)
  config.mode = config.mode || 0o600;

  return fileAppender(
    config.filename,
    layout,
    config.maxLogSize,
    config.backups,
    config,
    config.timezoneOffset
  );
}

module.exports.configure = configure;


code:
const debug = require('debug')('log4js:fileSync');
const path = require('path');
const fs = require('fs');
const os = require('os');

const eol = os.EOL;

function touchFile(file, options) {
  // attempt to create the directory
  const mkdir = (dir) => {
    try {
      return fs.mkdirSync(dir, { recursive: true });
    } catch (e) {
      // backward-compatible fs.mkdirSync for nodejs pre-10.12.0 (without recursive option)
      // recursive creation of parent first
      if (e.code === 'ENOENT') {
        mkdir(path.dirname(dir));
        return mkdir(dir);
      }

      // throw error for all except EEXIST and EROFS (read-only filesystem)
      if (e.code !== 'EEXIST' && e.code !== 'EROFS') {
        throw e;
      }

      // EEXIST: throw if file and not directory
      // EROFS : throw if directory not found
      else {
        try {
          if (fs.statSync(dir).isDirectory()) {
            return dir;
          }
          throw e;
        } catch (err) {
          throw e;
        }
      }
    }
  };
  mkdir(path.dirname(file));

  // try to throw EISDIR, EROFS, EACCES
  fs.appendFileSync(file, '', { mode: options.mode, flag: options.flags });
}

class RollingFileSync {
  constructor(filename, maxLogSize, backups, options) {
    debug('In RollingFileStream');

    if (maxLogSize < 0) {
      throw new Error(`maxLogSize (${maxLogSize}) should be > 0`);
    }

    this.filename = filename;
    this.size = maxLogSize;
    this.backups = backups;
    this.options = options;
    this.currentSize = 0;

    function currentFileSize(file) {
      let fileSize = 0;

      try {
        fileSize = fs.statSync(file).size;
      } catch (e) {
        // file does not exist
        touchFile(file, options);
      }
      return fileSize;
    }

    this.currentSize = currentFileSize(this.filename);
  }

  shouldRoll() {
    debug(
      'should roll with current size %d, and max size %d',
      this.currentSize,
      this.size
    );
    return this.currentSize >= this.size;
  }

  roll(filename) {
    const that = this;
    const nameMatcher = new RegExp(`^${path.basename(filename)}`);

    function justTheseFiles(item) {
      return nameMatcher.test(item);
    }

    function index(filename_) {
      return (
        parseInt(filename_.slice(`${path.basename(filename)}.`.length), 10) || 0
      );
    }

    function byIndex(a, b) {
      return index(a) - index(b);
    }

    function increaseFileIndex(fileToRename) {
      const idx = index(fileToRename);
      debug(`Index of ${fileToRename} is ${idx}`);
      if (that.backups === 0) {
        fs.truncateSync(filename, 0);
      } else if (idx < that.backups) {
        // on windows, you can get a EEXIST error if you rename a file to an existing file
        // so, we'll try to delete the file we're renaming to first
        try {
          fs.unlinkSync(`${filename}.${idx + 1}`);
        } catch (e) {
          // ignore err: if we could not delete, it's most likely that it doesn't exist
        }

        debug(`Renaming ${fileToRename} -> ${filename}.${idx + 1}`);
        fs.renameSync(
          path.join(path.dirname(filename), fileToRename),
          `${filename}.${idx + 1}`
        );
      }
    }

    function renameTheFiles() {
      // roll the backups (rename file.n to file.n+1, where n <= numBackups)
      debug('Renaming the old files');

      const files = fs.readdirSync(path.dirname(filename));
      files
        .filter(justTheseFiles)
        .sort(byIndex)
        .reverse()
        .forEach(increaseFileIndex);
    }

    debug('Rolling, rolling, rolling');
    renameTheFiles();
  }

  // eslint-disable-next-line no-unused-vars
  write(chunk, encoding) {
    const that = this;

    function writeTheChunk() {
      debug('writing the chunk to the file');
      that.currentSize += chunk.length;
      fs.appendFileSync(that.filename, chunk);
    }

    debug('in write');

    if (this.shouldRoll()) {
      this.currentSize = 0;
      this.roll(this.filename);
    }

    writeTheChunk();
  }
}

/**
 * File Appender writing the logs to a text file. Supports rolling of logs by size.
 *
 * @param file the file log messages will be written to
 * @param layout a function that takes a logevent and returns a string
 *   (defaults to basicLayout).
 * @param logSize - the maximum size (in bytes) for a log file,
 *   if not provided then logs won't be rotated.
 * @param numBackups - the number of log files to keep after logSize
 *   has been reached (default 5)
 * @param options - options to be passed to the underlying stream
 * @param timezoneOffset - optional timezone offset in minutes (default system local)
 */
function fileAppender(
  file,
  layout,
  logSize,
  numBackups,
  options,
  timezoneOffset
) {
  if (typeof file !== 'string' || file.length === 0) {
    throw new Error(`Invalid filename: ${file}`);
  } else if (file.endsWith(path.sep)) {
    throw new Error(`Filename is a directory: ${file}`);
  } else if (file.indexOf(`~${path.sep}`) === 0) {
    // handle ~ expansion: https://github.com/nodejs/node/issues/684
    // exclude ~ and ~filename as these can be valid files
    file = file.replace('~', os.homedir());
  }
  file = path.normalize(file);
  numBackups = !numBackups && numBackups !== 0 ? 5 : numBackups;

  debug(
    'Creating fileSync appender (',
    file,
    ', ',
    logSize,
    ', ',
    numBackups,
    ', ',
    options,
    ', ',
    timezoneOffset,
    ')'
  );

  function openTheStream(filePath, fileSize, numFiles) {
    let stream;

    if (fileSize) {
      stream = new RollingFileSync(filePath, fileSize, numFiles, options);
    } else {
      stream = ((f) => {
        // touch the file to apply flags (like w to truncate the file)
        touchFile(f, options);

        return {
          write(data) {
            fs.appendFileSync(f, data);
          },
        };
      })(filePath);
    }

    return stream;
  }

  const logFile = openTheStream(file, logSize, numBackups);

  return (loggingEvent) => {
    logFile.write(layout(loggingEvent, timezoneOffset) + eol);
  };
}

function configure(config, layouts) {
  let layout = layouts.basicLayout;
  if (config.layout) {
    layout = layouts.layout(config.layout.type, config.layout);
  }

  const options = {
    flags: config.flags || 'a',
    encoding: config.encoding || 'utf8',
    mode: config.mode || 0o600,
  };

  return fileAppender(
    config.filename,
    layout,
    config.maxLogSize,
    config.backups,
    options,
    config.timezoneOffset
  );
}

module.exports.configure = configure;