<file_sep>/**
 * @file The wrapper for a specific test suite.
 */

const colorize = require('./colorize');
const { stub } = require('./stub');

/** @typedef {import('../index').MockData} MockData */

/** @type {MockData | undefined} */
let mockData;

/**
 * Set the mocks used for simulating `fs` methods return values.
 *
 * @public
 * @param {MockData} newMockData - Mock data to stub native modules.
 */
function setMocks (newMockData) {
	mockData = newMockData;
}

/**
 * Create a test suite to test a specific "check-outdated" call.
 *
 * @public
 * @param {string} title - The title of the test suite.
 * @param {string[]} argv - Arguments which are used for the `check-outdated` call.
 * @param {any} dependencies - Mock of the `npm outdated --json` response.
 * @param {(command: string | undefined, exitCode: number, stdout: string) => void} expectedCallback - Callback with for the assertion functionality.
 * @returns {Promise<void>} The Promise is resolved with `void` as soon as the test suite is finished.
 */
async function test (title, argv, dependencies, expectedCallback) {
	/* eslint-disable no-console -- console.log() is used to output the test results */
	console.log();
	console.log(`  ${JSON.stringify(argv)} ${title.replace(/\n/gu, '\\n').replace(/`(.+?)`/gu, colorize.underline('$1'))}`);
	console.log();
	/* eslint-enable no-console */

	let usedCommand;

	const checkOutdated = stub(mockData, dependencies, (command) => { usedCommand = command; });

	const unhookCapture = captureStdout();
	const exitCode = await checkOutdated(argv);
	const stdout = unhookCapture();

	expectedCallback(usedCommand, exitCode, stdout);
}

/**
 * Start capturing the output to process.stdout.
 *
 * @private
 * @returns {() => string} A callback function to stop the capturing, which returns the captured output as string.
 */
function captureStdout () {
	const write = process.stdout.write;

	/** @type {string[]} */
	const data = [];

	process.stdout.write = (/** @type {string | Uint8Array} */ buffer) => {
		data.push(buffer.toString());

		return false;
	};

	return () => {
		process.stdout.write = write;

		return data.join('');
	};
}

module.exports = {
	setMocks,
	test
};
<file_sep>/**
 * @file Grouping of related test cases.
 */

const colorize = require('./colorize');

/**
 * Wrapper to group related test cases.
 *
 * @public
 * @param {string} title - The title of the group of tests.
 * @param {() => Promise<void>} tests - The function which contains the different test suites.
 * @returns {Promise<void>} The Promise is resolved with `void` as soon as all test suites are finished.
 */
async function describe (title, tests) {
	/* eslint-disable no-console -- console.log() is used to output the test results */
	console.log();
	console.log(String(colorize.cyan(title.replace(/`(.+?)`/gu, colorize.underline('$1')))));
	/* eslint-enable no-console */

	await tests();
}

module.exports = {
	describe
};
<file_sep>/**
 * @file Different functionality to work with semantic version numbers.
 */

/** @typedef {import('./colorize').ColorizeProperty} ColorizeProperty */

/**
 * Colorize differences in parts of two semantic version numbers.
 *
 * @public
 * @param {readonly [string, string]} versions - Version numbers to compare.
 * @param {readonly [ColorizeProperty, ColorizeProperty]} equalColorizers - Styles for the first and second version number, for equal parts.
 * @param {readonly [ColorizeProperty, ColorizeProperty]} diffColorizers - Styles for the first and second version number, for unequal parts.
 * @returns {[string, string]} The colorized version numbers, in the same order as the input `versions` array.
 */
function semverDiff (versions, equalColorizers, diffColorizers) {
	const splitRegExp = /([.+-])/u;
	const parts1 = versions[0].split(splitRegExp);
	const parts2 = versions[1].split(splitRegExp);

	for (let partIndex = 0; partIndex < Math.max(parts1.length, parts2.length); partIndex++) {
		if (parts1[partIndex] !== parts2[partIndex]) {
			if (parts1[partIndex]) { parts1[partIndex] = diffColorizers[0](parts1[partIndex]); }
			if (parts2[partIndex]) { parts2[partIndex] = diffColorizers[1](parts2[partIndex]); }
		}
		else {
			parts1[partIndex] = equalColorizers[0](parts1[partIndex]);
			parts2[partIndex] = equalColorizers[1](parts2[partIndex]);
		}
	}

	return [parts1.join(''), parts2.join('')];
}

/**
 * Returns the type of the difference between two semantic version numbers.
 *
 * @public
 * @param {string} v1 - First version.
 * @param {string} v2 - Second version.
 * @returns {'major' | 'minor' | 'patch' | 'prerelease' | 'build' | 'revert' | undefined} The type as `string`, or `undefined` on same version or invalid semver formats.
 */
function semverDiffType (v1, v2) {
	if (v1 === v2) {
		return;
	}

	const semverRegExp = /^(\d+).(\d+).(\d+).*?(?:([-+]).+)?$/u;

	const match1 = semverRegExp.exec(v1);

	if (match1 === null) {
		return;
	}

	const match2 = semverRegExp.exec(v2);

	if (match2 === null) {
		return;
	}

	if (Number.parseInt(match2[1], 10) > Number.parseInt(match1[1], 10)) {
		return 'major';
	}

	if (Number.parseInt(match2[1], 10) < Number.parseInt(match1[1], 10)) {
		return 'revert';
	}

	if (Number.parseInt(match2[2], 10) > Number.parseInt(match1[2], 10)) {
		return 'minor';
	}

	if (Number.parseInt(match2[2], 10) < Number.parseInt(match1[2], 10)) {
		return 'revert';
	}

	if (Number.parseInt(match2[3], 10) > Number.parseInt(match1[3], 10)) {
		return 'patch';
	}

	if (Number.parseInt(match2[3], 10) < Number.parseInt(match1[3], 10)) {
		return 'revert';
	}

	if (match2[4] === '-') {
		return 'prerelease';
	}

	if (match2[4] === '+') {
		return 'build';
	}

	return;
}

/**
 * Checks if a semver range pattern matches to a specific version.
 *
 * @public
 * @param {string} version - Version.
 * @param {string} pattern - Range pattern.
 * @returns {boolean} `true` if the range pattern matches, otherwise `false`.
 */
function semverInRange (version, pattern) {
	const semverRangeRegExp = /^([\^~])?(?:(\d+|\*|x)?(?:\.(\d+|\*|x))?(?:\.(\d+|\*|x))?)?$/u;

	/** @type {(string | undefined)[] | null} */
	const match = semverRangeRegExp.exec(pattern);

	if (match === null) {
		return (version === pattern);
	}

	if (match[2] === '*' || match[2] === 'x') {
		return true;
	}

	const splitRegExp = /[.+-]/u;
	const parts = version.split(splitRegExp);

	if (match[1] === '^' || match[3] === '*' || match[3] === 'x') {
		return (parts[0] === match[2]);
	}

	if (match[1] === '~') {
		return (parts[0] === match[2] && (match[3] === undefined || parts[1] === match[3]) && (match[4] === undefined || Number.parseInt(parts[1], 10) >= Number.parseInt(match[4], 10)));
	}

	if (match[4] === '*' || match[4] === 'x') {
		return (parts[0] === match[2] && parts[1] === match[3]);
	}

	if (match[3] === undefined) {
		return (parts[0] === match[2]);
	}

	if (match[4] === undefined) {
		return (parts[0] === match[2] && parts[1] === match[3]);
	}

	return (version === pattern);
}

module.exports = {
	semverDiff,
	semverDiffType,
	semverInRange
};
<file_sep># @todo

<!-- markdownlint-disable MD034 -->

## Functionality improvements

- If no changelog is found, could we create one automatically, if version tags are available?
  e.g. https://github.com/GoogleChrome/workbox/compare/v6.5.2...v6.5.3

- Also check "overrides" and "resolutions":
  Overrides:
  https://docs.npmjs.com/cli/v8/configuring-npm/package-json#overrides
    "foo": {
      ".": "1.0.0",
      "bar": "1.0.0"
    }
    "bar": {
      "foo": "1.0.0"
    }
    "baz": {
      "bar": {
        "foo": "1.0.0"
      }
    }
    "bar@2.0.0": {
      "foo": "1.0.0"
    }

  References can be ignored as they do not "manage" their own version number:
  "overrides": {
    "luxon": "^1.21.3",
    "@vlc/components": "$@vlc/components",
    "@vlc/dialog": "$@vlc/dialog",
    "@vlc/form": "$@vlc/form",
    "@vlc/snackbar": "$@vlc/snackbar",
    "@vlc/table": "$@vlc/table",
    "formik-material-ui": "$formik-material-ui"
  },

- Resolutions:
  https://classic.yarnpkg.com/en/docs/selective-version-resolutions/
    "d2/left-pad": "1.1.1",
    "c/**/left-pad": "^1.1.2"

  When resolving the position within the file, it must be taken into account that different packages can contain different versions of sub-components.

- Delay warning for major and minor releases by X days to ensure that the new version has been adequately teste by others

- "--check-also" option which allows to check additional packages which are not referenced in the package.json
  --check-also better-npm-audit@1.9.1,improved-yarn-audit@2.3.3

- Ensure that [optionalDependencies](https://docs.npmjs.com/cli/v7/configuring-npm/package-json#optionaldependencies) are handled correctly.

- Support of aliases:
  "gsap": "npm:@gsap/shockingly@^3.6.0",
  
  Installable using:
  npm install -D tailwindcss@npm:@tailwindcss/postcss7-compat

  See the npm issue [[BUG] npm outdated fails to parse aliases
](https://github.com/npm/cli/issues/2800)

- [Test for CHANGELOG at GitLab](https://gitlab.com/allardyce/vectato)
  
- Add main-branch detection in helper\urls.js -> getChangelogFromURL() (right now, we assume the main branch is called "master")

- Add option to automatically update the outdated dependencies
  - It must be configurable if you want to auto-update only patches, or minor or major (all)
  - It must be configurable if the status code should be still -1 or if it should be 0
    - This must be configurable on a version-type base, e.g. patches should be silently updated, while minor and major updates should return -1

## Code quality improvements

## Test improvements

- Improve the "--depth" test by adding modules with deeper node_modules-structure.
- Add some more tests for edge-cases
  - child_process.exec() returns with error (first callback parameter)
<file_sep>/**
 * @file Generates a list from an array with key/value-pairs.
 */

/**
 * Generates a list from an array with key/value-pairs.
 * If the `value` is multiline text, each line will be prefixed by the `key`.
 *
 * @example
 * code ELIFECYCLE
 * errno 0
 * message Some additional
 * message multiline text.
 * additionalInfo null
 *
 * @public
 * @param {[string, any][]} entries - Array with subarray containing key/value-pairs.
 * @returns {string} A multiline string containing representing the array items.
 */
function generateKeyValueList (entries) {
	return entries.map(([key, value]) => (typeof value === 'string' ? value : JSON.stringify(value, null, '  ')).replace(/^/gmu, `$&${key} `)).join('\n');
}

module.exports = generateKeyValueList;
<file_sep><!-- markdownlint-configure-file { "MD033": { "allowed_elements": ["br", "img", "li", "ol", "span", "u"] } } -->

# check-outdated

[![npm version](https://badge.fury.io/js/check-outdated.svg)](https://badge.fury.io/js/check-outdated)
[![install size](https://packagephobia.com/badge?p=check-outdated@2.10.0)](https://packagephobia.com/result?p=check-outdated)
[![Known Vulnerabilities](https://snyk.io/test/github/jens-duttke/check-outdated/badge.svg?targetFile=package.json)](https://snyk.io/test/github/jens-duttke/check-outdated?targetFile=package.json)
[![npm](https://img.shields.io/npm/dm/check-outdated.svg?maxAge=2592000)](https://www.npmjs.com/package/check-outdated)
[![node](https://img.shields.io/node/v/check-outdated)](https://www.npmjs.com/package/check-outdated)
![Ensure Node.js compatibility](https://github.com/jens-duttke/check-outdated/workflows/Ensure%20Node.js%20compatibility/badge.svg?branch=main)
[![MIT license](https://img.shields.io/github/license/jens-duttke/check-outdated.svg?style=flat)](https://opensource.org/licenses/MIT)

Light-weight CLI tool to ensure that your dependencies are up-to-date, otherwise the process is terminated with status code 1.

This is an improved version of `npm outdated`, which can be used in build-pipelines, pre-publish scripts (npm) or pre-commit hook (Git) to make sure all the used dependencies are up-to-date.

- Zero dependencies
- Optionally ignore pre-releases (e.g. "2.1.0-alpha", "2.1.0-beta", "2.1.0-rc.1")
- Optionally ignore dev dependencies
- Optionally ignore specific packages
- Optionally ignore a specific version or version range of a package (e.g. to skip a broken version)
- Optionally restrict the update type (e.g. only show minor updates, or reverted versions)
- Optionally check globally installed packages
- Optionally set depth for checking dependency tree
- Show link to changelogs
- Configure visible columns

## Example Screenshot

[<img src="https://cdn.jsdelivr.net/gh/jens-duttke/check-outdated@93cb33b/screenshot.png" width="100%" />](https://cdn.jsdelivr.net/gh/jens-duttke/check-outdated@93cb33b/screenshot.png)

## Usage without installation

The benefit of using `npx` is, that it ensures that always the latest version of `check-outdated` is used. In addition, a download is only necessary in environments where it is needed - e.g. if `check-outdated` is not needed on the build server, it does not have to be downloaded there, which may
speeds up the dependency installation slightly.

On command-line you can run the command like this:

```sh
npx check-outdated --ignore-pre-releases --ignore-dev-dependencies --ignore-packages package1,package2 --columns name,type,current,latest,changes
```

Or put it into your `package.json`:

```json
{
  "scripts": {
    "check-outdated": "npx --yes -- check-outdated --ignore-pre-releases --ignore-dev-dependencies --ignore-packages package1,package2 --columns name,type,current,latest,changes --types major,minor,patch,reverted",
    "preversion": "npm run lint && npm run test && npm run check-outdated"
  }
}
```

## Usage with installation

### Install

```sh
npm install check-outdated --save-dev
# or
yarn add check-outdated -D
```

### Usage

After you've installed `check-outdated` you can run the command like this:

```sh
node_modules/.bin/check-outdated --ignore-pre-releases --ignore-dev-dependencies --ignore-packages package1,package2 --columns name,type,current,latest,changes --types major,minor,patch,reverted
```

Or put it into your `package.json`:

```json
{
  "scripts": {
    "check-outdated": "check-outdated --ignore-pre-releases --ignore-dev-dependencies --ignore-packages package1,package2 --columns name,type,current,latest,changes --types major,minor,patch,reverted",
    "preversion": "npm run lint && npm run test && npm run check-outdated"
  }
}
```

### Command-line arguments

| Argument | Description | Example |
|-|-|-|
| --help, -h | Show the help | `--help` |
| --ignore-pre-releases | Don't recommend to update to versions which contain a hyphen (e.g. "2.1.0-alpha", "2.1.0-beta", "2.1.0-rc.1") | `--ignore-pre-releases` |
| --ignore-dev-dependencies | Do not warn if devDependencies are outdated. | `--ignore-dev-dependencies` |
| --ignore-packages \<comma-separated-list-of-package-names\> | Ignore the listed packages, even if they are outdated.<br />Using the `@` syntax (`<package>@<version>`) you can also, only ignore a specific version, or a semver range (like `^2`, `~2.3.4`, `2.*`, `2.3.x`) of a package (e.g. if it's broken). | `--ignore-packages typescript,terser-webpack-plugin@3.0.0,got@^12` |
| --prefer-wanted | Compare the `Current` version to the `Wanted` version, instead of the `Latest` version. | `--prefer-wanted` |
| --columns \<comma-separated-list-of-columns\> | Defines which columns should be shown in which order. (See [Available Columns](#available-columns) below) | `--columns name,current,latest,changes` |
| --types \<comma-separated-list-of-update-types\> | Restrict the update type (e.g. only show minor updates, or reverted versions) (See [Available Types](#available-types) below) | `--types minor,reverted` |
| --global | Check packages in the global install prefix instead of in the current project (equal to the npm outdated-option) | `--global` |
| --depth \<number\> | Max depth for checking dependency tree (equal to the npm outdated-option) | `--depth 3` |

### Available Columns

By default, the following columns are shown:  
`package`, `current`, `wanted`, `latest`, `reference`, `changes`, `location`

You are able to overwrite the default by using the `--columns` argument.

| Caption | --columns value | Description | Example |
|-|-|-|-|
| Package | `package` | The name of the package.<br />**Red** means there's a newer version matching your semver requirements, so you should update now.<br />**Yellow** indicates that there's a newer version above your semver requirements (usually new major, or new 0.x minor) so proceed with caution. | typescript |
| Current | `current` | The currently installed version of the package. | 3.<u>7</u>.<u>2</u> |
| Wanted | `wanted` | The maximum version of the package that satisfies the semver range specified in **package.json**. If there's no available semver range (i.e. you're using the `--global` argument, or the package isn't included in **package.json**), then **wanted** shows the currently-installed version.<br />This column is always colored in **green**. | 3.7.2 |
| Latest | `latest` | The version of the package tagged as latest in the npm registry.<br />This column is always colored in **magenta**. | 3.<u>8</u>.<u>3</u> |
| Reference | `reference` | Contains a link to the line and column of the dependency in the **package.json**.<br />By using a terminal which supports clicking on such links, you can navigate directly the the item. | P:\my-project\package.json:47:3 |
| Changes | `changes` | **check-outdated** tries to find a direct link to changelog of the package. The following places are considered in the given order:<ol><li>{package}/package.json > "repository" \*</li><li>{package}/package.json > "homepage"</li><li>`https://www.npmjs.com/package/{name}`</li></ol>\* GitHub-repository URLs are adjusted, so that they directly link to the **CHANGELOG<span>.</span>md** or the **Releases** section. | https<span>:</span>//github<span>.</span>com/Microsoft/TypeScript/releases |
| Changes | `changesPreferLocal` | Same as `changes`, but first check for a CHANGELOG<span>.</span>md in the package folder.<br />Keep in mind, you'll only see the changelog of the currently installed version, not of the version which is recommended. | node_modules/fs-extra/CHANGELOG.md |
| Type | `type` | Shows if the difference between **Current** and **Latest** is a `major`, `minor`, `patch`, `prerelease`, `build` or `reverted` update, in [Semantic Versioning](https://semver.org/spec/v2.0.0.html). For more details see [Available Types](#available-types) below. | minor |
| Location | `location` | Shows where in the dependency tree the package is located. Note that **check-outdated** defaults to a depth of 0, so unless you override that, you'll always be seeing only top-level dependencies that are outdated. | node_modules/typescript |
| Package Type | `packageType` | Tells you whether this package is a `dependency` or a `devDependency`. Packages not included in **package.json** are always marked dependencies. If this column is not activated, the packages are grouped by their type, otherwise they are ordered by their name. | devDependencies |
| Homepage | `homepage` | An URL with additional information to the package. The following places are considered in the given order:<ol><li>{package}/package.json > "homepage"</li><li>{package}/package.json > "repository"</li><li>{package}/package.json > "author"</li><li>`https://www.npmjs.com/package/{name}`</li></ol> | https<span>:</span>//www<span>.</span>typescriptlang<span>.</span>org/ |
| npmjs<span>.</span>com | `npmjs` | A link to the package on the npmjs.com website. | https<span>:</span>//www<span>.</span>npmjs<span>.</span>com/package/typescript |

### Available Types

The type describes the difference between the **Current** version and **Latest** version, in [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
By default, all types are shown.

You are able to overwrite the default by using the `--types` argument.

| --types value | Description | Example |
|-|-|-|
| `major` | Backward-incompatible updates | `1.2.3` -> `2.0.0` |
| `minor` | Backward-compatible features | `1.2.3` -> `1.3.0` |
| `patch` | Backward-compatible bug fixes | `1.2.3` -> `1.2.4` |
| `reverted` | Latest available version is lower than the installed version | `1.2.3` -> `1.1.5` |
| `prerelease` | Only the pre-release version has been amended or added | `1.2.3` -> `1.2.3-beta.1` |
| `build` | Only build metadata has been amended or added | `1.2.3` -> `1.2.3+build.2` |
<file_sep>/**
 * @file Use ANSI escape sequences for coloring terminal output.
 */

/**
 * @typedef {(text: string) => string} ColorizeFunction
 */

/**
 * @typedef {Readonly<Colorize> & ColorizeFunction} ColorizeProperty
 */

/**
 * @typedef {object} Colorize
 *
 * General
 *
 * @property {ColorizeProperty} disabled
 *
 * Foreground colors
 *
 * @property {ColorizeProperty} gray
 * @property {ColorizeProperty} red
 * @property {ColorizeProperty} green
 * @property {ColorizeProperty} yellow
 * @property {ColorizeProperty} blue
 * @property {ColorizeProperty} magenta
 * @property {ColorizeProperty} cyan
 * @property {ColorizeProperty} white
 *
 * Text Decoration
 *
 * @property {ColorizeProperty} bold
 * @property {ColorizeProperty} underline
 */

/**
 * @typedef {object} ColorizeOptions
 * @property {keyof FOREGROUND_COLORS} [fgColor]
 * @property {keyof TEXT_DECORATIONS} [textDecoration]
 * @property {boolean} [disabled]
 */

const FOREGROUND_COLORS = {
	gray: 90,
	red: 31,
	green: 32,
	yellow: 33,
	blue: 34,
	magenta: 35,
	cyan: 36,
	white: 97
};

const TEXT_DECORATIONS = {
	bold: 1,
	underline: 4
};

const RESET_OFFSET = 20;
const DEFAULT_FOREGROUND_COLOR = 39;

/**
 * Allows to style strings by using ANSI escape sequences for coloring terminal output.
 *
 * @example
 * colorize({ fgColor: 'magenta', textDecoration: 'underline' })('text');
 * colorize({}).magenta.underline('text');
 *
 * @public
 * @param {ColorizeOptions} options - Properties, used to set the initial styles and to manage the internal styles on chaining.
 * @returns {ColorizeProperty} Function which can be used to colorize the given text, or for chaining.
 */
function colorize (options) {
	return /** @type {ColorizeProperty} */(Object.defineProperties(
		/**
		 * Styles the given `text` using ANSI escape sequences, based on the previously set `options`.
		 *
		 * @example
		 * [...].magenta.underline('text');
		 *
		 * @param {string} text - Plain text.
		 * @returns {string} Input `text` wrapped by ANSI escape sequences.
		 */
		(text) => {
			if (options.disabled || (!options.fgColor && !options.textDecoration)) {
				return text;
			}

			const openCode = [];
			const closeCode = [];

			if (options.fgColor) {
				openCode.push(FOREGROUND_COLORS[options.fgColor]);
				closeCode.push(DEFAULT_FOREGROUND_COLOR);
			}

			if (options.textDecoration) {
				openCode.push(TEXT_DECORATIONS[options.textDecoration]);
				closeCode.push(TEXT_DECORATIONS[options.textDecoration] + RESET_OFFSET);
			}

			return `\u001B[${openCode.join(';')}m${text}\u001B[${closeCode.join(';')}m`;
		},
		Object.assign(
			{
				disabled: {
					get () {
						return colorize({ ...options, disabled: true });
					}
				}
			},
			.../** @type {(keyof FOREGROUND_COLORS)[]} */(Object.keys(FOREGROUND_COLORS)).map((key) => ({
				[key]: {
					get () {
						return colorize({ ...options, fgColor: key });
					}
				}
			})),
			.../** @type {(keyof TEXT_DECORATIONS)[]} */(Object.keys(TEXT_DECORATIONS)).map((key) => ({
				[key]: {
					get () {
						return colorize({ ...options, textDecoration: key });
					}
				}
			}))
		)
	));
}

module.exports = colorize({});
<file_sep>/**
 * @file Assertion methods.
 */

const assert = require('assert').strict;

/**
 * Assert that a string contains a specific word, considering word boundaries.
 *
 * @public
 * @param {string} string - The source string.
 * @param {string} word - A word which shall be part of the `str`.
 * @param {boolean} [ignoreEscapeSequences=true] - ANSI escape sequences for coloring in `str` shall be ignored.
 * @returns {void}
 * @throws {assert.AssertionError}
 */
function assertHasWord (string, word, ignoreEscapeSequences = true) {
	if (!hasWord(string, word, ignoreEscapeSequences)) {
		throw new assert.AssertionError({
			message: 'Input A expected to include word input B',
			actual: string,
			expected: word,
			operator: 'assertHasWord',
			stackStartFn: assertHasWord
		});
	}
}

/**
 * Assert that a string does not contains a specific word, considering word boundaries.
 *
 * @public
 * @param {string} string - The source string.
 * @param {string} word - A word which shall not be part of `str`.
 * @param {boolean} [ignoreEscapeSequences=true] - ANSI escape sequences for coloring in `str` shall be ignored.
 * @returns {void}
 * @throws {assert.AssertionError}
 */
function assertNotHasWord (string, word, ignoreEscapeSequences = true) {
	if (hasWord(string, word, ignoreEscapeSequences)) {
		throw new assert.AssertionError({
			message: 'Input A expected to not include word input B',
			actual: string,
			expected: word,
			operator: 'assertNotHasWord',
			stackStartFn: assertNotHasWord
		});
	}
}

/**
 * Returns true if the given string contains a specific word, considering word boundaries.
 *
 * @private
 * @param {string} string - The source string.
 * @param {string} word - A word which shall be part of `str`.
 * @param {boolean} ignoreEscapeSequences - ANSI escape sequences for coloring in `str` shall be ignored.
 * @returns {boolean} If `str` contains `word`, `true` is returned, otherwise `false`.
 */
function hasWord (string, word, ignoreEscapeSequences) {
	return new RegExp(`(^|[^A-Za-z0-9$-_])${escapeRegExp(word)}($|[^A-Za-z0-9$-_])`, 'um').test(ignoreEscapeSequences ? string.replace(/\u001B\[.+?m/gu, '') : string);
}

/**
 * Escape string to use it as part of a RegExp pattern.
 *
 * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping
 *
 * @private
 * @param {string} string - The source string.
 * @returns {string} The escaped string.
 */
function escapeRegExp (string) {
	return string.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
}

module.exports = {
	assertHasWord,
	assertNotHasWord
};
<file_sep>/**
 * @file File- and path-handling related functionality.
 */

const fs = require('fs');
const path = require('path');

/**
 * Structure of the package.json.
 *
 * @typedef {{
 *   readonly name?: string;
 *   readonly version?: string;
 *   readonly homepage?: string;
 *   readonly repository?: string | {
 *     readonly type?: string;
 *     readonly url?: string;
 *     readonly directory?: string
 *   };
 *   readonly author?: string | {
 *     readonly name?: string;
 *     readonly email?: string;
 *     readonly url?: string
 *   };
 * }} PackageJSON
 */

/**
 * Returns the content of the package.json of a given dependency.
 *
 * @public
 * @param {string} dependencyLocation - The folder where the dependency is located.
 * @returns {PackageJSON} Either the content of the package.json or an empty object.
 */
function getDependencyPackageJSON (dependencyLocation) {
	const filePath = path.join(getRelativeDependencyPath(dependencyLocation), 'package.json');
	const fileContent = readFile(filePath);

	if (fileContent !== undefined) {
		try {
			return JSON.parse(fileContent);
		}
		catch { /* We ignore errors here */ }
	}

	return {};
}

/**
 * Returns the content of the package.json of a given dependency.
 *
 * @public
 * @param {string} dependencyLocation - The folder where the dependency is located.
 * @returns {string} Either the content of the package.json or an empty object.
 */
function getParentPackageJSONPath (dependencyLocation) {
	let filePath = path.resolve(process.cwd(), getRelativeDependencyPath(dependencyLocation));

	while (filePath !== '' && path.basename(filePath) !== 'node_modules') {
		filePath = path.dirname(filePath);
	}

	return path.join(path.dirname(filePath), 'package.json');
}

/**
 * Returns the file path to the CHANGELOG.md of a given dependency.
 *
 * @public
 * @param {string} dependencyLocation - The folder where the dependency is located.
 * @returns {string | undefined} Either the file path or undefined, if the file does not exist.
 */
function getChangelogPath (dependencyLocation) {
	const filePath = path.join(getRelativeDependencyPath(dependencyLocation), 'CHANGELOG.md');

	try {
		if (fs.existsSync(filePath)) {
			return path.relative(process.cwd(), filePath).replace(/\\/gu, '/');
		}
	}
	catch { /* We ignore errors here */ }

	return;
}

/**
 * Returns the content of a file.
 *
 * @public
 * @param {string} filePath - The path/file name.
 * @returns {string | undefined} The content of the file or `undefined`, if an error occurs.
 */
function readFile (filePath) {
	try {
		return fs.readFileSync(filePath, 'utf8');
	}
	catch { /* Do nothing here, but return undefined in the next step */ }

	return;
}

/**
 * Returns the relative path to dependency to the current working directory.
 *
 * @private
 * @param {string} dependencyLocation - The folder where the dependency is located.
 * @returns {string} The path to a dependency.
 */
function getRelativeDependencyPath (dependencyLocation) {
	return path.relative(process.cwd(), dependencyLocation.replace(/^node_modules[\\/]/u, path.join(process.cwd(), 'node_modules/'))).replace(/\\/gu, '/');
}

module.exports = {
	getChangelogPath,
	getDependencyPackageJSON,
	getParentPackageJSONPath,
	readFile
};
<file_sep>/**
 * @file Acquires and prepares outdated dependencies.
 */

const childProcess = require('child_process');

/**
 * One dependency item, returned by `npm outdated --json`.
 *
 * @typedef {object} OutdatedDependency
 * @property {string} name
 * @property {string} current
 * @property {string} wanted
 * @property {string} latest
 * @property {string} location
 * @property {'dependencies' | 'devDependencies'} [type]
 * @property {string} [homepage]
 */

/**
 * Original npm-outdated object, returned by `npm outdated --json`.
 *
 * @typedef {{ [dependencyName: string]: OutdatedDependency; }} OutdatedDependencies
 */

/**
 * One dependency item, returned by `npm outdated --json`.
 *
 * @typedef {object} NpmOptions
 * @property {boolean} [global]
 * @property {number} [depth]
 */

/**
 * Calls `npm outdated` to retrieve information about the outdated dependencies.
 *
 * @public
 * @param {NpmOptions} options - Options which shall be appended to the `npm outdated` command-line call.
 * @returns {Promise<OutdatedDependencies>} The original object returned by `npm outdated --json`.
 */
async function getOutdatedDependencies (options) {
	return new Promise((resolve, reject) => {
		childProcess.exec([
			'npm outdated',
			'--json',
			'--long',
			'--save false',
			(options.global ? '--global' : ''),
			(options.depth ? `--depth ${options.depth}` : '')
		].filter((item) => item).join(' '), (error, stdout) => {
			if (error && stdout.length === 0) {
				reject(error);

				return;
			}

			const response = parseResponse(stdout);

			if ('error' in response) {
				reject(response.error);

				return;
			}

			if (typeof response !== 'object' || response === null) {
				reject(new TypeError('npm did not respond with an object.'));

				return;
			}

			resolve(prepareResponseObject(response));
		});
	});
}

/**
 * Compare function used with `Array.sort()` to sort outdated dependencies by their name.
 *
 * @public
 * @param {OutdatedDependency} firstDependency - First dependency objects.
 * @param {OutdatedDependency} secondDependency - Second dependency objects.
 * @returns {-1 | 0 | 1} - Defines the sorting order.
 */
function compareByName (firstDependency, secondDependency) {
	if (firstDependency.name < secondDependency.name) {
		return -1;
	}
	else if (firstDependency.name > secondDependency.name) {
		return 1;
	}

	return 0;
}

/**
 * Compare function used with `Array.sort()` to sort outdated dependencies primary by their type, secondary by their name.
 *
 * @public
 * @param {OutdatedDependency} firstDependency - First dependency objects.
 * @param {OutdatedDependency} secondDependency - Second dependency objects.
 * @returns {-1 | 0 | 1} - Defines the sorting order.
 */
function compareByType (firstDependency, secondDependency) {
	const first = `${(firstDependency.type === 'dependencies' ? 'A' : 'B')}${firstDependency.name}`;
	const second = `${(secondDependency.type === 'dependencies' ? 'A' : 'B')}${secondDependency.name}`;

	if (first < second) {
		return -1;
	}
	else if (first > second) {
		return 1;
	}

	return 0;
}

/**
 * Adds missing properties to the dependencies object.
 *
 * @private
 * @param {{ readonly [dependencyName: string]: Partial<OutdatedDependency>; }} dependencies - The partial filled outdated dependency object.
 * @returns {{ [dependencyName: string]: OutdatedDependency; }} The enriched outdated dependency object.
 */
function prepareResponseObject (dependencies) {
	/** @type {{ [dependencyName: string]: OutdatedDependency; }} */
	const outdatedDependencies = {};

	for (const [name, dependency] of Object.entries(dependencies)) {
		// Adding the name, makes it easier to work with the dependency object.
		const outdatedDependency = {
			...dependency,
			name
		};

		outdatedDependency.current = outdatedDependency.current || '';
		outdatedDependency.wanted = outdatedDependency.wanted || '';
		outdatedDependency.latest = outdatedDependency.latest || '';

		/**
		 * Sometimes, npm returns an empty `location` string. So we add it.
		 *
		 * @todo We should try to resolve the path on the same way as npm is doing it.
		 *
		 * @see path.relative(process.cwd(), require.resolve(name));
		 * @see module.path
		 */
		if (!outdatedDependency.location) {
			outdatedDependency.location = `node_modules/${name}`;
		}

		outdatedDependencies[name] = /** @type {OutdatedDependency} */(outdatedDependency);
	}

	return outdatedDependencies;
}

/**
 * Parse the stdout of `npm outdated --json` and convert it into an `object`.
 *
 * @private
 * @param {string} stdout - Response of `npm outdated --json`.
 * @returns {any} The parsed response, or an `object` containing an `error` property.
 */
function parseResponse (stdout) {
	try {
		const response = JSON.parse(stdout || '{}');

		if (typeof response !== 'object' || response === null) {
			throw new Error('Unexpected JSON response');
		}

		return response;
	}
	catch (error) {
		if (error instanceof Error) {
			return {
				error: {
					message: error.message,
					stack: error.stack,
					source: stdout
				}
			};
		}

		return {
			message: (typeof error === 'string' ? error : 'Unknown error'),
			source: stdout
		};
	}
}

module.exports = {
	getOutdatedDependencies,
	compareByName,
	compareByType
};
<file_sep>#!/usr/bin/env node

/**
 * @file The CLI entry point.
 */

/**
 * @external NodeModule
 */

const parseArguments = require('./helper/args');
const colorize = require('./helper/colorize');
const { getOutdatedDependencies, compareByName, compareByType } = require('./helper/dependencies');
const { getChangelogPath, getDependencyPackageJSON, getParentPackageJSONPath, readFile } = require('./helper/files');
const generateKeyValueList = require('./helper/list');
const { getRegExpPosition, escapeRegExp } = require('./helper/regexp');
const { semverDiff, semverDiffType, semverInRange } = require('./helper/semver');
const prettifyTable = require('./helper/table');
const { getNpmJSLink, getPackageAuthor, getPackageHomepage, getPackageRepository } = require('./helper/urls');
const pkg = require('./package.json');

/**
 * @typedef {import('./helper/dependencies').NpmOptions} NpmOptions
 * @typedef {import('./helper/dependencies').OutdatedDependencies} OutdatedDependencies
 * @typedef {import('./helper/dependencies').OutdatedDependency} OutdatedDependency
 * @typedef {import('./helper/files').PackageJSON} PackageJSON
 * @typedef {import('./helper/table').Table} Table
 * @typedef {import('./helper/table').TableColumn} TableColumn
 * @typedef {import('./helper/args').AvailableArguments} AvailableArguments
 */

/**
 * The options based on the CLI arguments.
 *
 * @typedef {object} CheckOutdatedOptions
 * @property {string[]} [ignorePackages]
 * @property {boolean} [ignoreDevDependencies]
 * @property {boolean} [ignorePreReleases]
 * @property {boolean} [preferWanted]
 * @property {string[]} [columns]
 * @property {string[]} [types]
 */

/** @typedef {CheckOutdatedOptions & NpmOptions} Options */

/**
 * Array of outdated dependencies.
 *
 * @typedef {OutdatedDependency[]} Dependencies
 */

/**
 * The details change can be used to share data between columns.
 * For example, if the first column reads the package.json, the next column can rely of this data, without to acquire it again.
 *
 * @typedef {object} DependencyDetailsCache
 * @property {[string, string]} [semverDiff]
 * @property {PackageJSON} [packageJSON]
 */

const DEFAULT_COLUMNS = ['package', 'current', 'wanted', 'latest', 'reference', 'changes', 'location'];

/**
 * @typedef {object} Column
 * @property {TableColumn | string} caption;
 * @property {(dependency: OutdatedDependency, options: Options, detailsCache: DependencyDetailsCache) => Promise<TableColumn | string>} getValue
 */

/** @type {{ [filePath: string]: string }} */
const packageJsonCache = {};

/** @type {{ readonly [columnName: string]: Column; }} */
const AVAILABLE_COLUMNS = {
	package: {
		caption: colorize.underline('Package'),
		getValue: async (dependency, options) => {
			switch (semverDiffType(dependency.current, getWantedOrLatest(dependency, options))) {
				case 'major':
					return colorize.yellow(dependency.name);

				case 'minor':
					return colorize.cyan(dependency.name);

				case 'patch':
					return colorize.green(dependency.name);

				case 'revert':
					return colorize.red(dependency.name);

				default:
					return dependency.name;
			}
		}
	},
	current: {
		caption: {
			text: colorize.underline('Current'),
			alignRight: true
		},
		getValue: async (dependency, options) => {
			if (dependency.current === '') {
				return {
					text: colorize.gray('-'),
					alignRight: true
				};
			}

			const diff = semverDiff(
				[dependency.current, getWantedOrLatest(dependency, options)],
				[colorize, colorize.magenta],
				[colorize.underline, colorize.magenta.underline]
			);

			return {
				text: diff[0],
				alignRight: true
			};
		}
	},
	wanted: {
		caption: {
			text: colorize.underline('Wanted'),
			alignRight: true
		},
		getValue: async (dependency) => {
			if (dependency.wanted === '') {
				return {
					text: colorize.gray('-'),
					alignRight: true
				};
			}

			const diff = semverDiff(
				[dependency.current, dependency.wanted],
				[colorize, colorize.green],
				[colorize.underline, colorize.green.underline]
			);

			return {
				text: diff[1],
				alignRight: true
			};
		}
	},
	latest: {
		caption: {
			text: colorize.underline('Latest'),
			alignRight: true
		},
		getValue: async (dependency) => {
			if (dependency.latest === '') {
				return {
					text: colorize.gray('-'),
					alignRight: true
				};
			}

			const diff = semverDiff(
				[dependency.current, dependency.latest],
				[colorize, colorize.magenta],
				[colorize.underline, colorize.magenta.underline]
			);

			return {
				text: diff[1],
				alignRight: true
			};
		}
	},
	type: {
		caption: colorize.underline('Type'),
		getValue: async (dependency, options) => (semverDiffType(dependency.current, getWantedOrLatest(dependency, options)) || colorize.gray('-'))
	},
	location: {
		caption: colorize.underline('Location'),
		getValue: async (dependency) => dependency.location || colorize.gray('-')
	},
	packageType: {
		caption: colorize.underline('Package Type'),
		getValue: async (dependency) => dependency.type || colorize.gray('-')
	},
	reference: {
		caption: colorize.underline('Reference'),
		getValue: async (dependency) => {
			const filePath = getParentPackageJSONPath(dependency.location);
			let fileContent = packageJsonCache[filePath] || readFile(filePath);

			if (fileContent !== undefined) {
				fileContent = fileContent.replace(/\r\n|\r/gu, '\n');

				if (!('filePath' in packageJsonCache)) {
					packageJsonCache[filePath] = fileContent;
				}

				const json = JSON.parse(fileContent);
				const actualVersion = (dependency.type && dependency.type in json ? json[dependency.type][dependency.name] : undefined);

				const needle = new RegExp(`"${escapeRegExp(dependency.name)}"[^:]*:[^"]*"[^"]*${(actualVersion ? escapeRegExp(actualVersion) : '')}"`, 'u');
				const [line, column] = getRegExpPosition(fileContent, needle);

				if (line && column) {
					return `${filePath}:${line}:${column}`;
				}
			}

			return colorize.gray('-');
		}
	},
	changes: {
		caption: colorize.underline('Changes'),
		getValue: async (dependency, _options, detailsCache) => {
			detailsCache.packageJSON = detailsCache.packageJSON || getDependencyPackageJSON(dependency.location);

			return (
				await getPackageRepository(detailsCache.packageJSON, true) ||
				getPackageHomepage(detailsCache.packageJSON) ||
				getNpmJSLink(dependency.name) ||
				colorize.gray('-')
			);
		}
	},
	changesPreferLocal: {
		caption: colorize.underline('Changes'),
		getValue: async (dependency, _options, detailsCache) => {
			const changelogFile = getChangelogPath(dependency.location);

			if (changelogFile) {
				return changelogFile;
			}

			detailsCache.packageJSON = detailsCache.packageJSON || getDependencyPackageJSON(dependency.location);

			return (
				await getPackageRepository(detailsCache.packageJSON, true) ||
				getPackageHomepage(detailsCache.packageJSON) ||
				getNpmJSLink(dependency.name) ||
				colorize.gray('-')
			);
		}
	},
	homepage: {
		caption: colorize.underline('Homepage'),
		getValue: async (dependency, _options, detailsCache) => {
			detailsCache.packageJSON = detailsCache.packageJSON || getDependencyPackageJSON(dependency.location);

			return (
				dependency.homepage ||
				getPackageHomepage(detailsCache.packageJSON) ||
				await getPackageRepository(detailsCache.packageJSON) ||
				getPackageAuthor(detailsCache.packageJSON) ||
				getNpmJSLink(dependency.name) ||
				colorize.gray('-')
			);
		}
	},
	npmjs: {
		caption: colorize.underline('npmjs.com'),
		getValue: async (dependency) => getNpmJSLink(dependency.name) || colorize.gray('-')
	}
};

/**
 * @deprecated `name` has been replaced by `package` in version 2.8.0.
 */
// @ts-expect-error -- That's the easiest way the clone the `package` property values
AVAILABLE_COLUMNS.name = AVAILABLE_COLUMNS.package;

/** @type {AvailableArguments} */
const AVAILABLE_ARGUMENTS = {
	'--help': () => help(),
	'-h': () => help(),
	'--ignore-pre-releases': {
		ignorePreReleases: true
	},
	'--ignore-dev-dependencies': {
		ignoreDevDependencies: true
	},
	'--ignore-packages': (value) => {
		const ignorePackages = value.split(',');

		if (ignorePackages.length === 1 && (ignorePackages[0] === '' || ignorePackages[0].startsWith('-'))) {
			return help('Invalid value of --ignore-packages');
		}

		return { ignorePackages };
	},
	'--prefer-wanted': {
		preferWanted: true
	},
	'--columns': (value) => {
		const columns = value.split(',');
		const availableColumnsNames = Object.keys(AVAILABLE_COLUMNS);

		if (columns.length === 1 && (columns[0] === '' || columns[0].startsWith('-'))) {
			return help('Invalid value of --columns');
		}

		const invalidColumn = columns.find((name) => !availableColumnsNames.includes(name));
		if (invalidColumn) {
			return help(`Invalid column name "${invalidColumn}" in --columns\nAvailable columns are:\n${availableColumnsNames.join(', ')}`);
		}

		return { columns };
	},
	'--types': (value) => {
		const types = value.split(',');
		const availableTypesNames = ['major', 'minor', 'patch', 'prerelease', 'build', 'reverted'];

		if (types.length === 1 && (types[0] === '' || types[0].startsWith('-'))) {
			return help('Invalid value of --types');
		}

		const invalidType = types.find((name) => !availableTypesNames.includes(name));
		if (invalidType) {
			return help(`Invalid type name "${invalidType}" in --types\nAvailable types are:\n${availableTypesNames.join(', ')}`);
		}

		return { types };
	},
	'--global': {
		global: true
	},
	'--depth': (value) => {
		const depth = Number.parseInt(value, 10);

		if (!Number.isFinite(depth)) {
			return help('Invalid value of --depth');
		}

		return { depth };
	}
};

if (require.main === /** @type {NodeModule} */(/** @type {any} */(module))) {
	process.title = pkg.name;
	const argv = process.argv.slice(2);

	void (async () => {
		process.exitCode = await checkOutdated(argv);
	})();
}
else {
	module.exports = checkOutdated;
}

/**
 * The main functionality of the tool.
 *
 * @public
 * @param {string[]} argv - Arguments given in the command line (`process.argv.slice(2)`).
 * @returns {Promise<number>} A number which shall used as process exit code.
 */
async function checkOutdated (argv) {
	/** @type {Options | string} */
	let args;

	try {
		args = /** @type {Options | string} */(parseArguments(argv, AVAILABLE_ARGUMENTS));
	}
	catch (error) {
		if (error instanceof Error) {
			args = help(error.message);
		}
		else {
			args = help();
		}
	}

	if (typeof args === 'string') {
		process.stdout.write(args);

		return 1;
	}

	try {
		const outdatedDependencies = Object.values(await getOutdatedDependencies(args));
		const filteredDependencies = getFilteredDependencies(outdatedDependencies, args);

		if (filteredDependencies.length === 0) {
			process.stdout.write('All dependencies are up-to-date.\n');

			return 0;
		}

		if (filteredDependencies.length === 1) {
			process.stdout.write('1 outdated dependency found:\n\n');
		}
		else {
			process.stdout.write(`${filteredDependencies.length} outdated dependencies found:\n\n`);
		}

		const visibleColumns = (args.columns === undefined || args.columns.length === 0 ? DEFAULT_COLUMNS : args.columns);

		await writeOutdatedDependenciesToStdout(visibleColumns, filteredDependencies, args);

		writeUnnecessaryIgnoredPackagesToStdout(filteredDependencies, args);
	}
	catch (error) {
		if (typeof error === 'object' && error !== null) {
			// Required for TypeScript to ensure the type is an `object` instead of `unknown`.
			/** @type {Record<string, any>} */
			const errorObject = error;

			const out = generateKeyValueList(Object.getOwnPropertyNames(errorObject).map((property) => [colorize.magenta(property), errorObject[property]]));

			process.stdout.write(`${colorize.red('Error while gathering outdated dependencies:')}\n\n${out}\n`);
		}
		else {
			process.stdout.write(`${colorize.red('Unknown error while gathering outdated dependencies.')}\n`);
		}
	}

	return 1;
}

/**
 * Returns the help text of the CLI tool.
 *
 * @private
 * @param {string[]} additionalLines - Additional text (error messages etc.) which shall be shown after the help.
 * @returns {string} Multiline text containing the whole help text.
 */
function help (...additionalLines) {
	return [
		`${pkg.name} v${pkg.version} - ${pkg.description}`,
		'Usage: ',
		[
			'[--ignore-pre-releases]',
			'[--ignore-dev-dependencies]',
			'[--ignore-packages <comma-separated-list-of-package-names>]',
			'[--prefer-wanted]',
			'[--columns <comma-separated-list-of-columns>]',
			'[--types <comma-separated-list-of-update-types>]',
			'[--global]',
			'[--depth <number>]'
		].join(' '),
		'',
		'Arguments:',
		prettifyTable([
			[
				'--help, -h',
				'Show this help.'
			],
			[
				'--ignore-pre-releases',
				'Don\'t recommend to update to versions which contain a hyphen (e.g. "2.1.0-alpha", "2.1.0-beta", "2.1.0-rc.1").'
			],
			[
				'--ignore-dev-dependencies',
				'Do not warn if devDependencies are outdated.'
			],
			[
				'--ignore-packages <comma-separated-list-of-package-names>',
				'Ignore the listed packages, even if they are outdated.'
			],
			[
				'--prefer-wanted',
				'Compare the "Current" version to the "Wanted" version, instead of the "Latest" version.'
			],
			[
				'--columns <comma-separated-list-of-columns>',
				'Defines which columns should be shown in which order.'
			],
			[
				// Follow-up line for '--columns' description
				'',
				`Possible values: ${Object.keys(AVAILABLE_COLUMNS).join(',')}`
			],
			[
				'--types <comma-separated-list-of-update-types>',
				'Restrict the update type (e.g. only show minor updates, or reverted versions)'
			],
			[
				// Follow-up line for '--types' description
				'',
				'Possible values: major,minor,patch,prerelease,build,reverted'
			],
			[
				'--global',
				'Check packages in the global install prefix instead of in the current project (equal to the npm outdated-option).'
			],
			[
				'--depth <number>',
				'Max depth for checking dependency tree (equal to the npm outdated-option).'
			]
		]),
		...(Array.isArray(additionalLines) ? [''].concat(additionalLines) : []),
		''
	].join('\n');
}

/**
 * Filters dependencies by the given filter `options`.
 *
 * @private
 * @param {Dependencies} dependencies - Array of dependency objects which shall be filtered.
 * @param {Options} options - Options to configure the filtering.
 * @returns {Dependencies} Array with of the filtered dependency objects.
 */
function getFilteredDependencies (dependencies, options) {
	let filteredDependencies = dependencies.filter((dependency) => (
		!['git', 'linked', 'remote'].includes(getWantedOrLatest(dependency, options))
	));

	if (options.ignorePackages) {
		const ignorePackages = options.ignorePackages;
		const packageVersionRegExp = /^(.+?)@(.*)$/u;

		filteredDependencies = filteredDependencies.filter((dependency) => {
			for (const ignoredPackage of ignorePackages) {
				const match = packageVersionRegExp.exec(ignoredPackage);

				if (match === null) {
					if (ignoredPackage === dependency.name) {
						return false;
					}
				}
				else {
					if (match[1] === dependency.name) {
						if (semverInRange(getWantedOrLatest(dependency, options), match[2])) {
							return false;
						}
					}
				}
			}

			return true;
		});
	}

	if (options.ignoreDevDependencies) {
		filteredDependencies = filteredDependencies.filter(({ type }) => (
			type !== 'devDependencies'
		));
	}

	if (options.ignorePreReleases) {
		filteredDependencies = filteredDependencies.filter((dependency) => (
			!dependency.current.includes('-') && !getWantedOrLatest(dependency, options).includes('-')
		));
	}

	if (options.preferWanted) {
		filteredDependencies = filteredDependencies.filter(({ current, wanted }) => current !== wanted);
	}

	if (options.types) {
		filteredDependencies = filteredDependencies.filter(({ current, latest }) => options.types && options.types.includes(semverDiffType(current, latest) || ''));
	}

	return filteredDependencies;
}

/**
 * Depending on the `preferWanted` option, either the `wanted` or the `latest` property of a dependency is returned.
 *
 * @param {OutdatedDependency} dependency - A specific outdated dependency.
 * @param {Options} options - The arguments which the user provided.
 * @returns {string} `wanted` or `latest`
 */
function getWantedOrLatest (dependency, options) {
	return (options.preferWanted ? dependency.wanted : dependency.latest);
}

/**
 * Show the version information of outdated dependencies in a styled way on the terminal (stdout).
 *
 * @private
 * @param {string[]} visibleColumns - The columns which should be shown in the given order.
 * @param {Dependencies} dependencies - Array of dependency objects, which shall be formatted and shown in the terminal.
 * @param {Options} options - The arguments which the user provided.
 * @returns {Promise<void>}
 */
async function writeOutdatedDependenciesToStdout (visibleColumns, dependencies, options) {
	/** @type {(string | (string | TableColumn)[] | Promise<string | (string | TableColumn)[]>)[]} */
	const table = [
		visibleColumns.map((columnName) => AVAILABLE_COLUMNS[columnName].caption)
	];
	const groupByPackageType = !visibleColumns.includes('packageType');

	/** @type {undefined | string} */
	let previousPackageTypeGroup;

	dependencies.sort((groupByPackageType ? compareByType : compareByName));

	for (const dependency of dependencies) {
		if (groupByPackageType && previousPackageTypeGroup !== dependency.type) {
			table.push(`\n${colorize.underline(dependency.type || colorize.gray('unknown'))}`);

			previousPackageTypeGroup = dependency.type;
		}

		table.push((async () => {
			/** @type {DependencyDetailsCache} */
			const dependencyDetailsCache = {};

			return Promise.all(visibleColumns.map(async (columnName) => AVAILABLE_COLUMNS[columnName].getValue(dependency, options, dependencyDetailsCache)));
		})());
	}

	process.stdout.write([
		prettifyTable(await Promise.all(table)),
		'',
		colorize.underline('Color legend'),
		`${colorize.yellow('Major update')}: backward-incompatible updates`,
		`${colorize.cyan('Minor update')}: backward-compatible features`,
		`${colorize.green('Patch update')}: backward-compatible bug fixes`,
		`${colorize.red('Reverted')}:     latest available version is lower than the installed version`,
		'',
		''
	].join('\n'));
}

/**
 * Show information about packages which are ignored by `--ignore-packages` with version number, but where the `latest` version differs.
 *
 * Example:
 * Current "module" version: 2.0.0
 * Latest "module" version: 2.0.2
 * --ignore-packages module@^1
 * --ignore-packages module@2.0.1
 * In these cases, the ignore-statements have no effect, because version ^1 and 2.0.1 are outdated. That means, the ignore-statement can be removed.
 *
 * @private
 * @param {Dependencies} filteredDependencies - Array of dependency objects, which will be shown in the terminal.
 * @param {Options} options - The arguments which the user provided.
 * @returns {void}
 */
function writeUnnecessaryIgnoredPackagesToStdout (filteredDependencies, options) {
	const packageVersionRegExp = /^(.+?)@(.*)$/u;

	if (!options.ignorePackages) {
		return;
	}

	for (const ignoredPackage of options.ignorePackages) {
		const match = packageVersionRegExp.exec(ignoredPackage);

		if (match !== null) {
			const dependency = filteredDependencies.find(({ name }) => name === match[1]);

			if (dependency) {
				process.stdout.write(`The --ignore-packages filter "${ignoredPackage}" has no effect, the latest version is ${dependency.latest}.\n\n`);
			}
		}
	}
}
<file_sep>/**
 * @file Create a demo result for the `screenshot.png`
 */

const { stub } = require('../tests/helper/stub');

const mockData = /** @type {import('../tests/index').MockData} */(require('./mock-data.json'));

const checkOutdated = stub(mockData, mockData.defaultResponse);

/** @type {string[]} */
const argv = [];

void (async () => {
	// eslint-disable-next-line no-console -- Add some spacing for the screenshot
	console.log();

	await checkOutdated(argv);

	// eslint-disable-next-line no-console -- Add some spacing for the screenshot
	console.log();
})();
<file_sep>/**
 * @file ESLint configuration
 */

module.exports = {
	extends: [
		require.resolve('linter-bundle/eslint'),
		require.resolve('linter-bundle/eslint/overrides-javascript-lazy'),
		require.resolve('linter-bundle/eslint/overrides-jsdoc'),
		require.resolve('linter-bundle/eslint/overrides-type-declarations')
	],
	rules: {
		'@typescript-eslint/prefer-nullish-coalescing': 'off', // Disabled as we are supporting Node.js v10, which does not support Nullish Coalescing
		'@typescript-eslint/prefer-optional-chain': 'off', // Disabled as we are supporting Node.js v10, which does not support Nullish Coalescing
		'logical-assignment-operators': 'off' // Disabled as we are supporting Node.js v10, which does not support Logical assignment operators
	}
};
<file_sep>/**
 * @file Assertion methods.
 */

const path = require('path');

const proxyquire = require('proxyquire').noPreserveCache();

/** @typedef {import('../index').MockData} MockData */

/**
 * Stub native modules used by check-outdated.
 *
 * @param {MockData | undefined} mockData - Mock data to stub native modules.
 * @param {any} dependencies - Mock of the `npm outdated --json` response.
 * @param {(command: string) => void} [setUsedCommand] - Callback function which receives the used `child_process` command.
 * @returns {import('../../check-outdated')} New check-outdated with stubbed native modules.
 */
function stub (mockData, dependencies, setUsedCommand) {
	/** @type {import('../../check-outdated')} */
	return proxyquire(path.join(process.cwd(), 'check-outdated'), {
		'./helper/dependencies': proxyquire(path.join(process.cwd(), 'helper/dependencies'), {
			child_process: {
				/**
				 * Mock of the child_process.exec() function, which is used by `check-outdated` to call `npm outdated`.
				 *
				 * @param {string} command - The command to run.
				 * @param {(error: Error | null, stdout: string, stderr: string) => void} callback - Called with the output when process terminates.
				 * @returns {void}
				 */
				exec (command, callback) {
					if (typeof setUsedCommand === 'function') {
						setUsedCommand(command);
					}

					callback(null, (typeof dependencies === 'string' ? dependencies : JSON.stringify(dependencies)), '');
				}
			}
		}),
		'./helper/files': proxyquire(path.join(process.cwd(), 'helper/files'), {
			fs: {
				/**
				 * Mock of the fs.existsSync() function, which is used by `check-outdated` to figure out of CHANGELOG.md in the package folder exists.
				 *
				 * @param {string | Buffer | URL} filePath - Filename or file descriptor.
				 * @returns {boolean} Returns true if the `filePath` exists, false otherwise.
				 * @throws Error if no mock data for the file exist.
				 */
				existsSync (filePath) {
					if (typeof filePath !== 'string') {
						throw new TypeError('fs.existsSync(): Mock only support strings as path.');
					}

					if (mockData === undefined) { return false; }

					const normalizedPath = filePath.replace(/\\/gu, '/');

					if (!(normalizedPath in mockData.fsExists)) {
						throw new Error(`fs.existsSync(): Mocked data for "${normalizedPath}" not found.`);
					}

					return mockData.fsExists[normalizedPath];
				},

				/**
				 * Mock of the fs.readFileSync() function, which is used by `check-outdated` to read package.json files.
				 *
				 * @param {string | Buffer | URL | number} filePath - Filename or file descriptor.
				 * @param {{ encoding?: string | null; flag?: string; } | string} options - Either an object, or an string representing the encoding.
				 * @returns {string | Buffer} Returns the contents of the `filePath`.
				 * @throws Error if no mock data for the file exist.
				 */
				readFileSync (filePath, options) {
					if (typeof filePath !== 'string') {
						throw new TypeError('fs.readFileSync(): Mock only support strings as path.');
					}

					if (options !== 'utf8') {
						throw new Error('fs.readFileSync(): Mock only support "utf8" encoding.');
					}

					if (mockData === undefined) { return ''; }

					const normalizedPath = filePath.replace(/\\/gu, '/');

					if (!(normalizedPath in mockData.fsReadFile)) {
						throw new Error(`fs.readFileSync(): Mocked data for "${normalizedPath}" not found.`);
					}

					const content = mockData.fsReadFile[normalizedPath];

					return (typeof content === 'string' ? content : JSON.stringify(content, null, '  '));
				}
			},
			path: {
				/**
				 * Mock of the path.resolve() function, which is used by `check-outdated` to get the absolute path of the referencing package.json.
				 *
				 * @param {string[]} pathSegments - A sequence of paths or path segments.
				 * @returns {string} Returns an absolute path.
				 * @throws Error if the number of arguments is not 2, or if if the second argument is not a string.
				 */
				resolve (...pathSegments) {
					if (pathSegments.length !== 2) {
						throw new RangeError('path.resolve(): Mock expects exactly 2 path segments.');
					}

					// eslint-disable-next-line no-unnecessary-typeof -- From the Node.js documentation: "A `TypeError` is thrown if any of the arguments is not a string.""
					if (typeof pathSegments[1] !== 'string') {
						throw new TypeError('path.resolve(): Mock expects the second path segment to be an string.');
					}

					return pathSegments[1];
				}
			}
		}),
		'./helper/urls': proxyquire(path.join(process.cwd(), 'helper/urls'), {
			https: {
				/**
				 * Mock of the https.get() function, which is used by `check-outdated` to check if a file exists on a specific server.
				 *
				 * @param {{ host: string; path: string; }} options - The command to run.
				 * @param {(
				 *   res: {
				 *     statusCode: number;
				 *     on (event: 'data' | 'end' | 'error', listener: (...args: any[]) => void): void;
				 *     setEncoding (encoding: string): void, destroy (): void;
				 *   }
				 * ) => void} callback - Called with the output when process terminates.
				 * @returns {void}
				 */
				get (options, callback) {
					const STATUS_NOT_FOUND = 404;

					/** @type {{ statusCode: number; data?: string; }} */
					const response = (mockData && mockData.httpsGet[`${options.host}${options.path}`]) || { statusCode: STATUS_NOT_FOUND };

					// eslint-disable-next-line n/no-callback-literal -- `callback()` is not using the Node.js error-first callback pattern.
					callback({
						...response,
						on (event, listener) {
							switch (event) {
								case 'data':
									if (typeof response.data === 'string') {
										listener(response.data);
									}
									break;

								case 'end':
									listener();
									break;

								default:
							}
						},
						setEncoding () { /* Do nothing */ },
						destroy () { /* Do nothing */ }
					});
				}
			}
		})
	});
}

module.exports = {
	stub
};
<file_sep>/**
 * @file Generate URLs.
 */

const https = require('https');

const STATUS_OK = 200;

/** @typedef {import('./files').PackageJSON} PackageJSON */

/**
 * @type {readonly {
 *   readonly regExp: RegExp;
 *   readonly getRepositoryURL: (match: readonly string[], directory: string) => string;
 *   readonly getChangelogURL: (match: readonly string[], directory: string) => Promise<string> | string;
 * }[]}
 */
const REPOSITORY_URLS = [
	{
		regExp: /^github:(.+)/u,
		getRepositoryURL: (match, directory) => `https://github.com/${match[1]}${directory}`,
		getChangelogURL: (match) => `https://github.com/${match[1]}/releases`
	},
	{
		regExp: /^gist:(.+)/u,
		getRepositoryURL: (match, directory) => `https://gist.github.com/${match[1]}${directory}`,
		getChangelogURL: (match) => `https://gist.github.com/${match[1]}/revisions`
	},
	{
		regExp: /^bitbucket:(.+)/u,
		getRepositoryURL: (match, directory) => `https://bitbucket.org/${match[1]}${directory}`,
		// @todo Right now, I can't find a reference, to a public bitbucket repository
		getChangelogURL: (match) => `https://bitbucket.org/${match[1]}`
	},
	{
		regExp: /^gitlab:(.+)/u,
		getRepositoryURL: (match) => `https://gitlab.com/${match[1]}`,
		getChangelogURL: (match) => `https://gitlab.com/${match[1]}/-/releases`
	},
	{
		regExp: /^git\+(https?:\/\/.+)\.git$/u,
		getRepositoryURL: (match, directory) => `${match[1]}${directory}`,
		getChangelogURL: async (match, directory) => getChangelogFromURL(match[1], directory)
	},
	{
		regExp: /^git\+ssh:\/\/git@(.+)\.git$/u,
		getRepositoryURL: (match, directory) => `https://${match[1]}${directory}`,
		getChangelogURL: async (match, directory) => getChangelogFromURL(`https://${match[1]}`, directory)
	},
	// Fallback (should be the last item)
	{
		regExp: /^(https?:\/\/.+?)(\.git)?$/u,
		getRepositoryURL: (match, directory) => `${match[1]}${directory}`,
		getChangelogURL: async (match, directory) => getChangelogFromURL(match[1], directory)
	}
];

/**
 * Generates a link to the npmjs.com website, based on a dependency name..
 *
 * @public
 * @param {string} packageName - Options which shall be appended to the `npm outdated` command-line call.
 * @returns {string} The link to the package on the npmjs.com website.
 */
function getNpmJSLink (packageName) {
	return `https://www.npmjs.com/package/${encodeURIComponent(packageName)}`;
}

/**
 * Returns the URL of the author from the package.json.
 *
 * @public
 * @param {PackageJSON} packageJSON - The content of a package.json.
 * @returns {string | undefined} The URL of the author.
 */
function getPackageAuthor (packageJSON) {
	if (packageJSON.author) {
		if (typeof packageJSON.author === 'string') {
			const match = (/\((http.+?)\)/u).exec(packageJSON.author);

			if (match !== null) {
				return match[1];
			}
		}
		else if (packageJSON.author.url) {
			return packageJSON.author.url;
		}
	}

	return;
}

/**
 * Returns the URL of the homepage from the package.json.
 *
 * @public
 * @param {PackageJSON} packageJSON - The content of a package.json.
 * @returns {string | undefined} The URL of the homepage.
 */
function getPackageHomepage (packageJSON) {
	if (packageJSON.homepage) {
		return packageJSON.homepage;
	}

	return;
}

/**
 * Returns the URL of the repository from the package.json.
 *
 * @public
 * @param {PackageJSON} packageJSON - The content of a package.json.
 * @param {boolean} [linkToChangelog] - If the returned URL should link to the "Releases" page.
 * @returns {Promise<string | undefined>} The URL of the repository.
 */
async function getPackageRepository (packageJSON, linkToChangelog) {
	if (packageJSON.repository) {
		if (typeof packageJSON.repository === 'string') {
			for (const repo of REPOSITORY_URLS) {
				const match = repo.regExp.exec(packageJSON.repository);

				if (match !== null) {
					return (linkToChangelog ? repo.getChangelogURL(match, '') : repo.getRepositoryURL(match, ''));
				}
			}
		}
		else if (packageJSON.repository.url) {
			for (const repo of REPOSITORY_URLS) {
				const match = repo.regExp.exec(packageJSON.repository.url);

				if (match !== null) {
					const directory = packageJSON.repository.directory ? packageJSON.repository.directory.replace(/^\/*/u, '/').replace(/\/+$/u, '') : '';

					return (linkToChangelog ? repo.getChangelogURL(match, directory) : repo.getRepositoryURL(match, directory));
				}
			}
		}
	}

	return;
}

/**
 * Tries to determine the URL to the changelog for a specific service `url`.
 *
 * @private
 * @param {string} url - Service URL
 * @param {string} directory - Sub-directory path, starting with "/" or an empty string
 * @returns {Promise<string>} Either returns an URL to the changelog for a specific service, or it returns `url`.
 */
async function getChangelogFromURL (url, directory) {
	const githubMatch = (/^https?:\/\/github.com\/([^/]+?\/[^/#?]+)/u).exec(url);

	if (githubMatch !== null) {
		const linkToChangelog = await getFileOnGitHub(githubMatch[1], 'CHANGELOG.md', directory, 256);

		if (linkToChangelog) {
			return linkToChangelog;
		}

		return `https://github.com/${githubMatch[1]}/releases`;
	}

	const gistMatch = (/^https?:\/\/gist.github.com\/([^/]+?\/)?[^/#?]+/u).exec(url);

	if (gistMatch !== null) {
		return `${gistMatch[0]}/revisions`;
	}

	const gitlabMatch = (/^https?:\/\/gitlab.com\/[^/]+?\/[^/#?]+/u).exec(url);

	if (gitlabMatch !== null) {
		return `${gitlabMatch[0]}/-/releases`;
	}

	return url;
}

/**
 * Returns a link to the HTML representation of a specific file in a GitHub repository.
 *
 * @private
 * @param {string} repoName - GitHub repository name, e.g. "jens.duttke/check-outdated"
 * @param {string} fileName - File name, e.g. "README.md"
 * @param {string} directory - Sub-directory path, starting with "/" or an empty string
 * @param {number} minimumContentSize - The minimum required size of the file content.
 * @returns {Promise<string | undefined>} Returns either the URL to the HTML representation of the file, or `undefined` if the file has not been found, or if it is too small.
 */
async function getFileOnGitHub (repoName, fileName, directory = '', minimumContentSize = 0) {
	return new Promise((resolve) => {
		https.get({
			host: 'api.github.com',
			path: `/repos/${repoName}/contents${directory}/${fileName}`,
			method: 'GET',
			headers: {
				'User-Agent': 'Mozilla/5.0'
			}
		}, (response) => {
			if (response.statusCode !== STATUS_OK) {
				response.destroy();

				resolve(undefined);

				return;
			}

			/** @type {string[]} */
			const data = [];

			response.setEncoding('utf8');
			response.on('data', (chunk) => data.push(chunk));
			response.on('end', () => {
				try {
					const json = JSON.parse(data.join(''));

					if (json.html_url && json.size >= minimumContentSize) {
						resolve(json.html_url);
					}
					else {
						resolve(undefined);
					}
				}
				catch {
					resolve(undefined);
				}
			});
			response.on('error', () => resolve(undefined));
		});
	});
}

module.exports = {
	getNpmJSLink,
	getPackageAuthor,
	getPackageHomepage,
	getPackageRepository
};
<file_sep>/**
 * @file Helper functionality to work with regular expressions.
 */

/**
 * Escape strings for use in regular expressions.
 *
 * @public
 * @param {string} string - The input string.
 * @param {RegExp} regexp - The regular expression used for the search.
 * @returns {[number, number]} The first value represents the line, the second value the column.
 */
function getRegExpPosition (string, regexp) {
	let pos = string.search(regexp);

	if (pos !== -1) {
		const lines = string.split('\n');
		let line;
		let count = 0;

		for (line = 0; line < lines.length; line++) {
			const lineLength = lines[line].length;

			if (count + lineLength >= pos) {
				break;
			}

			count += lineLength;

			// Fix to compensate line break
			pos--;
		}

		if (line < lines.length) {
			return [
				line + 1,
				(pos - count) + 1
			];
		}
	}

	return [0, 0];
}

/**
 * Escape strings for use in regular expressions.
 *
 * @public
 * @param {string} string - The input string.
 * @returns {string} The escaped output string.
 */
function escapeRegExp (string) {
	return string.replace(/([[\\^$.|?*+()])/gu, '\\$1');
}

module.exports = {
	getRegExpPosition,
	escapeRegExp
};
<file_sep>/**
 * @file The expectation handers.
 */

const assert = require('assert').strict;
const fs = require('fs');

const { assertHasWord, assertNotHasWord } = require('./assert');
const colorize = require('./colorize');

/** @type {Record<string, string[]>} */
const expectVarFileCache = {};

const sum = {
	passed: 0,
	failed: 0
};

/**
 * Returns the sum of the test `passed` and `failed`.
 *
 * @public
 * @returns {{ passed: number; failed: number; }} Object with the sum of the test `passed` and `failed.
 */
function getExpectResult () {
	return sum;
}

/**
 * Handle a specific test.
 *
 * @public
 * @param {string} message - The message which shall be shown if an assertion fails.
 * @param {() => void | never} assertion - A function which throws an error to indicate that an assertion fails.
 */
function expect (message, assertion) {
	const styledMessage = message.replace(/\n/gu, '\\n').replace(/`(.+?)`/gu, colorize.underline('$1'));

	/* eslint-disable no-console -- console.log() is used to output the test results */

	try {
		assertion();
	}
	catch (error) {
		const errorType = (error instanceof assert.AssertionError ? 'Test failed' : 'Error in code');
		let errorMessage;

		if (error instanceof Error) {
			errorMessage = error.message;
		}
		else if (typeof error === 'string') {
			errorMessage = error;
		}
		else {
			errorMessage = 'Unknown error';
		}

		console.log(`    ${colorize.red(`× ${styledMessage}`)}`);
		console.log();
		console.log(`      ${colorize.gray(`${errorType}: ${errorMessage.trim().replace(/\n/gu, '\n      ')}`)}`);
		console.log();

		if (error instanceof assert.AssertionError) {
			if (typeof error.expected === 'string' && typeof error.actual === 'string') {
				console.log(`      ${colorize.red(`- ${error.expected.replace(/\n/gu, '\n      ')}`)}`);
				console.log(`      ${colorize.green(`+ ${error.actual.replace(/\n/gu, '\n      ')}`)}`);
				console.log();
			}
		}

		console.log(`      ${colorize.gray((new Error('dummy').stack || '').split('\n').slice(2, 3).join('').trim())}`);
		console.log();

		sum.failed++;

		return;
	}

	console.log(`    ${colorize.green('√')} ${colorize.gray(styledMessage)}`);

	/* eslint-enable no-console */

	sum.passed++;
}

/**
 * Assert that only the specified number of dependencies is affected by a test suite.
 *
 * @public
 * @param {string} stdout - The process output.
 * @param {{ [name: string]: Partial<import('../../helper/dependencies').OutdatedDependency>; }} dependencies - The dependencies object.
 * @param {number} noOfAffectedDependencies - Number of dependencies which are effected by the test suite.
 * @returns {void}
 */
function expectNoOfAffectedDependencies (stdout, dependencies, noOfAffectedDependencies) {
	const dependencyValues = Object.values(dependencies);
	const noOfDepsLeft = dependencyValues.filter(({ latest }) => !latest || !['git', 'linked', 'remote'].includes(latest)).length - noOfAffectedDependencies;

	expect(
		`\`stdout\` should contain \`"${noOfDepsLeft} outdated dependencies found:"\``,
		() => assertHasWord(stdout, `${noOfDepsLeft} outdated dependencies found:`)
	);
}

/**
 * Assert that a variable matches a value.
 *
 * @public
 * @param {any} variable - The variable containing the actual value.
 * @param {any} value - The expected value.
 */
function expectVariableToEqual (variable, value) {
	expect(`\`${getFirstArgument()}\` should be \`${JSON.stringify(value)}\``, () => assert.equal(variable, value));
}

/**
 * Assert that a variable contains a word.
 *
 * @public
 * @param {any} variable - The variable containing the actual value.
 * @param {any} value - The expected value.
 * @param {boolean} [ignoreEscapeSequences=true] - ANSI escape sequences for coloring in `str` shall be ignored.
 */
function expectVariableToHaveWord (variable, value, ignoreEscapeSequences) {
	expect(
		`\`${getFirstArgument()}\` should contain ${(!ignoreEscapeSequences ? 'styled ' : '')}\`${JSON.stringify(value)}\``,
		() => assertHasWord(variable, value, ignoreEscapeSequences)
	);
}

/**
 * Assert that a variable does not contains a word.
 *
 * @public
 * @param {any} variable - The variable containing the actual value.
 * @param {any} value - The expected value.
 * @param {boolean} [ignoreEscapeSequences=true] - ANSI escape sequences for coloring in `str` shall be ignored.
 */
function expectVariableNotToHaveWord (variable, value, ignoreEscapeSequences = true) {
	expect(
		`\`${getFirstArgument()}\` should not contain ${(!ignoreEscapeSequences ? 'styled ' : '')}\`${JSON.stringify(value)}\``,
		() => assertNotHasWord(variable, value, ignoreEscapeSequences)
	);
}

/**
 * Determine the first arguments of a function call from source code.
 *
 * This code does some tricky code analysis to find the variable name.
 *
 * @private
 * @returns {string} Either the first argument or in red colored "[unknown argument]".
 */
function getFirstArgument () {
	const match = (/^\s+at\s(.+?):(\d+):(\d+)$/u).exec((new Error('dummy').stack || '').split('\n')[3]);

	if (match !== null) {
		const [, file, line, col] = match;

		let fileContent;

		if (file in expectVarFileCache) {
			fileContent = expectVarFileCache[file];
		}
		else {
			fileContent = fs.readFileSync(file, 'utf8').split('\n');

			expectVarFileCache[file] = fileContent;
		}

		const codeFromLine = fileContent.slice(Number.parseInt(line, 10) - 1).join(' ');
		const code = codeFromLine.substr(Number.parseInt(col, 10) - 1, codeFromLine.indexOf(';', Number.parseInt(col, 10)));

		const firstArgumentMatch = (/\(([^,)]+)/u).exec(code);

		if (firstArgumentMatch !== null) {
			return firstArgumentMatch[1].trim();
		}
	}

	return colorize.red('[unknown argument]');
}

module.exports = {
	getExpectResult,
	expect,
	expectNoOfAffectedDependencies,
	expectVarToEqual: expectVariableToEqual,
	expectVarToHaveWord: expectVariableToHaveWord,
	expectVarNotToHaveWord: expectVariableNotToHaveWord
};
<file_sep># Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

[Show all code changes](https://github.com/jens-duttke/check-outdated/compare/v2.12.0...HEAD)

## [2.12.0] - 2023-08-11

### Changed

- Updated dependencies

[Show all code changes](https://github.com/jens-duttke/check-outdated/compare/v2.11.0...v2.12.0)

## [2.11.0] - 2022-09-24

### Added

- `--ignore-packages` now support semver ranges like `foo@^2`, `foo@~2.3.4`, `foo@2.*`, `foo@2.3.x`

[Show all code changes](https://github.com/jens-duttke/check-outdated/compare/v2.10.2...v2.11.0)

## [2.10.2] - 2021-09-25

### Fixed

- Fixed syntax error with Node.js v13 and below

[Show all code changes](https://github.com/jens-duttke/check-outdated/compare/v2.10.0...v2.10.2)

## [2.10.0] - 2021-09-18

### Changed

- Now supporting other "base" branches than `master` on GitHub, to find the CHANGELOG.md

[Show all code changes](https://github.com/jens-duttke/check-outdated/compare/v2.9.0...v2.10.0)

## [2.9.0] - 2021-09-09

### Changed

- For reverted versions, the "Type" column shows "reverted"
- The package names of reverted versions are shown in red
- The package name color of major-updates changed to yellow, and of minor-updates changed to cyan

### Added

- Added `--types` option to restrict the visible versions.

### Fixed

- Show `wanted` and `latest` version numbers even if `current` is empty

[Show all code changes](https://github.com/jens-duttke/check-outdated/compare/v2.8.0...v2.9.0)

## [2.8.0] - 2021-05-22

### Changed

- Rename `name` column to `package`
- Changed the minimum required NodeJS version to v10.0.0

[Show all code changes](https://github.com/jens-duttke/check-outdated/compare/v2.7.0...v2.8.0)

## [2.7.0] - 2021-03-24

### Added

- New `--prefer-wanted` option, which compares the the `Current` version to the `Wanted` version, instead of the `Latest` version. This allow you to ensure that your installed version is the latest in the specified version range, but not necessary the latest available version ([#7](https://github.com/jens-duttke/check-outdated/issues/7))

[Show all code changes](https://github.com/jens-duttke/check-outdated/compare/v2.6.0...v2.7.0)

## [2.6.0] - 2021-03-14

### Changed

- Change colorization of dependency name
- Reorder default columns
- Remove "Type" from default columns

### Added

- Added "Color legend"
- Group dependencies by their type

[Show all code changes](https://github.com/jens-duttke/check-outdated/compare/v2.5.1...v2.6.0)

## [2.5.1] - 2021-02-26

### Fixed

- Remove ".git" extension from GitHub repository URLs

[Show all code changes](https://github.com/jens-duttke/check-outdated/compare/v2.5.0...v2.5.1)

## [2.5.0] - 2020-12-16

### Fixed

- Sub-packages in mono-repositories on GitHub (like Gatsby) are now considered for CHANGELOG.md-link generation

[Show all code changes](https://github.com/jens-duttke/check-outdated/compare/v2.4.1...v2.5.0)

## [2.4.1] - 2020-09-29

### Changed

- Improve CHANGELOG.md detection

[Show all code changes](https://github.com/jens-duttke/check-outdated/compare/v2.4.0...v2.4.1)

## [2.4.0] - 2020-09-26

### Fixed

- Only link to CHANGELOG.md files on GitHub if they have content

[Show all code changes](https://github.com/jens-duttke/check-outdated/compare/v2.3.2...v2.4.0)

## [2.3.2] - 2020-08-04

### Changed

- Optimize detection of item in package.json for generting the Reference link

[Show all code changes](https://github.com/jens-duttke/check-outdated/compare/v2.3.1...v2.3.2)

## [2.3.1] - 2020-08-04

### Fixed

- Fix missing Reference link, if the installed package version does not match the version in the package.json

[Show all code changes](https://github.com/jens-duttke/check-outdated/compare/v2.3.0...v2.3.1)

## [2.3.0] - 2020-06-24

### Added

- If a CHANGELOG.md exists in the GitHub repository, the Changes row, will link to this file, instead of the Releases page

[Show all code changes](https://github.com/jens-duttke/check-outdated/compare/v2.2.0...v2.3.0)

## [2.2.0] - 2020-05-16

### Added

- Add "Reference" column, which contains a link to the line and column of the dependency in the package.json.
  By using a terminal which supports clicking on such links, you can navigate directly the the item.

[Show all code changes](https://github.com/jens-duttke/check-outdated/compare/v2.1.1...v2.2.0)

## [2.1.1] - 2020-05-08

### Fixed

- Handle the issue that npm may return an empty current information for packages (thanks to <NAME> for reporting this problem)

[Show all code changes](https://github.com/jens-duttke/check-outdated/compare/v2.1.0...v2.1.1)

## [2.1.0] - 2020-05-08

### Removed

- Remove consideration of CHANGELOG.md from changes column

### Changed

- Changed the minimum required NodeJS version to v8.6.0

### Added

- Add functionality to optionally ignore a specific version of a package (e.g. to skip a broken version)
- Add new changesPreferLocal column, which behaves like the old changes column

[Show all code changes](https://github.com/jens-duttke/check-outdated/compare/v2.0.0...v2.1.0)

## [2.0.0] - 2020-04-26

### Added

- Add --columns argument to hide/show and reorder the columns.
- Add new columns "Changes" (changes), "Homepage" (homepage) and "npmjs.com" (npmjs)

[Show all code changes](https://github.com/jens-duttke/check-outdated/compare/v1.4.3...v2.0.0)

## [1.4.3] - 2020-03-16

[Show all code changes](https://github.com/jens-duttke/check-outdated/compare/v1.4.2...v1.4.3)

## [1.4.2] - 2019-09-08

[Show all code changes](https://github.com/jens-duttke/check-outdated/compare/v1.4.1...v1.4.2)

## [1.4.1] - 2019-08-30

[Show all code changes](https://github.com/jens-duttke/check-outdated/compare/v1.3.0...v1.4.1)

## [1.3.0] - 2019-08-09

[Show all code changes](https://github.com/jens-duttke/check-outdated/compare/v1.2.0...v1.3.0)

## [1.2.0] - 2019-08-02

[Show all code changes](https://github.com/jens-duttke/check-outdated/compare/v1.1.0...v1.2.0)

## [1.1.0] - 2019-08-02

[Show all code changes](https://github.com/jens-duttke/check-outdated/compare/v1.0.1...v1.1.0)

## [1.0.1] - 2019-08-01

[Show all code changes](https://github.com/jens-duttke/check-outdated/compare/8fa4721...v1.0.1)

## [1.0.0] - 2019-08-01

Initial commit
<file_sep>/**
 * @file Wrapper for the colorize module, to disable colorization if the test output is written to a non-terminal target (e.g. a file).
 */

const tty = require('tty');

const colorize = require('../../helper/colorize');

const isTerminal = tty.isatty(1);

if (!isTerminal) {
	module.exports = colorize.disabled;
}
else {
	module.exports = colorize;
}
<file_sep>/**
 * @file Parses the given `argv` array, based on an object with supported args, into key/value pairs.
 */

/** @typedef {{ [key: string]: any; }} Options */

/** @typedef {{ [argName: string]: ((value: string) => Partial<Options> | string) | Partial<Options>; }} AvailableArguments */

/**
 * Parses the given `argv` array, based on an object with supported args, into key/value pairs.
 *
 * @public
 * @param {string[]} argv - Arguments given in the command line (`process.argv.slice(2)`).
 * @param {AvailableArguments} availableArgs - Configuration object with supported arguments.
 * @returns {Options | string} Either a `Options` object or a `string` which should be returned to the user, if arguments cannot be parsed.
 * @throws If arguments is unknown.
 */
function parseArguments (argv, availableArgs) {
	const args = {};

	const unsupportedArguments = argv.filter((argument) => argument.startsWith('-') && !Object.keys(availableArgs).includes(argument));
	if (unsupportedArguments.length > 0) {
		throw new Error(`Unknown argument${(unsupportedArguments.length > 1 ? 's' : '')}: ${unsupportedArguments.join(', ')}`);
	}

	for (const [name, value] of Object.entries(availableArgs)) {
		if (typeof value === 'function') {
			const index = argv.indexOf(name);

			if (index !== -1) {
				const finalValue = value(argv[index + 1] || '');

				if (typeof finalValue === 'string') {
					return finalValue;
				}

				Object.assign(args, finalValue);
			}
		}
		else if (argv.includes(name)) {
			Object.assign(args, value);
		}
	}

	return args;
}

module.exports = parseArguments;
