diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..7a116af
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,7 @@
+version: 2
+updates:
+ - package-ecosystem: "npm" # See documentation for possible values
+ directory: "/" # Location of package manifests
+ schedule:
+ interval: "weekly"
+ versioning-strategy: widen
diff --git a/.github/workflows/npm-test.yml b/.github/workflows/npm-test.yml
new file mode 100644
index 0000000..e29736f
--- /dev/null
+++ b/.github/workflows/npm-test.yml
@@ -0,0 +1,68 @@
+name: npm test
+
+on:
+ push:
+ branches: ["main"]
+ paths:
+ [
+ "*.ts",
+ "jest.config.js?(on)",
+ "package-lock.json",
+ ".github/workflows/npm-test.yml",
+ ]
+ pull_request:
+ branches: ["main", "dev"]
+ paths:
+ [
+ "*.ts",
+ "jest.config.js?(on)",
+ "package-lock.json",
+ ".github/workflows/npm-test.yml",
+ ]
+ workflow_dispatch:
+
+jobs:
+ test:
+ runs-on: ${{ matrix.os }}
+
+ strategy:
+ matrix:
+ os: [ubuntu-latest, windows-latest, macos-latest]
+ version: [16]
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ - name: Setup node.js @ ${{ matrix.version }}
+ uses: actions/setup-node@v3
+ with:
+ node-version: ${{ matrix.version }}
+
+ - name: Get npm cache directory
+ id: npm-cache
+ run: echo "::set-output name=dir::$(npm config get cache)"
+
+ - name: Cache node_modules
+ id: cache-npm
+ uses: actions/cache@v3
+ env:
+ cache-name: cache-node-modules
+ with:
+ path: ${{ steps.npm-cache.outputs.dir }}
+ key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
+ restore-keys: |
+ ${{ runner.os }}-build-${{ env.cache-name }}-
+ ${{ runner.os }}-build-
+ ${{ runner.os }}-
+
+ - if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }}
+ name: List the state of node modules
+ continue-on-error: true
+ run: npm list
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Run tests
+ run: npm test
diff --git a/.github/workflows/publish-code-coverage.yml b/.github/workflows/publish-code-coverage.yml
new file mode 100644
index 0000000..7b53e8e
--- /dev/null
+++ b/.github/workflows/publish-code-coverage.yml
@@ -0,0 +1,59 @@
+name: publish code coverage
+
+on:
+ push:
+ branches: ["main"]
+ paths:
+ [
+ "**.ts",
+ "jest.config.js*",
+ ".github/workflows/publish-code-coverage.yml",
+ ]
+ workflow_dispatch:
+
+jobs:
+ publish-coverage:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ - name: Setup node.js @ 16
+ uses: actions/setup-node@v3
+ with:
+ node-version: 16
+
+ - name: Get npm cache directory
+ id: npm-cache
+ run: echo "::set-output name=dir::$(npm config get cache)"
+
+ - name: Cache node_modules
+ id: cache-npm
+ uses: actions/cache@v3
+ env:
+ cache-name: cache-node-modules
+ with:
+ path: ${{ steps.npm-cache.outputs.dir }}
+ key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
+ restore-keys: |
+ ${{ runner.os }}-build-${{ env.cache-name }}-
+ ${{ runner.os }}-build-
+ ${{ runner.os }}-
+
+ - if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }}
+ name: List the state of node modules
+ continue-on-error: true
+ run: npm list
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Generate code coverage reports
+ run: npm test -- --coverage
+ continue-on-error: true
+
+ - name: Publish code coverage to codecov 🚀
+ uses: codecov/codecov-action@v3
+ with:
+ verbose: true
+ fail_ci_if_error: true
diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml
new file mode 100644
index 0000000..5b4b2b8
--- /dev/null
+++ b/.github/workflows/publish-docs.yml
@@ -0,0 +1,65 @@
+name: publish docs
+
+on:
+ release:
+ types: [published]
+ workflow_dispatch:
+
+jobs:
+ deploy-docs:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout main
+ uses: actions/checkout@v3
+
+ - name: Checkout gh-pages
+ uses: actions/checkout@v3
+ with:
+ ref: gh-pages
+ path: "./gh-pages"
+
+ - name: Fix folder structure
+ run: mv gh-pages/docs docs
+ continue-on-error: true
+
+ - name: Clean up gh-pages
+ run: rm -rf gh-pages
+
+ - name: Setup node.js @ 16
+ uses: actions/setup-node@v3
+ with:
+ node-version: 16
+
+ - name: Get npm cache directory
+ id: npm-cache
+ run: echo "::set-output name=dir::$(npm config get cache)"
+
+ - name: Cache node_modules
+ id: cache-npm
+ uses: actions/cache@v3
+ env:
+ cache-name: cache-node-modules
+ with:
+ path: ${{ steps.npm-cache.outputs.dir }}
+ key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
+ restore-keys: |
+ ${{ runner.os }}-build-${{ env.cache-name }}-
+ ${{ runner.os }}-build-
+ ${{ runner.os }}-
+
+ - if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }}
+ name: List the state of node modules
+ continue-on-error: true
+ run: npm list
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Generate API reference
+ run: npm run docs
+
+ - name: Publish to GitHub Pages 🚀
+ uses: JamesIves/github-pages-deploy-action@v4
+ with:
+ folder: docs
+ target-folder: docs
diff --git a/.github/workflows/publish-package.yml b/.github/workflows/publish-package.yml
new file mode 100644
index 0000000..13189a9
--- /dev/null
+++ b/.github/workflows/publish-package.yml
@@ -0,0 +1,55 @@
+name: publish package
+
+on:
+ release:
+ types: [published]
+ workflow_dispatch:
+
+jobs:
+ publish-npm:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ - name: Setup node.js @ 16
+ uses: actions/setup-node@v3
+ with:
+ node-version: 16
+
+ - name: Get npm cache directory
+ id: npm-cache
+ run: echo "::set-output name=dir::$(npm config get cache)"
+
+ - name: Cache node_modules
+ id: cache-npm
+ uses: actions/cache@v3
+ env:
+ cache-name: cache-node-modules
+ with:
+ path: ${{ steps.npm-cache.outputs.dir }}
+ key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
+ restore-keys: |
+ ${{ runner.os }}-build-${{ env.cache-name }}-
+ ${{ runner.os }}-build-
+ ${{ runner.os }}-
+
+ - if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }}
+ name: List the state of node modules
+ continue-on-error: true
+ run: npm list
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Run tests
+ run: npm test
+
+ - name: Build
+ run: npm run build
+
+ - name: Publish to npm 🚀
+ uses: JS-DevTools/npm-publish@v1
+ with:
+ token: ${{ secrets.NPM_TOKEN }}
+ access: public
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..91dfa24
--- /dev/null
+++ b/README.md
@@ -0,0 +1,125 @@
+
+
+# typedoc-plugin-versions-cli 🧑💻
+
+A companion CLI tool for working with [typedoc-plugin-versions](https://citkane.github.io/typedoc-plugin-versions).
+
+[![npm package version](https://img.shields.io/npm/v/typedoc-plugin-versions-cli.svg?logo=npm&label&labelColor=222&style=flat-square)](https://npmjs.org/package/typedoc-plugin-versions-cli "View typedoc-plugin-versions-cli on npm") [![npm package downloads](https://img.shields.io/npm/dw/typedoc-plugin-versions-cli.svg?logo=npm&labelColor=222&style=flat-square)](https://npmjs.org/package/typedoc-plugin-versions-cli "View typedoc-plugin-versions-cli on npm") [![typedocs](https://img.shields.io/badge/docs-informational.svg?logo=typescript&labelColor=222&style=flat-square)](https://toebeann.github.io/typedoc-plugin-versions-cli "Read the documentation on Github Pages") [![coverage](https://img.shields.io/codecov/c/github/toebeann/typedoc-plugin-versions-cli.svg?logo=codecov&labelColor=222&style=flat-square)](https://codecov.io/gh/toebeann/typedoc-plugin-versions-cli "View code coverage on Codecov") [![code quality](https://img.shields.io/codefactor/grade/github/toebeann/typedoc-plugin-versions-cli.svg?logo=codefactor&labelColor=222&style=flat-square)](https://www.codefactor.io/repository/github/toebeann/typedoc-plugin-versions-cli "View code quality on CodeFactor") [![license](https://img.shields.io/github/license/toebeann/typedoc-plugin-versions-cli.svg?color=informational&labelColor=222&style=flat-square)](https://github.com/toebeann/typedoc-plugin-versions-cli/blob/main/LICENSE "View the license on GitHub")
+
+[![npm test](https://img.shields.io/github/workflow/status/toebeann/typedoc-plugin-versions-cli/npm%20test.svg?logo=github&logoColor=aaa&label=npm%20test&labelColor=222&style=flat-square)](https://github.com/toebeann/typedoc-plugin-versions-cli/actions/workflows/npm-test.yml "View npm test on GitHub Actions") [![publish code coverage](https://img.shields.io/github/workflow/status/toebeann/typedoc-plugin-versions-cli/publish%20code%20coverage.svg?logo=github&logoColor=aaa&label=publish%20code%20coverage&labelColor=222&style=flat-square)](https://github.com/toebeann/typedoc-plugin-versions-cli/actions/workflows/publish-code-coverage.yml "View publish code coverage on GitHub Actions") [![publish package](https://img.shields.io/github/workflow/status/toebeann/typedoc-plugin-versions-cli/publish%20package.svg?logo=github&logoColor=aaa&label=publish%20package&labelColor=222&style=flat-square)](https://github.com/toebeann/typedoc-plugin-versions-cli/actions/workflows/publish-package.yml "View publish package on GitHub Actions") [![publish docs](https://img.shields.io/github/workflow/status/toebeann/typedoc-plugin-versions-cli/publish%20docs.svg?logo=github&logoColor=aaa&label=publish%20docs&labelColor=222&style=flat-square)](https://github.com/toebeann/typedoc-plugin-versions-cli/actions/workflows/publish-docs.yml "View publish docs on GitHub Actions")
+
+[![github](https://img.shields.io/badge/source-informational.svg?logo=github&labelColor=222&style=flat-square)](https://github.com/toebeann/typedoc-plugin-versions-cli "View typedoc-plugin-versions-cli on GitHub") [![twitter](https://img.shields.io/badge/follow-blue.svg?logo=twitter&label&labelColor=222&style=flat-square)](https://twitter.com/toebean__ "Follow @toebean__ on Twitter") [![GitHub Sponsors donation button](https://img.shields.io/badge/sponsor-e5b.svg?logo=github%20sponsors&labelColor=222&style=flat-square)](https://github.com/sponsors/toebeann "Sponsor typedoc-plugin-versions-cli on GitHub") [![PayPal donation button](https://img.shields.io/badge/donate-e5b.svg?logo=paypal&labelColor=222&style=flat-square)](https://paypal.me/tobeyblaber "Donate to typedoc-plugin-versions-cli with PayPal")
+
+
+
+## Table of contents
+
+- [typedoc-plugin-versions-cli 🧑💻](#typedoc-plugin-versions-cli-)
+ - [Table of contents](#table-of-contents)
+ - [Install](#install)
+ - [npm](#npm)
+ - [Usage](#usage)
+ - [Commands](#commands)
+ - [purge](#purge)
+ - [Options](#options)
+ - [synchronize](#synchronize)
+ - [Options](#options-1)
+ - [License](#license)
+
+## Install
+
+### [npm](https://www.npmjs.com/package/toebean/typedoc-plugin-versions-cli "npm is a package manager for JavaScript")
+
+```text
+npm i -D typedoc-plugin-versions-cli typedoc-plugin-versions
+```
+
+## Usage
+
+Run any of the following from the command line:
+
+```text
+tpv [options..]
+```
+
+```text
+typedoc-plugin-versions-cli [options..]
+```
+
+```text
+typedoc-plugin-versions [options..]
+```
+
+```text
+typedoc-versions [options..]
+```
+
+See details about the various [commands](#commands) and their options via the `--help` flag:
+
+```text
+tpv --help
+```
+
+All `boolean` flags which are `true` by default can be negated by prefixing with `no-`, e.g., the following are equivalent:
+
+```text
+tpv purge --no-stale
+tpv purge --stale false
+```
+
+On Windows, you may need to prefix the commands with `npx `, e.g.:
+
+```text
+npx tpv [options..]
+```
+
+### Commands
+
+#### purge
+
+Deletes old doc builds and/or versions matching semver ranges.
+
+```text
+tpv purge [versions..] [flags]
+```
+
+Displays a confirmation prompt before performing changes.
+
+##### Options
+
+- **`--stale [boolean] [default: true]`**
Purge stale doc builds, e.g. `v1.0.0-alpha.1` is considered stale once `v1.0.0` has been built.
+- **`--major [default: Infinity]`**
Purge all but the specified number of major versions.
+- **`--minor [default: Infinity]`**
Purge all but the specified number of minor versions per major version.
+- **`--patch [default: Infinity]`**
Purge all but the specified number of patch versions per minor version.
+- **`--exclude `**
Exclude versions matching the specified semver ranges from the purge operation.
+- **`--pre`**, **`--prerelease [boolean] [default: false]`**
Include prerelease versions when evaluating semver ranges.
+- **`-y`**, **`--yes [boolean] [default: false]`**
Automatically confirms prompts.
+- **`--out `**
The path to your typedoc output directory. By default this is inferred from your typedoc configuration.
+- **`--typedoc [default: "."]`**
The path to your typedoc configuration file, e.g. `typedoc.json`. By default this is searched for in the current working directory.
+- **`--tsconfig [default: "."]`**
The path to your TypeScript tsconfig file, e.g. `tsconfig.json`. By default this is searched for in the current working directory.
+
+#### synchronize
+
+Ensures your [typedoc-plugin-versions](https://citkane.github.io/typedoc-plugin-versions) metadata and symbolic links are up-to-date. Useful after deleting old doc builds.
+
+```text
+tpv synchronize [flags]
+```
+
+```text
+tpv sync [flags]
+```
+
+Displays a confirmation prompt before performing changes.
+
+##### Options
+
+- **`-y`**, **`--yes [boolean] [default: false]`**
Automatically confirms prompts.
+- **`--symlink [boolean] [default: false]`**
Always ensures symbolic links are up-to-date, regardless of confirmation prompts.
+- **`--out `**
The path to your typedoc output directory. By default this is inferred from your typedoc configuration.
+- **`--typedoc [default: "."]`**
The path to your typedoc configuration file, e.g. `typedoc.json`. By default this is searched for in the current working directory.
+- **`--tsconfig [default: "."]`**
The path to your TypeScript tsconfig file, e.g. `tsconfig.json`. By default this is searched for in the current working directory.
+
+## License
+
+typedoc-plugin-versions-cli is licensed under [MIT](https://github.com/toebeann/typedoc-plugin-versions-cli/blob/main/LICENSE) © 2022 Tobey Blaber.
diff --git a/package-lock.json b/package-lock.json
index e5a2e18..d2182f3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -24,6 +24,7 @@
"darwin"
],
"dependencies": {
+ "async": "^3.2.4",
"cli-diff": "^1.0.0",
"prompts": "^2.4.2",
"semver": "^7.3.7",
@@ -31,27 +32,28 @@
},
"bin": {
"tpv": "dist/cli.js",
- "tpv-cli": "dist/cli.js",
"typedoc-plugin-versions": "dist/cli.js",
- "typedoc-plugin-versions-cli": "dist/cli.js"
+ "typedoc-plugin-versions-cli": "dist/cli.js",
+ "typedoc-versions": "dist/cli.js"
},
"devDependencies": {
"@tsconfig/node14": "^1.0.3",
+ "@types/async": "^3.2.15",
"@types/fs-extra": "^9.0.13",
- "@types/jest": "^29.0.1",
+ "@types/jest": "^29.0.3",
"@types/prompts": "^2.0.14",
"@types/semver": "^7.3.12",
"@types/yargs": "^17.0.12",
- "@typescript-eslint/eslint-plugin": "^5.37.0",
- "@typescript-eslint/parser": "^5.37.0",
+ "@typescript-eslint/eslint-plugin": "^5.38.0",
+ "@typescript-eslint/parser": "^5.38.0",
"eslint": "^8.23.1",
"eslint-config-prettier": "^8.5.0",
"fs-extra": "^10.1.0",
"jest": "^29.0.3",
"prettier": "^2.7.1",
- "ts-jest": "^29.0.0",
+ "ts-jest": "^29.0.1",
"ts-node": "^10.9.1",
- "typedoc": "^0.23.14",
+ "typedoc": "^0.23.15",
"typedoc-plugin-versions": "^0.2.0",
"typescript": "^4.8.3"
},
@@ -1273,6 +1275,12 @@
"integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==",
"dev": true
},
+ "node_modules/@types/async": {
+ "version": "3.2.15",
+ "resolved": "https://registry.npmjs.org/@types/async/-/async-3.2.15.tgz",
+ "integrity": "sha512-PAmPfzvFA31mRoqZyTVsgJMsvbynR429UTTxhmfsUCrWGh3/fxOrzqBtaTPJsn4UtzTv4Vb0+/O7CARWb69N4g==",
+ "dev": true
+ },
"node_modules/@types/babel__core": {
"version": "7.1.19",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.19.tgz",
@@ -1357,9 +1365,9 @@
}
},
"node_modules/@types/jest": {
- "version": "29.0.1",
- "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.0.1.tgz",
- "integrity": "sha512-CAZrjLRZs4xEdIrfrdV74xK1Vo/BKQZwUcjJv3gp6gMeV3BsVxMnXTcgtYOKyphT4DPPo7jxVEVhuwJTQn3oPQ==",
+ "version": "29.0.3",
+ "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.0.3.tgz",
+ "integrity": "sha512-F6ukyCTwbfsEX5F2YmVYmM5TcTHy1q9P5rWlRbrk56KyMh3v9xRGUO3aa8+SkvMi0SHXtASJv1283enXimC0Og==",
"dev": true,
"dependencies": {
"expect": "^29.0.0",
@@ -1421,16 +1429,15 @@
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "5.37.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.37.0.tgz",
- "integrity": "sha512-Fde6W0IafXktz1UlnhGkrrmnnGpAo1kyX7dnyHHVrmwJOn72Oqm3eYtddrpOwwel2W8PAK9F3pIL5S+lfoM0og==",
+ "version": "5.38.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.38.0.tgz",
+ "integrity": "sha512-GgHi/GNuUbTOeoJiEANi0oI6fF3gBQc3bGFYj40nnAPCbhrtEDf2rjBmefFadweBmO1Du1YovHeDP2h5JLhtTQ==",
"dev": true,
"dependencies": {
- "@typescript-eslint/scope-manager": "5.37.0",
- "@typescript-eslint/type-utils": "5.37.0",
- "@typescript-eslint/utils": "5.37.0",
+ "@typescript-eslint/scope-manager": "5.38.0",
+ "@typescript-eslint/type-utils": "5.38.0",
+ "@typescript-eslint/utils": "5.38.0",
"debug": "^4.3.4",
- "functional-red-black-tree": "^1.0.1",
"ignore": "^5.2.0",
"regexpp": "^3.2.0",
"semver": "^7.3.7",
@@ -1454,14 +1461,14 @@
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "5.37.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.37.0.tgz",
- "integrity": "sha512-01VzI/ipYKuaG5PkE5+qyJ6m02fVALmMPY3Qq5BHflDx3y4VobbLdHQkSMg9VPRS4KdNt4oYTMaomFoHonBGAw==",
+ "version": "5.38.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.38.0.tgz",
+ "integrity": "sha512-/F63giJGLDr0ms1Cr8utDAxP2SPiglaD6V+pCOcG35P2jCqdfR7uuEhz1GIC3oy4hkUF8xA1XSXmd9hOh/a5EA==",
"dev": true,
"dependencies": {
- "@typescript-eslint/scope-manager": "5.37.0",
- "@typescript-eslint/types": "5.37.0",
- "@typescript-eslint/typescript-estree": "5.37.0",
+ "@typescript-eslint/scope-manager": "5.38.0",
+ "@typescript-eslint/types": "5.38.0",
+ "@typescript-eslint/typescript-estree": "5.38.0",
"debug": "^4.3.4"
},
"engines": {
@@ -1481,13 +1488,13 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "5.37.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.37.0.tgz",
- "integrity": "sha512-F67MqrmSXGd/eZnujjtkPgBQzgespu/iCZ+54Ok9X5tALb9L2v3G+QBSoWkXG0p3lcTJsL+iXz5eLUEdSiJU9Q==",
+ "version": "5.38.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.38.0.tgz",
+ "integrity": "sha512-ByhHIuNyKD9giwkkLqzezZ9y5bALW8VNY6xXcP+VxoH4JBDKjU5WNnsiD4HJdglHECdV+lyaxhvQjTUbRboiTA==",
"dev": true,
"dependencies": {
- "@typescript-eslint/types": "5.37.0",
- "@typescript-eslint/visitor-keys": "5.37.0"
+ "@typescript-eslint/types": "5.38.0",
+ "@typescript-eslint/visitor-keys": "5.38.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -1498,13 +1505,13 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "5.37.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.37.0.tgz",
- "integrity": "sha512-BSx/O0Z0SXOF5tY0bNTBcDEKz2Ec20GVYvq/H/XNKiUorUFilH7NPbFUuiiyzWaSdN3PA8JV0OvYx0gH/5aFAQ==",
+ "version": "5.38.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.38.0.tgz",
+ "integrity": "sha512-iZq5USgybUcj/lfnbuelJ0j3K9dbs1I3RICAJY9NZZpDgBYXmuUlYQGzftpQA9wC8cKgtS6DASTvF3HrXwwozA==",
"dev": true,
"dependencies": {
- "@typescript-eslint/typescript-estree": "5.37.0",
- "@typescript-eslint/utils": "5.37.0",
+ "@typescript-eslint/typescript-estree": "5.38.0",
+ "@typescript-eslint/utils": "5.38.0",
"debug": "^4.3.4",
"tsutils": "^3.21.0"
},
@@ -1525,9 +1532,9 @@
}
},
"node_modules/@typescript-eslint/types": {
- "version": "5.37.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.37.0.tgz",
- "integrity": "sha512-3frIJiTa5+tCb2iqR/bf7XwU20lnU05r/sgPJnRpwvfZaqCJBrl8Q/mw9vr3NrNdB/XtVyMA0eppRMMBqdJ1bA==",
+ "version": "5.38.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.38.0.tgz",
+ "integrity": "sha512-HHu4yMjJ7i3Cb+8NUuRCdOGu2VMkfmKyIJsOr9PfkBVYLYrtMCK/Ap50Rpov+iKpxDTfnqvDbuPLgBE5FwUNfA==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -1538,13 +1545,13 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "5.37.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.37.0.tgz",
- "integrity": "sha512-JkFoFIt/cx59iqEDSgIGnQpCTRv96MQnXCYvJi7QhBC24uyuzbD8wVbajMB1b9x4I0octYFJ3OwjAwNqk1AjDA==",
+ "version": "5.38.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.38.0.tgz",
+ "integrity": "sha512-6P0RuphkR+UuV7Avv7MU3hFoWaGcrgOdi8eTe1NwhMp2/GjUJoODBTRWzlHpZh6lFOaPmSvgxGlROa0Sg5Zbyg==",
"dev": true,
"dependencies": {
- "@typescript-eslint/types": "5.37.0",
- "@typescript-eslint/visitor-keys": "5.37.0",
+ "@typescript-eslint/types": "5.38.0",
+ "@typescript-eslint/visitor-keys": "5.38.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -1565,15 +1572,15 @@
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "5.37.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.37.0.tgz",
- "integrity": "sha512-jUEJoQrWbZhmikbcWSMDuUSxEE7ID2W/QCV/uz10WtQqfOuKZUqFGjqLJ+qhDd17rjgp+QJPqTdPIBWwoob2NQ==",
+ "version": "5.38.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.38.0.tgz",
+ "integrity": "sha512-6sdeYaBgk9Fh7N2unEXGz+D+som2QCQGPAf1SxrkEr+Z32gMreQ0rparXTNGRRfYUWk/JzbGdcM8NSSd6oqnTA==",
"dev": true,
"dependencies": {
"@types/json-schema": "^7.0.9",
- "@typescript-eslint/scope-manager": "5.37.0",
- "@typescript-eslint/types": "5.37.0",
- "@typescript-eslint/typescript-estree": "5.37.0",
+ "@typescript-eslint/scope-manager": "5.38.0",
+ "@typescript-eslint/types": "5.38.0",
+ "@typescript-eslint/typescript-estree": "5.38.0",
"eslint-scope": "^5.1.1",
"eslint-utils": "^3.0.0"
},
@@ -1589,12 +1596,12 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "5.37.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.37.0.tgz",
- "integrity": "sha512-Hp7rT4cENBPIzMwrlehLW/28EVCOcE9U1Z1BQTc8EA8v5qpr7GRGuG+U58V5tTY48zvUOA3KHvw3rA8tY9fbdA==",
+ "version": "5.38.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.38.0.tgz",
+ "integrity": "sha512-MxnrdIyArnTi+XyFLR+kt/uNAcdOnmT+879os7qDRI+EYySR4crXJq9BXPfRzzLGq0wgxkwidrCJ9WCAoacm1w==",
"dev": true,
"dependencies": {
- "@typescript-eslint/types": "5.37.0",
+ "@typescript-eslint/types": "5.38.0",
"eslint-visitor-keys": "^3.3.0"
},
"engines": {
@@ -1734,6 +1741,11 @@
"node": ">=8"
}
},
+ "node_modules/async": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
+ "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
+ },
"node_modules/babel-jest": {
"version": "29.0.3",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.0.3.tgz",
@@ -2721,12 +2733,6 @@
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
},
- "node_modules/functional-red-black-tree": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
- "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==",
- "dev": true
- },
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -4722,9 +4728,9 @@
}
},
"node_modules/ts-jest": {
- "version": "29.0.0",
- "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.0.0.tgz",
- "integrity": "sha512-OxUaigbv5Aon3OMLY9HBtwkGMs1upWE/URrmmVQFzzOcGlEPVuWzGmXUIkWGt/95Dj/T6MGuTrHHGL6kT6Yn8g==",
+ "version": "29.0.1",
+ "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.0.1.tgz",
+ "integrity": "sha512-htQOHshgvhn93QLxrmxpiQPk69+M1g7govO1g6kf6GsjCv4uvRV0znVmDrrvjUrVCnTYeY4FBxTYYYD4airyJA==",
"dev": true,
"dependencies": {
"bs-logger": "0.x",
@@ -4862,9 +4868,9 @@
}
},
"node_modules/typedoc": {
- "version": "0.23.14",
- "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.23.14.tgz",
- "integrity": "sha512-s2I+ZKBET38EctZvbXp2GooHrNaKjWZkrwGEK/sttnOGiKJqU0vHrsdcwLgKZGuo2aedNL3RRPj1LnAAeYscig==",
+ "version": "0.23.15",
+ "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.23.15.tgz",
+ "integrity": "sha512-x9Zu+tTnwxb9YdVr+zvX7LYzyBl1nieOr6lrSHbHsA22/RJK2m4Y525WIg5Mj4jWCmfL47v6f4hUzY7EIuwS5w==",
"dev": true,
"dependencies": {
"lunr": "^2.3.9",
@@ -6079,6 +6085,12 @@
"integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==",
"dev": true
},
+ "@types/async": {
+ "version": "3.2.15",
+ "resolved": "https://registry.npmjs.org/@types/async/-/async-3.2.15.tgz",
+ "integrity": "sha512-PAmPfzvFA31mRoqZyTVsgJMsvbynR429UTTxhmfsUCrWGh3/fxOrzqBtaTPJsn4UtzTv4Vb0+/O7CARWb69N4g==",
+ "dev": true
+ },
"@types/babel__core": {
"version": "7.1.19",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.19.tgz",
@@ -6163,9 +6175,9 @@
}
},
"@types/jest": {
- "version": "29.0.1",
- "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.0.1.tgz",
- "integrity": "sha512-CAZrjLRZs4xEdIrfrdV74xK1Vo/BKQZwUcjJv3gp6gMeV3BsVxMnXTcgtYOKyphT4DPPo7jxVEVhuwJTQn3oPQ==",
+ "version": "29.0.3",
+ "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.0.3.tgz",
+ "integrity": "sha512-F6ukyCTwbfsEX5F2YmVYmM5TcTHy1q9P5rWlRbrk56KyMh3v9xRGUO3aa8+SkvMi0SHXtASJv1283enXimC0Og==",
"dev": true,
"requires": {
"expect": "^29.0.0",
@@ -6227,16 +6239,15 @@
"dev": true
},
"@typescript-eslint/eslint-plugin": {
- "version": "5.37.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.37.0.tgz",
- "integrity": "sha512-Fde6W0IafXktz1UlnhGkrrmnnGpAo1kyX7dnyHHVrmwJOn72Oqm3eYtddrpOwwel2W8PAK9F3pIL5S+lfoM0og==",
+ "version": "5.38.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.38.0.tgz",
+ "integrity": "sha512-GgHi/GNuUbTOeoJiEANi0oI6fF3gBQc3bGFYj40nnAPCbhrtEDf2rjBmefFadweBmO1Du1YovHeDP2h5JLhtTQ==",
"dev": true,
"requires": {
- "@typescript-eslint/scope-manager": "5.37.0",
- "@typescript-eslint/type-utils": "5.37.0",
- "@typescript-eslint/utils": "5.37.0",
+ "@typescript-eslint/scope-manager": "5.38.0",
+ "@typescript-eslint/type-utils": "5.38.0",
+ "@typescript-eslint/utils": "5.38.0",
"debug": "^4.3.4",
- "functional-red-black-tree": "^1.0.1",
"ignore": "^5.2.0",
"regexpp": "^3.2.0",
"semver": "^7.3.7",
@@ -6244,53 +6255,53 @@
}
},
"@typescript-eslint/parser": {
- "version": "5.37.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.37.0.tgz",
- "integrity": "sha512-01VzI/ipYKuaG5PkE5+qyJ6m02fVALmMPY3Qq5BHflDx3y4VobbLdHQkSMg9VPRS4KdNt4oYTMaomFoHonBGAw==",
+ "version": "5.38.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.38.0.tgz",
+ "integrity": "sha512-/F63giJGLDr0ms1Cr8utDAxP2SPiglaD6V+pCOcG35P2jCqdfR7uuEhz1GIC3oy4hkUF8xA1XSXmd9hOh/a5EA==",
"dev": true,
"requires": {
- "@typescript-eslint/scope-manager": "5.37.0",
- "@typescript-eslint/types": "5.37.0",
- "@typescript-eslint/typescript-estree": "5.37.0",
+ "@typescript-eslint/scope-manager": "5.38.0",
+ "@typescript-eslint/types": "5.38.0",
+ "@typescript-eslint/typescript-estree": "5.38.0",
"debug": "^4.3.4"
}
},
"@typescript-eslint/scope-manager": {
- "version": "5.37.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.37.0.tgz",
- "integrity": "sha512-F67MqrmSXGd/eZnujjtkPgBQzgespu/iCZ+54Ok9X5tALb9L2v3G+QBSoWkXG0p3lcTJsL+iXz5eLUEdSiJU9Q==",
+ "version": "5.38.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.38.0.tgz",
+ "integrity": "sha512-ByhHIuNyKD9giwkkLqzezZ9y5bALW8VNY6xXcP+VxoH4JBDKjU5WNnsiD4HJdglHECdV+lyaxhvQjTUbRboiTA==",
"dev": true,
"requires": {
- "@typescript-eslint/types": "5.37.0",
- "@typescript-eslint/visitor-keys": "5.37.0"
+ "@typescript-eslint/types": "5.38.0",
+ "@typescript-eslint/visitor-keys": "5.38.0"
}
},
"@typescript-eslint/type-utils": {
- "version": "5.37.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.37.0.tgz",
- "integrity": "sha512-BSx/O0Z0SXOF5tY0bNTBcDEKz2Ec20GVYvq/H/XNKiUorUFilH7NPbFUuiiyzWaSdN3PA8JV0OvYx0gH/5aFAQ==",
+ "version": "5.38.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.38.0.tgz",
+ "integrity": "sha512-iZq5USgybUcj/lfnbuelJ0j3K9dbs1I3RICAJY9NZZpDgBYXmuUlYQGzftpQA9wC8cKgtS6DASTvF3HrXwwozA==",
"dev": true,
"requires": {
- "@typescript-eslint/typescript-estree": "5.37.0",
- "@typescript-eslint/utils": "5.37.0",
+ "@typescript-eslint/typescript-estree": "5.38.0",
+ "@typescript-eslint/utils": "5.38.0",
"debug": "^4.3.4",
"tsutils": "^3.21.0"
}
},
"@typescript-eslint/types": {
- "version": "5.37.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.37.0.tgz",
- "integrity": "sha512-3frIJiTa5+tCb2iqR/bf7XwU20lnU05r/sgPJnRpwvfZaqCJBrl8Q/mw9vr3NrNdB/XtVyMA0eppRMMBqdJ1bA==",
+ "version": "5.38.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.38.0.tgz",
+ "integrity": "sha512-HHu4yMjJ7i3Cb+8NUuRCdOGu2VMkfmKyIJsOr9PfkBVYLYrtMCK/Ap50Rpov+iKpxDTfnqvDbuPLgBE5FwUNfA==",
"dev": true
},
"@typescript-eslint/typescript-estree": {
- "version": "5.37.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.37.0.tgz",
- "integrity": "sha512-JkFoFIt/cx59iqEDSgIGnQpCTRv96MQnXCYvJi7QhBC24uyuzbD8wVbajMB1b9x4I0octYFJ3OwjAwNqk1AjDA==",
+ "version": "5.38.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.38.0.tgz",
+ "integrity": "sha512-6P0RuphkR+UuV7Avv7MU3hFoWaGcrgOdi8eTe1NwhMp2/GjUJoODBTRWzlHpZh6lFOaPmSvgxGlROa0Sg5Zbyg==",
"dev": true,
"requires": {
- "@typescript-eslint/types": "5.37.0",
- "@typescript-eslint/visitor-keys": "5.37.0",
+ "@typescript-eslint/types": "5.38.0",
+ "@typescript-eslint/visitor-keys": "5.38.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -6299,26 +6310,26 @@
}
},
"@typescript-eslint/utils": {
- "version": "5.37.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.37.0.tgz",
- "integrity": "sha512-jUEJoQrWbZhmikbcWSMDuUSxEE7ID2W/QCV/uz10WtQqfOuKZUqFGjqLJ+qhDd17rjgp+QJPqTdPIBWwoob2NQ==",
+ "version": "5.38.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.38.0.tgz",
+ "integrity": "sha512-6sdeYaBgk9Fh7N2unEXGz+D+som2QCQGPAf1SxrkEr+Z32gMreQ0rparXTNGRRfYUWk/JzbGdcM8NSSd6oqnTA==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.9",
- "@typescript-eslint/scope-manager": "5.37.0",
- "@typescript-eslint/types": "5.37.0",
- "@typescript-eslint/typescript-estree": "5.37.0",
+ "@typescript-eslint/scope-manager": "5.38.0",
+ "@typescript-eslint/types": "5.38.0",
+ "@typescript-eslint/typescript-estree": "5.38.0",
"eslint-scope": "^5.1.1",
"eslint-utils": "^3.0.0"
}
},
"@typescript-eslint/visitor-keys": {
- "version": "5.37.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.37.0.tgz",
- "integrity": "sha512-Hp7rT4cENBPIzMwrlehLW/28EVCOcE9U1Z1BQTc8EA8v5qpr7GRGuG+U58V5tTY48zvUOA3KHvw3rA8tY9fbdA==",
+ "version": "5.38.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.38.0.tgz",
+ "integrity": "sha512-MxnrdIyArnTi+XyFLR+kt/uNAcdOnmT+879os7qDRI+EYySR4crXJq9BXPfRzzLGq0wgxkwidrCJ9WCAoacm1w==",
"dev": true,
"requires": {
- "@typescript-eslint/types": "5.37.0",
+ "@typescript-eslint/types": "5.38.0",
"eslint-visitor-keys": "^3.3.0"
}
},
@@ -6411,6 +6422,11 @@
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
"dev": true
},
+ "async": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
+ "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
+ },
"babel-jest": {
"version": "29.0.3",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.0.3.tgz",
@@ -7158,12 +7174,6 @@
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
},
- "functional-red-black-tree": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
- "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==",
- "dev": true
- },
"gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -8630,9 +8640,9 @@
}
},
"ts-jest": {
- "version": "29.0.0",
- "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.0.0.tgz",
- "integrity": "sha512-OxUaigbv5Aon3OMLY9HBtwkGMs1upWE/URrmmVQFzzOcGlEPVuWzGmXUIkWGt/95Dj/T6MGuTrHHGL6kT6Yn8g==",
+ "version": "29.0.1",
+ "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.0.1.tgz",
+ "integrity": "sha512-htQOHshgvhn93QLxrmxpiQPk69+M1g7govO1g6kf6GsjCv4uvRV0znVmDrrvjUrVCnTYeY4FBxTYYYD4airyJA==",
"dev": true,
"requires": {
"bs-logger": "0.x",
@@ -8703,9 +8713,9 @@
"dev": true
},
"typedoc": {
- "version": "0.23.14",
- "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.23.14.tgz",
- "integrity": "sha512-s2I+ZKBET38EctZvbXp2GooHrNaKjWZkrwGEK/sttnOGiKJqU0vHrsdcwLgKZGuo2aedNL3RRPj1LnAAeYscig==",
+ "version": "0.23.15",
+ "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.23.15.tgz",
+ "integrity": "sha512-x9Zu+tTnwxb9YdVr+zvX7LYzyBl1nieOr6lrSHbHsA22/RJK2m4Y525WIg5Mj4jWCmfL47v6f4hUzY7EIuwS5w==",
"dev": true,
"requires": {
"lunr": "^2.3.9",
diff --git a/package.json b/package.json
index f793732..633e611 100644
--- a/package.json
+++ b/package.json
@@ -1,24 +1,28 @@
{
"name": "typedoc-plugin-versions-cli",
- "version": "0.0.1",
+ "version": "0.1.0",
"description": "A companion CLI tool for typedoc-plugins-versions.",
"main": "./dist/index.js",
"bin": {
- "tpv": "./dist/cli.js",
"typedoc-plugin-versions": "./dist/cli.js",
- "tpv-cli": "./dist/cli.js",
- "typedoc-plugin-versions-cli": "./dist/cli.js"
+ "typedoc-plugin-versions-cli": "./dist/cli.js",
+ "typedoc-versions": "./dist/cli.js",
+ "tpv": "./dist/cli.js"
},
"types": "./types/index.d.ts",
"scripts": {
"test": "jest",
+ "pretest": "npm run docs",
"posttest": "npm run format:check",
"format": "prettier -w . --ignore-path .gitignore",
"format:check": "prettier -c . --ignore-path .gitignore",
"build": "tsc",
"prebuild": "npm run format",
"docs": "typedoc",
- "predocs": "npm run format",
+ "postdocs": "npm run docs:purge",
+ "docs:purge": "npm run tpv purge -- -y",
+ "postdocs:purge": "npm run docs:sync",
+ "docs:sync": "npm run tpv sync -- -y --symlinks",
"tpv": "ts-node src/cli.ts",
"tpv:dist": "node dist/cli.js"
},
@@ -36,6 +40,11 @@
"cid"
],
"author": "Tobey Blaber (https://github.com/toebeann)",
+ "homepage": "https://toebeann.github.io/typedoc-plugin-versions-cli",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/toebeann/typedoc-plugin-versions-cli.git"
+ },
"funding": [
{
"type": "github",
@@ -62,6 +71,7 @@
"darwin"
],
"dependencies": {
+ "async": "^3.2.4",
"cli-diff": "^1.0.0",
"prompts": "^2.4.2",
"semver": "^7.3.7",
@@ -69,21 +79,22 @@
},
"devDependencies": {
"@tsconfig/node14": "^1.0.3",
+ "@types/async": "^3.2.15",
"@types/fs-extra": "^9.0.13",
- "@types/jest": "^29.0.1",
+ "@types/jest": "^29.0.3",
"@types/prompts": "^2.0.14",
"@types/semver": "^7.3.12",
"@types/yargs": "^17.0.12",
- "@typescript-eslint/eslint-plugin": "^5.37.0",
- "@typescript-eslint/parser": "^5.37.0",
+ "@typescript-eslint/eslint-plugin": "^5.38.0",
+ "@typescript-eslint/parser": "^5.38.0",
"eslint": "^8.23.1",
"eslint-config-prettier": "^8.5.0",
"fs-extra": "^10.1.0",
"jest": "^29.0.3",
"prettier": "^2.7.1",
- "ts-jest": "^29.0.0",
+ "ts-jest": "^29.0.1",
"ts-node": "^10.9.1",
- "typedoc": "^0.23.14",
+ "typedoc": "^0.23.15",
"typedoc-plugin-versions": "^0.2.0",
"typescript": "^4.8.3"
},
diff --git a/src/cli.ts b/src/cli.ts
index 5094849..306b5ac 100644
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -1,20 +1,10 @@
#!/usr/bin/env node
-import yargs from 'yargs';
-import { hideBin } from 'yargs/helpers';
-
-export const cli = yargs(hideBin(process.argv))
- .parserConfiguration({ 'duplicate-arguments-array': false })
- .commandDir('commands', { extensions: ['ts', 'js'] })
- .strictCommands()
- .demandCommand(1, '')
- .help()
- .version()
- .alias('h', 'help')
- .group(['help', 'version'], 'Help:');
+import process from 'node:process';
+import { cli } from './';
(async () => {
try {
- await cli.argv;
+ await cli().argv;
} catch (e) {
process.exitCode = 1;
console.error(e);
diff --git a/src/commands/builders.ts b/src/commands/builders.ts
new file mode 100644
index 0000000..f72ea42
--- /dev/null
+++ b/src/commands/builders.ts
@@ -0,0 +1,83 @@
+import { join, relative, resolve } from 'node:path';
+import { cwd } from 'node:process';
+import { findFile, findTsConfigFile, isDir } from '../utils';
+
+/**
+ * `out` option builder for {@link https://yargs.js.org/docs yargs} commands.
+ * @internal
+ * Intended for internal use; may not be exported in future.
+ */
+export const out = {
+ type: 'string' as const,
+ normalize: true,
+ description: 'path to your typedoc output directory',
+ defaultDescription: `"docs" (if not set in config)`,
+ coerce: async (path: string | string[]): Promise => {
+ path = resolve(
+ typeof path === 'string' ? path : (path.at(-1) as string)
+ );
+
+ if (!(await isDir(path)))
+ throw new Error(
+ `Directory does not exist: ${relative(cwd(), path)}`
+ );
+
+ return path;
+ },
+};
+
+/**
+ * `typedoc` option builder for {@link https://yargs.js.org/docs yargs} commands.
+ * @internal
+ * Intended for internal use; may not be exported in future.
+ */
+export const typedoc = {
+ type: 'string' as const,
+ normalize: true,
+ description: 'path to your typedoc config',
+ default: '.',
+ coerce: (path: string | string[]): Promise => {
+ path = typeof path === 'string' ? path : (path.at(-1) as string);
+ return findFile(path, [
+ 'typedoc.json',
+ 'typedoc.js',
+ join('.config', 'typedoc.js'),
+ join('.config', 'typedoc.json'),
+ ]);
+ },
+};
+
+/**
+ * `tsconfig` option builder for {@link https://yargs.js.org/docs yargs} commands.
+ * @internal
+ * Intended for internal use; may not be exported in future.
+ */
+export const tsconfig = {
+ type: 'string' as const,
+ normalize: true,
+ description: 'path to your tsconfig',
+ default: '.',
+ coerce: (path: string | string[]): string => {
+ path = typeof path === 'string' ? path : (path.at(-1) as string);
+ return findTsConfigFile(path);
+ },
+};
+
+/**
+ * Commonly used option builders for {@link https://yargs.js.org/docs yargs} commands.
+ * @internal
+ * Intended for internal use; may not be exported in future.
+ */
+export const commonOptions = { out, typedoc, tsconfig };
+
+/**
+ * `yes` option builder for {@link https://yargs.js.org/docs yargs} commands.
+ * @internal
+ * Intended for internal use; may not be exported in future.
+ */
+export const yes = {
+ alias: 'y',
+ type: 'boolean' as const,
+ description: 'automatically confirm prompts',
+ default: false,
+};
diff --git a/src/commands/purge.ts b/src/commands/purge.ts
new file mode 100644
index 0000000..a543d8c
--- /dev/null
+++ b/src/commands/purge.ts
@@ -0,0 +1,448 @@
+import { EOL } from 'node:os';
+import { join, relative, resolve } from 'node:path';
+import { cwd } from 'node:process';
+import { each, filter } from 'async';
+import { version } from 'typedoc-plugin-versions';
+import {
+ loadMetadata,
+ refreshMetadata,
+ getSemanticVersion,
+ getVersionAlias,
+} from 'typedoc-plugin-versions/src/etc/utils';
+import { rm } from 'fs-extra';
+import prompts from 'prompts';
+import {
+ gt,
+ major,
+ minor,
+ patch,
+ RangeOptions,
+ rcompare,
+ satisfies,
+} from 'semver';
+import { Argv } from 'yargs';
+import { commonOptions, yes } from './builders';
+import { coerceStringArray, drop, exclude, getOptions, isDir } from '../utils';
+import { Args, Options, refreshedMetadata } from '../';
+
+/**
+ * The `purge` command string syntax.
+ */
+export const command = ['purge [versions..]'];
+/**
+ * Description of the `purge` command.
+ */
+export const description = 'purge old doc builds';
+
+/**
+ * {@link https://yargs.js.org/docs yargs} builder for the `purge` command.
+ * @param {Argv} yargs A yargs instance used for building the command-specific options.
+ * @returns The yargs instance.
+ */
+export const builder = (yargs: Argv) =>
+ yargs
+ .options({
+ stale: {
+ alias: 's',
+ type: 'boolean' as const,
+ description: 'purge stale dev versions',
+ default: true,
+ },
+ major: {
+ type: 'number',
+ description:
+ 'purge all but the specified number of major versions',
+ default: Infinity,
+ coerce: coercePurgeVersionsNum,
+ },
+ minor: {
+ type: 'number',
+ description:
+ 'purge all but the specified number of minor versions per major version',
+ default: Infinity,
+ coerce: coercePurgeVersionsNum,
+ },
+ patch: {
+ type: 'number',
+ description:
+ 'purge all but the specified number of patch versions per minor version',
+ default: Infinity,
+ coerce: coercePurgeVersionsNum,
+ },
+ exclude: {
+ alias: 'e',
+ type: 'string',
+ array: true,
+ description: 'exclude versions from being purged',
+ coerce: coerceStringArray,
+ },
+ prerelease: {
+ alias: 'pre',
+ type: 'boolean',
+ default: false,
+ description:
+ 'include prerelease versions when evaluating ranges',
+ },
+ yes,
+ ...commonOptions,
+ })
+ .positional('versions', {
+ type: 'string',
+ array: true,
+ description: 'versions to purge',
+ coerce: coerceStringArray,
+ });
+
+/**
+ * {@link https://yargs.js.org/docs yargs} handler for the `purge` command.
+ * @param {T} args The {@link @types/yargs!yargs.Argv argv} object parsed from the command line arguments.
+ * @typeParam T The type of the parsed {@link @types/yargs!yargs.Argv argv} object.
+ */
+export async function handler>>(
+ args: T
+): Promise {
+ const options = await getOptions(args);
+ const metadata = refreshMetadata(
+ loadMetadata(options.out),
+ options.out,
+ options.versions.stable,
+ options.versions.dev
+ ) as refreshedMetadata;
+ const includePrerelease = args.prerelease;
+ const versions = exclude(metadata.versions, (v) =>
+ shouldExclude(v, args.exclude, {
+ loose: true,
+ includePrerelease,
+ })
+ );
+
+ // args.versions
+ const pending = getVersionsToPurge(versions, args.versions, {
+ loose: true,
+ includePrerelease,
+ });
+ drop(versions, pending);
+
+ // args.major
+ if (isValid(args.major)) {
+ const purge = getMajorVersionsToPurge(versions, args.major, {
+ loose: true,
+ includePrerelease,
+ });
+ pending.push(...purge);
+ drop(versions, purge);
+ }
+
+ // args.minor
+ if (isValid(args.minor)) {
+ const purge = getMinorVersionsToPurge(versions, args.minor, {
+ loose: true,
+ includePrerelease,
+ });
+ pending.push(...purge);
+ drop(versions, purge);
+ }
+
+ // args.patch
+ if (isValid(args.patch)) {
+ const purge = getPatchVersionsToPurge(versions, args.patch, {
+ loose: true,
+ includePrerelease,
+ });
+ pending.push(...purge);
+ drop(versions, purge);
+ }
+
+ // args.stale
+ if (args.stale) {
+ const purge = await getStaleVersionsToPurge(versions, options);
+ pending.push(...purge);
+ drop(versions, purge);
+ }
+
+ if (pending.length === 0) {
+ console.log('Nothing to purge!');
+ return;
+ }
+
+ console.log(
+ (args.yes ? 'Purging:' : 'Pending purge:').concat(EOL).concat(
+ pending
+ .filter((v, i, s) => s.indexOf(v) === i)
+ .sort(rcompare)
+ .map((v) => `- ${relative(cwd(), join(options.out, v))}`)
+ .join(EOL)
+ )
+ );
+
+ if (
+ args.yes ||
+ (
+ await prompts({
+ name: 'confirm',
+ type: 'confirm',
+ initial: false,
+ message: 'Purge docs for these versions?',
+ })
+ ).confirm
+ ) {
+ if (!args.yes) console.log('');
+
+ await each(pending, async (version) => {
+ const dir = resolve(join(options.out, version));
+ await rm(dir, { recursive: true, force: true });
+ });
+ }
+}
+
+/**
+ * Determines whether a given {@link typedoc-plugin-versions!version version} is considered `stable` by
+ * {@link https://citkane.github.io/typedoc-plugin-versions typedoc-plugin-versions}.
+ * @internal
+ * Intended for internal use; may not be exported in future.
+ * @param {version} version The version.
+ * @param {version | 'auto'} stable The {@link typedoc-plugin-versions!versionsOptions.stable versions.stable} option from the typedoc config.
+ * @returns {boolean} Whether the version is considered `stable`.
+ */
+export const isStable = (version: version, stable: version | 'auto'): boolean =>
+ getVersionAlias(version, stable) === 'stable';
+
+/**
+ * Determines whether a given {@link typedoc-plugin-versions!version version} is considered `dev` by
+ * {@link https://citkane.github.io/typedoc-plugin-versions typedoc-plugin-versions}.
+ * @internal
+ * Intended for internal use; may not be exported in future.
+ * @param {version} version The version.
+ * @param {version | 'auto'} dev The {@link typedoc-plugin-versions!versionsOptions.dev versions.dev} option from the typedoc config.
+ * @returns {boolean} Whether the version is considered `dev`.
+ */
+export const isDev = (version: version, dev: version | 'auto'): boolean =>
+ getVersionAlias(version, undefined, dev) === 'dev';
+
+/**
+ * Determines whether a given {@link typedoc-plugin-versions!version version} is "pinned" in the user's
+ * {@link https://citkane.github.io/typedoc-plugin-versions typedoc-plugin-versions} configuration, i.e. marked as `stable` or `dev`.
+ * @internal
+ * Intended for internal use; may not be exported in future.
+ * @param {version} version The version.
+ * @param {version | 'auto'} stable The {@link typedoc-plugin-versions!versionsOptions.stable versions.stable} option from the typedoc config.
+ * @param {version | 'auto'} dev The {@link typedoc-plugin-versions!versionsOptions.dev versions.dev} option from the typedoc config.
+ * @returns {boolean} Whether the version is pinned.
+ */
+export const isPinned = (
+ version: version,
+ stable: version | 'auto',
+ dev: version | 'auto'
+): boolean =>
+ (stable !== 'auto' &&
+ getSemanticVersion(version) === getSemanticVersion(stable)) ||
+ (dev !== 'auto' && getSemanticVersion(version) === getSemanticVersion(dev));
+
+/**
+ * Determines whether a given {@link typedoc-plugin-versions!version version} is "stale," e.g. prerelease versions
+ * that have been superseded by non-prerelease versions.
+ * @internal
+ * Intended for internal use; may not be exported in future.
+ * @param {version} version The version.
+ * @param {readonly version[]} versions An array of relevant {@link typedoc-plugin-versions!version versions} for reference.
+ * @param {version | 'auto'} stable The {@link typedoc-plugin-versions!versionsOptions.stable versions.stable} option from the typedoc config.
+ * @param {version | 'auto'} dev The {@link typedoc-plugin-versions!versionsOptions.dev versions.dev} option from the typedoc config.
+ * @returns {boolean} Whether the version is stale.
+ */
+export const isStale = (
+ version: version,
+ versions: readonly version[],
+ stable: version | 'auto',
+ dev: version | 'auto'
+): boolean =>
+ !isPinned(version, stable, dev) &&
+ isDev(version, dev) &&
+ versions.some((v) => isStable(v, stable) && gt(v, version, true));
+
+/**
+ * Determines whether a number passed is valid, i.e. is finite, non-NaN and greater than or equal to 0.
+ * @internal
+ * Intended for internal use; may not be exported in future.
+ * @param {number} number The number.
+ * @returns Whether the number is valid.
+ */
+export const isValid = (number: number): boolean =>
+ typeof number === 'number' &&
+ isFinite(number) &&
+ !isNaN(number) &&
+ number >= 0;
+
+/**
+ * Coerces a passed argument to a number. If the argument is an array of numbers, coerces only the last element.
+ * @internal
+ * Intended for internal use; may not be exported in future.
+ * @param {number | number[]} number
+ * @returns {number} The coerced number, or {@link typescript!Infinity Infinity} if the number was invalid.
+ */
+export const coercePurgeVersionsNum = (number: number | number[]): number =>
+ typeof number === 'number'
+ ? isValid(number)
+ ? number
+ : Infinity
+ : coercePurgeVersionsNum(number.at(-1) ?? Infinity);
+
+/**
+ * Determines whether a given {@link typedoc-plugin-versions!version version} should be excluded from purging based on
+ * the given {@link http://yargs.js.org yargs} `exclude` argument.
+ * @internal
+ * Intended for internal use; may not be exported in future.
+ * @param {version} version The version.
+ * @param {string[]} [exclude=[]] The {@link http://yargs.js.org yargs} `exclude` argument.
+ * @param {RangeOptions} [options=\{\}] Options to pass to {@link https://github.com/npm/node-semver#readme semver.satisfies}.
+ * @returns {boolean} Whether the version should be excluded from purging.
+ */
+export const shouldExclude = (
+ version: version,
+ exclude: string[] = [],
+ options: RangeOptions = {}
+): boolean => exclude.some((e) => satisfies(version, e, options));
+
+/**
+ * Filters an array of {@link typedoc-plugin-versions!version versions} to those which should be purged,
+ * based on whether they match a given array of semantic version ranges.
+ * @internal
+ * Intended for internal use; may not be exported in future.
+ * @param {readonly version[]} versions The versions to filter.
+ * @param {readonly string[]} [versionsToPurge=[]] The semantic version ranges to match.
+ * @param {RangeOptions} [options=\{\}] Options to pass to {@link https://github.com/npm/node-semver#readme semver.satisfies}.
+ * @returns {version[]} The filtered array consisting only of {@link typedoc-plugin-versions!version versions} which should be purged.
+ */
+export const getVersionsToPurge = (
+ versions: readonly version[],
+ versionsToPurge: readonly string[] = [],
+ options: RangeOptions = {}
+): version[] => [
+ ...versions.filter((version) =>
+ versionsToPurge.some((v) => satisfies(version, v, options))
+ ),
+];
+
+/**
+ * Filters an array of {@link typedoc-plugin-versions!version versions} to those which should be purged,
+ * based on the given {@link http://yargs.js.org yargs} `major` argument.
+ * @internal
+ * Intended for internal use; may not be exported in future.
+ * @param {readonly version[]} versions The versions to filter.
+ * @param {number} number The {@link http://yargs.js.org yargs} `major` argument.
+ * @param {RangeOptions} [options=\{\}] Options to pass to {@link https://github.com/npm/node-semver#readme semver.satisfies}.
+ * @returns {version[]} The filtered array consisting only of {@link typedoc-plugin-versions!version versions} which should be purged.
+ */
+export function getMajorVersionsToPurge(
+ versions: readonly version[],
+ number: number,
+ options: RangeOptions = {}
+): version[] {
+ const majorVersions = versions
+ .map((v) => `${major(v)}.x.x`)
+ .filter((v, i, s) => s.indexOf(v) === i)
+ .slice(number);
+
+ return versions.filter((version) =>
+ majorVersions.some((v) => satisfies(version, v, options))
+ );
+}
+
+/**
+ * Filters an array of {@link typedoc-plugin-versions!version versions} to those which should be purged,
+ * based on the given {@link http://yargs.js.org yargs} `minor` argument.
+ * @internal
+ * Intended for internal use; may not be exported in future.
+ * @param {readonly version[]} versions The versions to filter.
+ * @param {number} number The {@link http://yargs.js.org yargs} `minor` argument.
+ * @param {RangeOptions} [options=\{\}] Options to pass to {@link https://github.com/npm/node-semver#readme semver.satisfies}.
+ * @returns {version[]} The filtered array consisting only of {@link typedoc-plugin-versions!version versions} which should be purged.
+ */
+export function getMinorVersionsToPurge(
+ versions: readonly version[],
+ number: number,
+ options: RangeOptions = {}
+): version[] {
+ const minorVersionsByMajor = new Map();
+ for (const version of versions) {
+ const key = `${major(version)}`;
+ minorVersionsByMajor.set(key, [
+ ...(minorVersionsByMajor.get(key) ?? []),
+ `${key}.${minor(version)}.x`,
+ ]);
+ }
+
+ const minorVersions = sliceValues(minorVersionsByMajor, number).flat();
+ return versions.filter((version) =>
+ minorVersions.some((v) => satisfies(version, v, options))
+ );
+}
+
+/**
+ * Filters an array of {@link typedoc-plugin-versions!version versions} to those which should be purged,
+ * based on the given {@link http://yargs.js.org yargs} `patch` argument.
+ * @internal
+ * Intended for internal use; may not be exported in future.
+ * @param {readonly version[]} versions The versions to filter.
+ * @param {number} number The {@link http://yargs.js.org yargs} `patch` argument.
+ * @param {RangeOptions} [options=\{\}] Options to pass to {@link https://github.com/npm/node-semver#readme semver.satisfies}.
+ * @returns {version[]} The filtered array consisting only of {@link typedoc-plugin-versions!version versions} which should be purged.
+ */
+export function getPatchVersionsToPurge(
+ versions: readonly version[],
+ number: number,
+ options: RangeOptions = {}
+): version[] {
+ const patchVersionsByMinor = new Map();
+ for (const version of versions) {
+ const key = `${major(version)}.${minor(version)}`;
+ patchVersionsByMinor.set(key, [
+ ...(patchVersionsByMinor.get(key) ?? []),
+ `${key}.${patch(version)}`,
+ ]);
+ }
+
+ const patchVersions = sliceValues(patchVersionsByMinor, number).flat();
+ return versions.filter((version) =>
+ patchVersions.some((v) => satisfies(version, v, options))
+ );
+}
+
+/**
+ * Filters an array of {@link typedoc-plugin-versions!version versions} to those which should be purged,
+ * based on whether or not {@link isStale they are stale}.
+ * @internal
+ * Intended for internal use; may not be exported in future.
+ * @param {readonly version[]} versions The versions to filter.
+ * @param {Options} options The parsed {@link index!Options Options} object.
+ * @returns {version[]} The filtered array consisting only of {@link typedoc-plugin-versions!version versions} which should be purged.
+ */
+export const getStaleVersionsToPurge = (
+ versions: version[],
+ options: Options
+): Promise =>
+ filter(
+ versions,
+ async (version) =>
+ (await isDir(resolve(join(options.out, version)))) &&
+ isStale(
+ version,
+ versions,
+ options.versions.stable,
+ options.versions.dev
+ )
+ );
+
+/**
+ * Parses a given {@link typescript!Map Map}, filtering its values into unique values only, and returns them sliced by the given index.
+ * @internal
+ * Intended for internal use; may not be exported in future.
+ * @param {Map} map The map.
+ * @param {number} index The index.
+ * @returns {V[][]} The map of values as an array.
+ * @typeParam K The type of the {@link typescript!Map Map}'s keys.
+ * @typeParam V The type of the {@link typescript!Map Map}'s values.
+ */
+export const sliceValues = (map: Map, index: number): V[][] =>
+ [...map.values()].map((value) =>
+ value.filter((v, i, s) => s.indexOf(v) === i).slice(index)
+ );
diff --git a/src/commands/synchronize.ts b/src/commands/synchronize.ts
new file mode 100644
index 0000000..1d8ba86
--- /dev/null
+++ b/src/commands/synchronize.ts
@@ -0,0 +1,242 @@
+import { EOL } from 'node:os';
+import { join, relative, resolve } from 'node:path';
+import process, { cwd } from 'node:process';
+import { each } from 'async';
+import { metadata } from 'typedoc-plugin-versions';
+import {
+ getMetadataPath,
+ loadMetadata,
+ refreshMetadata,
+ saveMetadata,
+ makeJsKeys,
+ makeAliasLink,
+ makeMinorVersionLinks,
+ getSemanticVersion,
+} from 'typedoc-plugin-versions/src/etc/utils';
+import diff from 'cli-diff';
+import { readFile, writeFile } from 'fs-extra';
+import prompts from 'prompts';
+import { commonOptions, yes } from './builders';
+import { getOptions, isFile, isDir, unlinkBrokenSymlinks } from '../utils';
+import { Args, refreshedMetadata } from '../';
+
+/**
+ * The `synchronize` command string syntax.
+ */
+export const command = ['synchronize', 'sync'];
+/**
+ * Description of the `synchronize` command.
+ */
+export const description = 'synchronize metadata and symlinks';
+
+/**
+ * {@link https://yargs.js.org/docs yargs} builder for the `purge` command.
+ */
+export const builder = {
+ symlinks: {
+ type: 'boolean' as const,
+ description: 'always synchronize symlinks',
+ default: false,
+ },
+ yes,
+ ...commonOptions,
+};
+
+/**
+ * {@link https://yargs.js.org/docs yargs} handler for the `synchronize` command.
+ * @param {T} args The {@link @types/yargs!yargs.Argv argv} object parsed from the command line arguments.
+ * @typeParam T The type of the parsed {@link @types/yargs!yargs.Argv argv} object.
+ */
+export async function handler>(
+ args: T
+): Promise {
+ const options = await getOptions(args);
+
+ const metadata = loadMetadata(options.out);
+ const refreshedMetadata = refreshMetadata(
+ metadata,
+ options.out,
+ options.versions.stable,
+ options.versions.dev
+ ) as refreshedMetadata;
+
+ const packageVersion = getSemanticVersion();
+ if (!(await isDir(resolve(join(options.out, packageVersion))))) {
+ console.error(
+ `Missing docs for package.json version: ${packageVersion}${EOL}Did you forget to run typedoc?`
+ );
+ process.exitCode = 1;
+ return;
+ }
+
+ const changes = await getDiffs(options.out, metadata, refreshedMetadata);
+ if (!args.symlinks && changes.length === 0) {
+ console.log('Already up-to-date.');
+ return;
+ } else if (changes.length > 0) {
+ console.log(
+ (args.yes ? 'Synchonizing:' : 'Pending synchronization:')
+ .concat(`${EOL}${EOL}`)
+ .concat(
+ changes
+ .map((change) => `${change.label}:${EOL}${change.diff}`)
+ .join(EOL)
+ )
+ );
+ }
+
+ let symlinks = args.symlinks;
+ if (
+ changes.length > 0 &&
+ (args.yes ||
+ (
+ await prompts({
+ name: 'confirm',
+ type: 'confirm',
+ initial: false,
+ message: 'Apply pending synchronizations?',
+ })
+ ).confirm)
+ ) {
+ if (!args.yes) console.log('');
+
+ await each(changes, async (change) => {
+ console.time(change.label);
+ await change.save();
+ console.timeEnd(change.label);
+ });
+
+ symlinks = true;
+ }
+
+ if (symlinks) {
+ const symlinksLabel = 'symlinks';
+ console.time(symlinksLabel);
+ makeSymlinks(options.out, refreshedMetadata);
+ await unlinkBrokenSymlinks(options.out);
+ console.timeEnd(symlinksLabel);
+ }
+}
+
+/**
+ * An interface for diffs with a label and a callback for saving the changes.
+ * @internal
+ * Intended for internal use; may not be exported in future.
+ */
+export interface labelledDiff {
+ /**
+ * The diff generated by {@link cli-diff!diff cli-diff}.
+ */
+ diff: ReturnType;
+ /**
+ * The label for the diff.
+ */
+ label: string;
+ /**
+ * A callback which saves the changes.
+ */
+ save: () => Promise;
+}
+
+/**
+ * Generates {@link cli-diff!diff diffs} for all relevant {@link https://citkane.github.io/typedoc-plugin-versions typedoc-plugin-versions}
+ * metadata files between their current state and refreshed state, labelled by relative file path and generates callbacks to save the changes to disk.
+ * @internal
+ * Intended for internal use; may not be exported in future.
+ * @param {string} out The path to the user's typedoc `out` folder.
+ * @param {metadata} metadata The {@link https://citkane.github.io/typedoc-plugin-versions typedoc-plugin-versions}
+ * version {@link typedoc-plugin-versions!metadata metadata} currently saved to disk.
+ * @param {refreshedMetadata} refreshedMetadata Version {@link typedoc-plugin-versions!metadata metadata} freshly generated by
+ * {@link https://citkane.github.io/typedoc-plugin-versions typedoc-plugin-versions}, for comparison.
+ * @returns {Promise} The generated {@link labelledDiff labelled diffs}.
+ */
+export async function getDiffs(
+ out: string,
+ metadata: metadata,
+ refreshedMetadata: refreshedMetadata
+): Promise {
+ const versionsPath = resolve(join(out, 'versions.js'));
+ const versionsOutput = refreshVersionJs(refreshedMetadata);
+ const indexPath = resolve(join(out, 'index.html'));
+ const indexOutput = refreshIndexHtml(refreshedMetadata);
+ return [
+ {
+ diff: getMetadataDiff(metadata, refreshedMetadata),
+ label: relative(cwd(), getMetadataPath(out)),
+ save: async () => saveMetadata(refreshedMetadata, out),
+ },
+ {
+ diff: diff(
+ (await isFile(versionsPath))
+ ? await readFile(versionsPath, { encoding: 'utf8' })
+ : '',
+ versionsOutput
+ ),
+ label: relative(cwd(), versionsPath),
+ save: () => writeFile(versionsPath, versionsOutput),
+ },
+ {
+ diff: diff(
+ (await isFile(indexPath))
+ ? await readFile(indexPath, { encoding: 'utf8' })
+ : '',
+ refreshIndexHtml(refreshedMetadata)
+ ),
+ label: relative(cwd(), indexPath),
+ save: () => writeFile(indexPath, indexOutput),
+ },
+ ].filter((x) => x.diff.length > 0);
+}
+
+/**
+ * Generates a {@link cli-diff!diff diff} between two {@link typedoc-plugin-versions!metadata metadata} objects.
+ * @internal
+ * Intended for internal use; may not be exported in future.
+ * @param {metadata} left The left side of the diff comparison.
+ * @param {metadata} right The right side of the diff comparison.
+ * @returns {ReturnType} The generated {@link cli-diff!diff diff}.
+ */
+export const getMetadataDiff = (
+ left: metadata,
+ right: metadata
+): ReturnType =>
+ diff(
+ JSON.stringify(left, undefined, 2).concat(EOL),
+ JSON.stringify(right, undefined, 2).concat(EOL)
+ );
+
+/**
+ * Generates a {@link https://citkane.github.io/typedoc-plugin-versions typedoc-plugin-versions} `versions.js` string.
+ * @internal
+ * Intended for internal use; may not be exported in future.
+ * @param {metadata} metadata The {@link typedoc-plugin-versions!metadata metadata} object to use for generation.
+ * @returns {ReturnType} The generated `versions.js` string.
+ */
+export const refreshVersionJs = (
+ metadata: refreshedMetadata
+): ReturnType => makeJsKeys(metadata);
+
+/**
+ * Generates a {@link https://citkane.github.io/typedoc-plugin-versions typedoc-plugin-versions} `index.html` string.
+ * @internal
+ * Intended for internal use; may not be exported in future.
+ * @param {metadata} metadata The {@link typedoc-plugin-versions!metadata metadata} object to use for generation.
+ * @returns {string} The generated `index.html` string.
+ */
+export const refreshIndexHtml = (metadata: refreshedMetadata): string =>
+ ``;
+
+/**
+ * Generates all necessary symbolic links for {@link https://citkane.github.io/typedoc-plugin-versions typedoc-plugin-versions}.
+ * @internal
+ * Intended for internal use; may not be exported in future.
+ * @param {string} out The path to the user's typedoc `out` folder.
+ * @param {metadata} metadata The {@link typedoc-plugin-versions!metadata metadata} object to use for generation.
+ */
+export function makeSymlinks(out: string, metadata: refreshedMetadata): void {
+ makeAliasLink('stable', out, metadata.stable ?? metadata.dev);
+ makeAliasLink('dev', out, metadata.dev ?? metadata.stable);
+ makeMinorVersionLinks(metadata.versions, out);
+}
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..956cb58
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,78 @@
+import { argv } from 'node:process';
+import { metadata, version, versionsOptions } from 'typedoc-plugin-versions';
+import yargs, {
+ ArgumentsCamelCase,
+ Argv,
+ InferredOptionTypes,
+ Options as BuilderOptions,
+} from 'yargs';
+import { hideBin } from 'yargs/helpers';
+
+export * as purge from './commands/purge';
+export * as synchronize from './commands/synchronize';
+export * from './utils';
+
+/**
+ * Options object parsed from typedoc configs.
+ */
+export interface Options {
+ /**
+ * The typedoc `out` directory.
+ */
+ out: string;
+ /**
+ * Options for {@link https://citkane.github.io/typedoc-plugin-versions typedoc-plugin-versions}.
+ */
+ versions: Required;
+}
+
+/**
+ * Type guard for determining whether a given object implements the {@link Options} interface.
+ * @param {unknown} obj The object.
+ * @returns {obj is Options} Whether the object implements the {@link Options} interface.
+ */
+export const isOptions = (obj: unknown): obj is Options =>
+ obj !== null &&
+ typeof obj === 'object' &&
+ 'out' in obj &&
+ 'versions' in obj &&
+ typeof (obj).out === 'string' &&
+ typeof (obj).versions === 'object';
+
+/**
+ * The parsed {@link @types/yargs!yargs.Argv argv} which will be passed to a
+ * {@link https://yargs.js.org/docs yargs} {@link https://yargs.js.org/docs/#api-reference-commandcmd-desc-builder-handler command's}
+ * handler, its properties inferred from the builder used to compose the command.
+ * @typeParam T The type of the builder used to compose the command.
+ */
+export type Args = T extends Argv
+ ? ArgumentsCamelCase
+ : T extends { [key: string]: BuilderOptions }
+ ? ArgumentsCamelCase>
+ : unknown;
+
+/**
+ * Type alias for once a {@link typedoc-plugin-versions!metadata metadata} object has been refreshed via
+ * {@link typedoc-plugin-versions/etc/utils!refreshMetadata refreshMetadata}.
+ */
+export type refreshedMetadata = metadata & { versions: version[] } & (
+ | { stable: version; dev: version }
+ | { stable: version }
+ | { dev: version }
+ );
+
+/**
+ * {@link https://yargs.js.org/docs yargs} CLI builder.
+ * @internal
+ * Intended for internal use; may not be exported in future.
+ * @returns A new {@link https://yargs.js.org/docs yargs} instance with default options and commands.
+ */
+export const cli = () =>
+ yargs(hideBin(argv))
+ .commandDir('./commands', { extensions: ['ts', 'js'] })
+ .strictCommands()
+ .demandCommand(1, '')
+ .help()
+ .version()
+ .alias('h', 'help')
+ .group(['help', 'version'], 'Help:');
diff --git a/src/utils.ts b/src/utils.ts
new file mode 100644
index 0000000..fa0185d
--- /dev/null
+++ b/src/utils.ts
@@ -0,0 +1,320 @@
+import { join, relative, resolve } from 'node:path';
+import { cwd } from 'node:process';
+import { each, filter, find } from 'async';
+import {
+ pathExists,
+ pathExistsSync,
+ stat,
+ lstat,
+ readdir,
+ readJson,
+ readlink,
+ unlink,
+} from 'fs-extra';
+import {
+ findConfigFile,
+ getParsedCommandLineOfConfigFile,
+ sys,
+ formatDiagnosticsWithColorAndContext,
+} from 'typescript';
+import { commonOptions, out } from './commands/builders';
+import { Options, Args } from './';
+
+/**
+ * Compiles and parses common options from the config files and command line arguments into an {@link index!Options Options} object.
+ * @param {Args} args Command line arguments passed to the application.
+ * @returns {Promise} The parsed {@link index!Options Options} object.
+ * @typeParam O An object extending the {@link commands/builders!commonOptions commonOptions} object.
+ * @throws {@link typescript!Error Error} When the `out` option points to a directory which does not exist.
+ */
+export async function getOptions(
+ args: Args
+): Promise {
+ // get tsconfig, using typescript's own methods for reliability
+ const tsconfig = getParsedCommandLineOfConfigFile(
+ args.tsconfig,
+ {},
+ {
+ ...sys,
+ onUnRecoverableConfigFileDiagnostic: (diagnostic) =>
+ console.debug(
+ formatDiagnosticsWithColorAndContext([diagnostic], {
+ getCanonicalFileName: resolve,
+ getCurrentDirectory: () => cwd(),
+ getNewLine: () => sys.newLine,
+ })
+ ),
+ }
+ );
+
+ // load the typedoc config
+ const typedoc = await readJson(await args.typedoc);
+
+ // now attempt to discern the typedoc out docs from the given configs and command line args, reverting to './docs' if not specified
+ const out = resolve(
+ args.out ??
+ typedoc.out ??
+ tsconfig?.raw.typedocOptions?.out ??
+ join(cwd(), 'docs')
+ );
+
+ // check directory exists and throw if not
+ if (!(await isDir(out))) {
+ throw new Error(`Directory does not exist: ${relative(cwd(), out)}`);
+ }
+
+ // everything looks good, return our options object with defaults overridden by configs
+ return {
+ out,
+ versions: {
+ stable: 'auto',
+ dev: 'auto',
+ domLocation: 'false',
+ ...(tsconfig?.raw.typedocOptions?.versions ?? {}),
+ ...(typedoc.versions ?? {}),
+ },
+ };
+}
+
+/**
+ * Parses the `out` option from the given command line args, processing the user's config files if available.
+ * @param {Args} args Command line arguments passed to the application.
+ * @returns {Promise} The parsed {@link commands/builders!out out} option.
+ * @typeParam O An object extending a relevant subset of the {@link commands/builders!commonOptions commonOptions} object.
+ * @throws {@link typescript!Error Error} when the `out` option could not be found.
+ */
+export async function getOut(
+ args: Args
+): Promise {
+ if (args.out) return args.out;
+ else if (isOptionsArgs(args)) return (await getOptions(args))?.out;
+ else throw new Error(`Could not parse 'out'`);
+}
+
+/**
+ * Type guard for determining whether a given object is or extends {@link commands/builders!commonOptions commonOptions}.
+ * @param {unknown} obj The object.
+ * @returns {obj is Args} Whether the object is or extends {@link commands/builders!commonOptions commonOptions}.
+ */
+export const isOptionsArgs = (
+ obj: unknown
+): obj is Args =>
+ obj !== null &&
+ typeof obj === 'object' &&
+ 'typedoc' in obj &&
+ 'tsconfig' in obj;
+
+/**
+ * Attempts to find a tsconfig file at a given path.
+ * @param {string} [path=cwd()] The path to the tsconfig or a directory containing it.
+ * Defaults to {@link node:process!cwd process.cwd()}.
+ * @returns {string} The fully resolved path to the tsconfig file if found.
+ * @throws {@link typescript!Error Error} if the path does not exist or tsconfig file is invalid or not found.
+ */
+export function findTsConfigFile(path: string = cwd()): string {
+ path = resolve(path);
+
+ if (!pathExistsSync(path))
+ throw new Error(`Path does not exist: ${relative(cwd(), path)}`);
+
+ const file = findConfigFile(path, sys.fileExists);
+
+ if (!file)
+ throw new Error(
+ `Cannot find a valid tsconfig at path: ${relative(cwd(), path)}`
+ );
+
+ return resolve(file);
+}
+
+/**
+ * Attempts to find a file at a given path. When a directory is passed as the first argument `path`, uses that directory
+ * as a search path and searches for files passed in the second argument `filePaths`, resolving and returning the first hit.
+ * @param {string} [path=cwd()] The path to the file or a directory containing it. When a directory is passed,
+ * `filePaths` should contain at least one file path relative to the directory.
+ * Defaults to {@link node:process!cwd process.cwd()}.
+ * @param {readonly string[]} [filePaths=[]] An array of file paths to search for when `path` is a directory.
+ * @returns {Promise} The fully resolved path to the file if found.
+ * @throws {@link typescript!Error Error} if the path does not exist or the file was not found.
+ */
+export async function findFile(
+ path: string = cwd(),
+ filePaths: readonly string[] = []
+): Promise {
+ path = resolve(path);
+
+ if (!(await pathExists(path)))
+ throw new Error(`Path does not exist: ${relative(cwd(), path)}`);
+
+ const file = await find(
+ [path, ...filePaths.map((p) => join(path, p))],
+ isFile
+ );
+
+ if (!file && path === cwd() && filePaths.length === 0)
+ throw new Error(
+ 'filePaths must contain at least one file path when path is a directory!'
+ );
+ else if (!file) throw new Error(`A matching file could not be found.`);
+
+ return resolve(file);
+}
+
+/**
+ * Determines whether a given path is a file.
+ * @param {string} path The path.
+ * @returns {Promise} Whether the path is a file.
+ */
+export async function isFile(path: string): Promise {
+ try {
+ return (await stat(path)).isFile();
+ } catch {
+ return false;
+ }
+}
+
+/**
+ * Determines whether a given path is a directory.
+ * @param {string} path The path.
+ * @returns {Promise} Whether the path is a directory.
+ */
+export async function isDir(path: string): Promise {
+ try {
+ return (await stat(path)).isDirectory();
+ } catch {
+ return false;
+ }
+}
+
+/**
+ * Determines whether a given path is a symbolic link.
+ * @param {string} path The path.
+ * @returns {Promise} Whether the path is a symbolic link.
+ */
+export async function isSymlink(path: string): Promise {
+ try {
+ return (await lstat(path)).isSymbolicLink();
+ } catch {
+ return false;
+ }
+}
+
+/**
+ * Determines whether a given path is a symbolic link which points to a path which does not exist.
+ * @param {string} path The path.
+ * @returns {Promise} Whether the path is a broken symbolic link.
+ */
+export const isBrokenSymlink = async (path: string): Promise =>
+ (await isSymlink(path)) && !(await pathExists(await readlink(path)));
+
+/**
+ * Removes all broken symbolic links within a directory.
+ * @param {string} dir The path to the directory.
+ */
+export async function unlinkBrokenSymlinks(dir: string): Promise {
+ const paths = (await readdir(dir)).map((path) => join(dir, path));
+ const brokenSymLinks = await filter(paths, isBrokenSymlink);
+ await each(brokenSymLinks, unlink);
+}
+
+/**
+ * Removes elements from an array that meet the condition specified in a callback function, and returns the elements which were removed.
+ * @remarks Changes the contents of the array by removing existing elements {@link https://en.wikipedia.org/wiki/In-place_algorithm in place}.
+ * To retrieve a new array with the elements removed without modifying the original, see {@link exclude}.
+ * @param {T[]} array The array.
+ * @param {(T) => boolean} predicate A function which accepts a `typeof T` argument and returns a `boolean`. The drop function calls the
+ * predicate function one time for each element in the array, and removes those elements for which the predicate returns `true`.
+ * @returns {T[]} The elements which were removed from the array.
+ * @typeParam T The type of the elements in the array.
+ * @see {@link exclude}
+ */
+export function drop(array: T[], predicate: (value: T) => boolean): T[];
+/**
+ * Removes elements from an array which are found in another array.
+ * @remarks Changes the contents of the array by removing existing elements {@link https://en.wikipedia.org/wiki/In-place_algorithm in place}.
+ * To retrieve a new array with the elements removed without modifying the original, see {@link exclude}.
+ * @param {T[]} array The array.
+ * @param {readonly unknown[]} elements An array of elements to remove from the array.
+ * @returns {T[]} The elements which were removed from the array.
+ * @typeParam T The type of the elements in the array.
+ * @typeParam U The type of the elements in the array of elements to remove.
+ * @see {@link exclude}
+ */
+export function drop(array: T[], elements: readonly U[]): T[];
+export function drop(
+ array: T[],
+ elementsOrPredicate: readonly U[] | ((value: T) => boolean)
+): T[] {
+ const toBeDropped =
+ typeof elementsOrPredicate === 'function'
+ ? array.filter(elementsOrPredicate)
+ : array.filter((v) => elementsOrPredicate.includes(v));
+
+ const dropped = [];
+ for (const value of toBeDropped) {
+ const i = array.indexOf(value);
+ if (i >= 0) {
+ dropped.push(...array.splice(i, 1));
+ }
+ }
+ return dropped;
+}
+
+/**
+ * Creates and returns a {@link https://developer.mozilla.org/en-US/docs/Glossary/Shallow_copy shallow copy} of the given array,
+ * with the elements that meet the condition specified in the provided callback function excluded.
+ * @remarks For the equivalent function which acts on the array {@link https://en.wikipedia.org/wiki/In-place_algorithm in place}, see {@link drop}.
+ * @param {readonly T[]} array The array.
+ * @param {(T) => boolean} predicate A function which accepts a `typeof T` argument and returns a `boolean`. The exclude function calls the
+ * predicate function one time for each element in the array, and excludes those elements for which the predicate returns `true` from the returned array.
+ * @returns {T[]} A {@link https://developer.mozilla.org/en-US/docs/Glossary/Shallow_copy shallow copy} of the array, with the elements for which the predicate
+ * function returns `true` excluded.
+ * @typeParam T The type of the elements in the array.
+ * @see {@link drop}
+ */
+export function exclude(
+ array: readonly T[],
+ predicate: (value: T) => boolean
+): T[];
+/**
+ * Creates and returns a {@link https://developer.mozilla.org/en-US/docs/Glossary/Shallow_copy shallow copy} of the given array,
+ * with the elements from a second array excluded.
+ * @remarks For the equivalent function which acts on the array {@link https://en.wikipedia.org/wiki/In-place_algorithm in place}, see {@link drop}.
+ * @param {readonly T[]} array The array.
+ * @param {readonly U[]} elements An array of elements to exclude from the returned array.
+ * @returns {T[]} A {@link https://developer.mozilla.org/en-US/docs/Glossary/Shallow_copy shallow copy} of the array, with the elements which were found in the
+ * `elements` array excluded.
+ * @typeParam T The type of the elements in the array.
+ * @typeParam U The type of the elements in the array of elements to exclude.
+ * @see {@link drop}
+ */
+export function exclude(
+ array: readonly T[],
+ elements: readonly U[]
+): T[];
+export function exclude(
+ array: readonly T[],
+ elementsOrPredicate: readonly U[] | ((value: T) => boolean)
+): T[] {
+ return array.filter((v) =>
+ typeof elementsOrPredicate === 'function'
+ ? !elementsOrPredicate(v)
+ : !elementsOrPredicate.includes(v)
+ );
+}
+
+/**
+ * Removes surrounding whitespace and enclosing quotation marks from all strings of an array.
+ * @remarks Intended for use when handling string arrays passed by {@link http://yargs.js.org/ yargs} from the command line.
+ * @param {string[]} array The array of strings.
+ * @returns {string[]} The coerced array of strings.
+ */
+export const coerceStringArray = (array: string[]): string[] =>
+ array.map((value) => {
+ value = value.trim();
+ return (value.startsWith('"') && value.endsWith('"')) ||
+ (value.startsWith("'") && value.endsWith("'")) ||
+ (value.startsWith('`') && value.endsWith('`'))
+ ? [...value].slice(1, -1).join('')
+ : value;
+ });
diff --git a/test/commands/purge.spec/coercePurgeVersionsNum.spec.ts b/test/commands/purge.spec/coercePurgeVersionsNum.spec.ts
new file mode 100644
index 0000000..04138ea
--- /dev/null
+++ b/test/commands/purge.spec/coercePurgeVersionsNum.spec.ts
@@ -0,0 +1,40 @@
+import { coercePurgeVersionsNum } from '../../../src/commands/purge';
+
+describe('when passing a number', () => {
+ describe('when number is valid', () => {
+ test('should return the same number', () => {
+ expect(coercePurgeVersionsNum(5)).toBe(5);
+ expect(coercePurgeVersionsNum(0)).toBe(0);
+ expect(coercePurgeVersionsNum(100.25)).toBe(100.25);
+ });
+ });
+
+ describe('when number is invalid', () => {
+ test('should return Infinity', () => {
+ expect(coercePurgeVersionsNum(-1)).toBe(Infinity);
+ expect(coercePurgeVersionsNum(-0.00001)).toBe(Infinity);
+ expect(coercePurgeVersionsNum(Infinity)).toBe(Infinity);
+ expect(coercePurgeVersionsNum(-Infinity)).toBe(Infinity);
+ expect(coercePurgeVersionsNum(NaN)).toBe(Infinity);
+ });
+ });
+});
+
+describe('when passing an array of numbers', () => {
+ test('should only return the last number in the array, coerced', () => {
+ expect(coercePurgeVersionsNum([5])).toBe(5);
+ expect(coercePurgeVersionsNum([4, 5])).toBe(5);
+ expect(coercePurgeVersionsNum([Infinity, 5])).toBe(5);
+ expect(coercePurgeVersionsNum([NaN, 5])).toBe(5);
+ expect(coercePurgeVersionsNum([3, 5, NaN])).toBe(Infinity);
+ expect(coercePurgeVersionsNum([3, 5, Infinity])).toBe(Infinity);
+ expect(coercePurgeVersionsNum([3, 5, -Infinity])).toBe(Infinity);
+ expect(coercePurgeVersionsNum([3, 5, -1])).toBe(Infinity);
+ });
+});
+
+describe('when passing an empty array', () => {
+ test('should return Infinity', () => {
+ expect(coercePurgeVersionsNum([])).toBe(Infinity);
+ });
+});
diff --git a/test/commands/purge.spec/getMajorVersionsToPurge.spec.ts b/test/commands/purge.spec/getMajorVersionsToPurge.spec.ts
new file mode 100644
index 0000000..b34f50b
--- /dev/null
+++ b/test/commands/purge.spec/getMajorVersionsToPurge.spec.ts
@@ -0,0 +1,60 @@
+import { getMajorVersionsToPurge } from '../../../src/commands/purge';
+
+describe('when versions = []', () => {
+ test('should return []', () => {
+ expect(getMajorVersionsToPurge([], 0)).toEqual([]);
+ expect(getMajorVersionsToPurge([], 5)).toEqual([]);
+ });
+});
+
+describe('when versions = [ "v2.0.0", "v1.0.0", "v0.1.0" ]', () => {
+ const versions = ['v2.0.0', 'v1.0.0', 'v0.1.0'] as const;
+
+ describe('when number = 0', () => {
+ const number = 0;
+
+ test('should return [ "v2.0.0", "v1.0.0", "v0.1.0" ]', () => {
+ expect(
+ getMajorVersionsToPurge(versions, number, {
+ includePrerelease: true,
+ })
+ ).toEqual(['v2.0.0', 'v1.0.0', 'v0.1.0']);
+ });
+ });
+
+ describe('when number = 1', () => {
+ const number = 1;
+
+ test('should return [ "v1.0.0", "v0.1.0" ]', () => {
+ expect(
+ getMajorVersionsToPurge(versions, number, {
+ includePrerelease: true,
+ })
+ ).toEqual(['v1.0.0', 'v0.1.0']);
+ });
+ });
+
+ describe('when number = 2', () => {
+ const number = 2;
+
+ test('should return [ "v0.1.0" ]', () => {
+ expect(
+ getMajorVersionsToPurge(versions, number, {
+ includePrerelease: true,
+ })
+ ).toEqual(['v0.1.0']);
+ });
+ });
+
+ describe('when number >= 3', () => {
+ const number = 3;
+
+ test('should return []', () => {
+ expect(
+ getMajorVersionsToPurge(versions, number, {
+ includePrerelease: true,
+ })
+ ).toEqual([]);
+ });
+ });
+});
diff --git a/test/commands/purge.spec/getMinorVersionsToPurge.spec.ts b/test/commands/purge.spec/getMinorVersionsToPurge.spec.ts
new file mode 100644
index 0000000..8a7f932
--- /dev/null
+++ b/test/commands/purge.spec/getMinorVersionsToPurge.spec.ts
@@ -0,0 +1,74 @@
+import { getMinorVersionsToPurge } from '../../../src/commands/purge';
+
+describe('when versions = []', () => {
+ test('should return []', () => {
+ expect(getMinorVersionsToPurge([], 0)).toEqual([]);
+ expect(getMinorVersionsToPurge([], 5)).toEqual([]);
+ });
+});
+
+describe('when versions = [ "v2.1.0", "v2.0.0", "v1.2.0", "v1.1.0", "v1.0.0", "v0.1.0" ]', () => {
+ const versions = [
+ 'v2.1.0',
+ 'v2.0.0',
+ 'v1.2.0',
+ 'v1.1.0',
+ 'v1.0.0',
+ 'v0.1.0',
+ ] as const;
+
+ describe('when number = 0', () => {
+ const number = 0;
+
+ test('should return [ "v2.1.0", "v2.0.0", "v1.2.0", "v1.1.0", "v1.0.0", "v0.1.0" ]', () => {
+ expect(
+ getMinorVersionsToPurge(versions, number, {
+ includePrerelease: true,
+ })
+ ).toEqual([
+ 'v2.1.0',
+ 'v2.0.0',
+ 'v1.2.0',
+ 'v1.1.0',
+ 'v1.0.0',
+ 'v0.1.0',
+ ]);
+ });
+ });
+
+ describe('when number = 1', () => {
+ const number = 1;
+
+ test('should return [ "v2.0.0", "v1.1.0", "v1.0.0" ]', () => {
+ expect(
+ getMinorVersionsToPurge(versions, number, {
+ includePrerelease: true,
+ })
+ ).toEqual(['v2.0.0', 'v1.1.0', 'v1.0.0']);
+ });
+ });
+
+ describe('when number = 2', () => {
+ const number = 2;
+
+ test('should return [ "v1.0.0" ]', () => {
+ expect(
+ getMinorVersionsToPurge(versions, number, {
+ includePrerelease: true,
+ })
+ ).toEqual(['v1.0.0']);
+ });
+ });
+
+ describe('when number >= 3', () => {
+ const number = 3;
+
+ test('should return []', () => {
+ expect(
+ getMinorVersionsToPurge(versions, number, {
+ includePrerelease: true,
+ })
+ ).toEqual([]);
+ });
+ });
+});
diff --git a/test/commands/purge.spec/getPatchVersionsToPurge.spec.ts b/test/commands/purge.spec/getPatchVersionsToPurge.spec.ts
new file mode 100644
index 0000000..d8fe226
--- /dev/null
+++ b/test/commands/purge.spec/getPatchVersionsToPurge.spec.ts
@@ -0,0 +1,80 @@
+import { getPatchVersionsToPurge } from '../../../src/commands/purge';
+
+describe('when versions = []', () => {
+ test('should return []', () => {
+ expect(getPatchVersionsToPurge([], 0)).toEqual([]);
+ expect(getPatchVersionsToPurge([], 5)).toEqual([]);
+ });
+});
+
+describe('when versions = [ "v2.1.0", "v2.0.1", "v2.0.0", "v1.2.2", "v1.2.1", "v1.2.0", "v1.1.0", "v1.0.0", "v0.1.0" ]', () => {
+ const versions = [
+ 'v2.1.0',
+ 'v2.0.1',
+ 'v2.0.0',
+ 'v1.2.2',
+ 'v1.2.1',
+ 'v1.2.0',
+ 'v1.1.0',
+ 'v1.0.0',
+ 'v0.1.0',
+ ] as const;
+
+ describe('when number = 0', () => {
+ const number = 0;
+
+ test('should return [ "v2.1.0", "v2.0.1", "v2.0.0", "v1.2.2", "v1.2.1", "v1.2.0", "v1.1.0", "v1.0.0", "v0.1.0" ]', () => {
+ expect(
+ getPatchVersionsToPurge(versions, number, {
+ includePrerelease: true,
+ })
+ ).toEqual([
+ 'v2.1.0',
+ 'v2.0.1',
+ 'v2.0.0',
+ 'v1.2.2',
+ 'v1.2.1',
+ 'v1.2.0',
+ 'v1.1.0',
+ 'v1.0.0',
+ 'v0.1.0',
+ ]);
+ });
+ });
+
+ describe('when number = 1', () => {
+ const number = 1;
+
+ test('should return [ "v2.0.0", "v1.2.1", "v1.2.0" ]', () => {
+ expect(
+ getPatchVersionsToPurge(versions, number, {
+ includePrerelease: true,
+ })
+ ).toEqual(['v2.0.0', 'v1.2.1', 'v1.2.0']);
+ });
+ });
+
+ describe('when number = 2', () => {
+ const number = 2;
+
+ test('should return [ "v1.2.0" ]', () => {
+ expect(
+ getPatchVersionsToPurge(versions, number, {
+ includePrerelease: true,
+ })
+ ).toEqual(['v1.2.0']);
+ });
+ });
+
+ describe('when number >= 3', () => {
+ const number = 3;
+
+ test('should return []', () => {
+ expect(
+ getPatchVersionsToPurge(versions, number, {
+ includePrerelease: true,
+ })
+ ).toEqual([]);
+ });
+ });
+});
diff --git a/test/commands/purge.spec/getStaleVersionsToPurge.spec.ts b/test/commands/purge.spec/getStaleVersionsToPurge.spec.ts
new file mode 100644
index 0000000..40e863f
--- /dev/null
+++ b/test/commands/purge.spec/getStaleVersionsToPurge.spec.ts
@@ -0,0 +1,53 @@
+import { join, resolve } from 'node:path';
+import { each } from 'async';
+import { ensureDir, rm } from 'fs-extra';
+import { version } from 'typedoc-plugin-versions';
+
+import { getStaleVersionsToPurge } from '../../../src/commands/purge';
+
+const out = 'test/.commands.purge.getStaleVersionsToPurge';
+const options = {
+ out,
+ versions: { stable: 'auto', dev: 'auto', domLocation: 'false' },
+} as const;
+let versions: version[];
+
+beforeAll(() => ensureDir(resolve(out)));
+afterAll(() => rm(resolve(out), { recursive: true, force: true }));
+
+beforeEach(() => each(versions, async (v) => ensureDir(resolve(join(out, v)))));
+
+describe('when versions = []', () => {
+ beforeAll(() => (versions = []));
+
+ test('should return []', async () => {
+ expect(await getStaleVersionsToPurge(versions, options)).toEqual([]);
+ });
+});
+
+describe('when versions = [ "v0.1.0" ]', () => {
+ beforeAll(() => (versions = ['v0.1.0']));
+
+ test('should return []', async () => {
+ expect(await getStaleVersionsToPurge(versions, options)).toEqual([]);
+ });
+});
+
+describe('when versions = [ "v1.0.0-alpha.1", "v0.1.0" ]', () => {
+ beforeAll(() => (versions = ['v1.0.0-alpha.1', 'v0.1.0']));
+
+ test('should return []', async () => {
+ expect(await getStaleVersionsToPurge(versions, options)).toEqual([]);
+ });
+});
+
+describe('when versions = [ "v1.0.0", "v1.0.0-alpha.1", "v0.1.0" ]', () => {
+ beforeAll(() => (versions = ['v1.0.0', 'v1.0.0-alpha.1', 'v0.1.0']));
+
+ test('should return [ "v1.0.0-alpha.1", "v0.1.0" ]', async () => {
+ expect(await getStaleVersionsToPurge(versions, options)).toEqual([
+ 'v1.0.0-alpha.1',
+ 'v0.1.0',
+ ]);
+ });
+});
diff --git a/test/commands/purge.spec/getVersionsToPurge.spec.ts b/test/commands/purge.spec/getVersionsToPurge.spec.ts
new file mode 100644
index 0000000..7f6cf9d
--- /dev/null
+++ b/test/commands/purge.spec/getVersionsToPurge.spec.ts
@@ -0,0 +1,34 @@
+import { getVersionsToPurge } from '../../../src/commands/purge';
+
+describe('when versionsToPurge is empty', () => {
+ test('should return []', () => {
+ expect(getVersionsToPurge([])).toEqual([]);
+ expect(getVersionsToPurge(['v1.0.0'])).toEqual([]);
+ expect(getVersionsToPurge(['v1.0.0', 'v2.0.0'])).toEqual([]);
+ });
+});
+
+describe('when versionsToPurge contains no matches', () => {
+ test('should return []', () => {
+ expect(getVersionsToPurge([], ['v1.0.0'])).toEqual([]);
+ expect(
+ getVersionsToPurge(['v1.0.0'], ['v0.1.0'], {
+ includePrerelease: true,
+ })
+ ).toEqual([]);
+ expect(
+ getVersionsToPurge(['v1.0.0', 'v2.0.0'], ['v0.1.0', 'v1.1.0'], {
+ includePrerelease: true,
+ })
+ ).toEqual([]);
+ });
+});
+
+describe('when versionsToPurge contains matches', () => {
+ test('should return the matched versions', () => {
+ expect(getVersionsToPurge(['v1.0.0'], ['v1.0.0'])).toEqual(['v1.0.0']);
+ expect(getVersionsToPurge(['v1.0.0', 'v2.0.0'], ['>=1.5'])).toEqual([
+ 'v2.0.0',
+ ]);
+ });
+});
diff --git a/test/commands/purge.spec/handler.spec.ts b/test/commands/purge.spec/handler.spec.ts
new file mode 100644
index 0000000..5e30659
--- /dev/null
+++ b/test/commands/purge.spec/handler.spec.ts
@@ -0,0 +1,321 @@
+import { join, resolve } from 'node:path';
+import { each } from 'async';
+import { ensureDir, pathExists, rm, stat } from 'fs-extra';
+import { inject } from 'prompts';
+import { version } from 'typedoc-plugin-versions';
+
+import { cli, exclude } from '../../../src';
+
+const out = 'test/.commands.purge.handler';
+let versions: version[];
+let consoleLogMock: jest.SpyInstance;
+
+beforeAll(() => ensureDir(resolve(out)));
+afterAll(() => rm(resolve(out), { recursive: true, force: true }));
+
+describe('when `out` points to empty directory', () => {
+ beforeEach(() => {
+ versions = [];
+ consoleLogMock = jest.spyOn(console, 'log').mockImplementation();
+ });
+ afterEach(() => consoleLogMock.mockRestore());
+
+ test('should log: Nothing to purge!', async () => {
+ await cli().parse(`purge --out ${out}`);
+ expect(console.log).toHaveBeenCalledTimes(1);
+ expect(console.log).toHaveBeenCalledWith('Nothing to purge!');
+ });
+});
+
+describe('when versions = [ "v2.1.0", "v2.0.1", "v2.0.0", "v2.0.0-alpha.1", "v1.2.2", "v1.2.1", "v1.2.0", "v1.1.0", "v1.0.0", "v0.1.0" ]', () => {
+ beforeEach(async () => {
+ versions = [
+ 'v2.1.0',
+ 'v2.0.1',
+ 'v2.0.0',
+ 'v2.0.0-alpha.1',
+ 'v1.2.2',
+ 'v1.2.1',
+ 'v1.2.0',
+ 'v1.1.0',
+ 'v1.0.0',
+ 'v0.1.0',
+ ];
+
+ await each(
+ versions,
+ async (v) => await ensureDir(resolve(join(out, v)))
+ );
+ });
+
+ describe('when user chooses "no"', () => {
+ beforeEach(
+ () =>
+ (consoleLogMock = jest
+ .spyOn(console, 'log')
+ .mockImplementation())
+ );
+ afterEach(() => consoleLogMock.mockRestore());
+
+ test('should exit without making changes', async () => {
+ inject(['']);
+ await cli().parse(`purge --out ${out}`);
+ expect(console.log).toHaveBeenCalledTimes(1);
+
+ await each(versions, async (v) =>
+ expect(
+ (await pathExists(resolve(join(out, v)))) &&
+ (await stat(resolve(join(out, v)))).isDirectory()
+ ).toBe(true)
+ );
+ });
+ });
+
+ describe('when user chooses "yes"', () => {
+ beforeEach(
+ () =>
+ (consoleLogMock = jest
+ .spyOn(console, 'log')
+ .mockImplementation())
+ );
+ afterEach(() => consoleLogMock.mockRestore());
+
+ test('should purge stale versions', async () => {
+ inject(['yes']);
+ await cli().parse(`purge --out ${out}`);
+ expect(console.log).toHaveBeenCalledTimes(2);
+
+ const stale = ['v2.0.0-alpha.1', 'v0.1.0'];
+
+ await each(exclude(versions, stale), async (v) =>
+ expect(
+ (await pathExists(resolve(join(out, v)))) &&
+ (await stat(resolve(join(out, v)))).isDirectory()
+ ).toBe(true)
+ );
+
+ await each(stale, async (v) =>
+ expect(
+ (await pathExists(resolve(join(out, v)))) &&
+ (await stat(resolve(join(out, v)))).isDirectory()
+ ).toBe(false)
+ );
+ });
+ });
+
+ describe('when user passes -y', () => {
+ beforeEach(
+ () =>
+ (consoleLogMock = jest
+ .spyOn(console, 'log')
+ .mockImplementation())
+ );
+ afterEach(() => consoleLogMock.mockRestore());
+
+ test('should purge stale versions', async () => {
+ await cli().parse(`purge --out ${out} -y`);
+ expect(console.log).toHaveBeenCalledTimes(1);
+
+ const stale = ['v2.0.0-alpha.1', 'v0.1.0'];
+
+ await each(exclude(versions, stale), async (v) =>
+ expect(
+ (await pathExists(resolve(join(out, v)))) &&
+ (await stat(resolve(join(out, v)))).isDirectory()
+ ).toBe(true)
+ );
+
+ await each(stale, async (v) =>
+ expect(
+ (await pathExists(resolve(join(out, v)))) &&
+ (await stat(resolve(join(out, v)))).isDirectory()
+ ).toBe(false)
+ );
+ });
+ });
+
+ describe('when user passes -y --exclude ">=2.0.0"', () => {
+ beforeEach(
+ () =>
+ (consoleLogMock = jest
+ .spyOn(console, 'log')
+ .mockImplementation())
+ );
+ afterEach(() => consoleLogMock.mockRestore());
+
+ test('should purge stale versions', async () => {
+ await cli().parse(`purge --out ${out} -y --exclude ">=2.0.0"`);
+ expect(console.log).toHaveBeenCalledTimes(1);
+
+ const stale = ['v0.1.0'];
+
+ await each(exclude(versions, stale), async (v) =>
+ expect(
+ (await pathExists(resolve(join(out, v)))) &&
+ (await stat(resolve(join(out, v)))).isDirectory()
+ ).toBe(true)
+ );
+
+ await each(stale, async (v) =>
+ expect(
+ (await pathExists(resolve(join(out, v)))) &&
+ (await stat(resolve(join(out, v)))).isDirectory()
+ ).toBe(false)
+ );
+ });
+ });
+
+ describe('when user passes --no-stale', () => {
+ beforeEach(
+ () =>
+ (consoleLogMock = jest
+ .spyOn(console, 'log')
+ .mockImplementation())
+ );
+ afterEach(() => consoleLogMock.mockRestore());
+
+ test('should log: Nothing to purge!', async () => {
+ await cli().parse(`purge --out ${out} --no-stale`);
+ expect(console.log).toHaveBeenCalledTimes(1);
+ expect(console.log).toHaveBeenCalledWith('Nothing to purge!');
+
+ consoleLogMock.mockReset();
+ await cli().parse(`purge --out ${out} --stale false`);
+ expect(console.log).toHaveBeenCalledTimes(1);
+ expect(console.log).toHaveBeenCalledWith('Nothing to purge!');
+
+ consoleLogMock.mockReset();
+ await cli().parse(`purge --out ${out} --stale=false`);
+ expect(console.log).toHaveBeenCalledTimes(1);
+ expect(console.log).toHaveBeenCalledWith('Nothing to purge!');
+ });
+ });
+
+ describe('when user passes 2.0.1 -y', () => {
+ beforeEach(
+ () =>
+ (consoleLogMock = jest
+ .spyOn(console, 'log')
+ .mockImplementation())
+ );
+ afterEach(() => consoleLogMock.mockRestore());
+
+ test('should purge stale versions & 2.0.1', async () => {
+ await cli().parse(`purge --out ${out} 2.0.1 -y`);
+ expect(console.log).toHaveBeenCalledTimes(1);
+
+ const stale = ['v2.0.0-alpha.1', 'v0.1.0'];
+ const purge = ['v2.0.1'];
+
+ await each(exclude(versions, [...purge, ...stale]), async (v) =>
+ expect(
+ (await pathExists(resolve(join(out, v)))) &&
+ (await stat(resolve(join(out, v)))).isDirectory()
+ ).toBe(true)
+ );
+
+ await each([...purge, ...stale], async (v) =>
+ expect(
+ (await pathExists(resolve(join(out, v)))) &&
+ (await stat(resolve(join(out, v)))).isDirectory()
+ ).toBe(false)
+ );
+ });
+ });
+
+ describe('when user passes --major 1 -y', () => {
+ beforeEach(
+ () =>
+ (consoleLogMock = jest
+ .spyOn(console, 'log')
+ .mockImplementation())
+ );
+ afterEach(() => consoleLogMock.mockRestore());
+
+ test('should purge stale versions & all but the last major version', async () => {
+ await cli().parse(`purge --out ${out} --major 1 -y`);
+ expect(console.log).toHaveBeenCalledTimes(1);
+
+ const stale = ['v2.0.0-alpha.1', 'v0.1.0'];
+ const major = ['v1.2.2', 'v1.2.1', 'v1.2.0', 'v1.1.0', 'v1.0.0'];
+
+ await each(exclude(versions, [...major, ...stale]), async (v) =>
+ expect(
+ (await pathExists(resolve(join(out, v)))) &&
+ (await stat(resolve(join(out, v)))).isDirectory()
+ ).toBe(true)
+ );
+
+ await each([...major, ...stale], async (v) =>
+ expect(
+ (await pathExists(resolve(join(out, v)))) &&
+ (await stat(resolve(join(out, v)))).isDirectory()
+ ).toBe(false)
+ );
+ });
+ });
+
+ describe('when user passes --minor 1 -y', () => {
+ beforeEach(
+ () =>
+ (consoleLogMock = jest
+ .spyOn(console, 'log')
+ .mockImplementation())
+ );
+ afterEach(() => consoleLogMock.mockRestore());
+
+ test('should purge stale versions & all but the last minor version per major version', async () => {
+ await cli().parse(`purge --out ${out} --minor 1 -y`);
+ expect(console.log).toHaveBeenCalledTimes(1);
+
+ const stale = ['v2.0.0-alpha.1', 'v0.1.0'];
+ const minor = ['v2.0.1', 'v2.0.0', 'v1.1.0', 'v1.0.0'];
+
+ await each(exclude(versions, [...minor, ...stale]), async (v) =>
+ expect(
+ (await pathExists(resolve(join(out, v)))) &&
+ (await stat(resolve(join(out, v)))).isDirectory()
+ ).toBe(true)
+ );
+
+ await each([...minor, ...stale], async (v) =>
+ expect(
+ (await pathExists(resolve(join(out, v)))) &&
+ (await stat(resolve(join(out, v)))).isDirectory()
+ ).toBe(false)
+ );
+ });
+ });
+
+ describe('when user passes --patch 1 -y', () => {
+ beforeEach(
+ () =>
+ (consoleLogMock = jest
+ .spyOn(console, 'log')
+ .mockImplementation())
+ );
+ afterEach(() => consoleLogMock.mockRestore());
+
+ test('should purge stale versions & all but the last patch version per minor version', async () => {
+ await cli().parse(`purge --out ${out} --patch 1 -y`);
+ expect(console.log).toHaveBeenCalledTimes(1);
+
+ const stale = ['v2.0.0-alpha.1', 'v0.1.0'];
+ const patch = ['v2.0.0', 'v1.2.1', 'v1.2.0'];
+
+ await each(exclude(versions, [...patch, ...stale]), async (v) =>
+ expect(
+ (await pathExists(resolve(join(out, v)))) &&
+ (await stat(resolve(join(out, v)))).isDirectory()
+ ).toBe(true)
+ );
+
+ await each([...patch, ...stale], async (v) =>
+ expect(
+ (await pathExists(resolve(join(out, v)))) &&
+ (await stat(resolve(join(out, v)))).isDirectory()
+ ).toBe(false)
+ );
+ });
+ });
+});
diff --git a/test/commands/purge.spec/isDev.spec.ts b/test/commands/purge.spec/isDev.spec.ts
new file mode 100644
index 0000000..fff4989
--- /dev/null
+++ b/test/commands/purge.spec/isDev.spec.ts
@@ -0,0 +1,73 @@
+import { isDev } from '../../../src/commands/purge';
+
+describe('when version = "v1.0.0"', () => {
+ const version = 'v1.0.0';
+
+ describe('when dev = "v1.0.0"', () => {
+ const dev = 'v1.0.0';
+
+ test('should return true', () => {
+ expect(isDev(version, dev)).toBe(true);
+ });
+ });
+
+ describe('when dev = "v2.0.0"', () => {
+ const dev = 'v2.0.0';
+
+ test('should return false', () => {
+ expect(isDev(version, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "v0.1.0"', () => {
+ const dev = 'v0.1.0';
+
+ test('should return false', () => {
+ expect(isDev(version, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "auto"', () => {
+ const dev = 'auto';
+
+ test('should return false', () => {
+ expect(isDev(version, dev)).toBe(false);
+ });
+ });
+});
+
+describe('when version = "v0.1.0"', () => {
+ const version = 'v0.1.0';
+
+ describe('when dev = "v1.0.0"', () => {
+ const dev = 'v1.0.0';
+
+ test('should return true', () => {
+ expect(isDev(version, dev)).toBe(true);
+ });
+ });
+
+ describe('when dev = "v2.0.0"', () => {
+ const dev = 'v2.0.0';
+
+ test('should return true', () => {
+ expect(isDev(version, dev)).toBe(true);
+ });
+ });
+
+ describe('when dev = "v0.1.0"', () => {
+ const dev = 'v0.1.0';
+
+ test('should return true', () => {
+ expect(isDev(version, dev)).toBe(true);
+ });
+ });
+
+ describe('when dev = "auto"', () => {
+ const dev = 'auto';
+
+ test('should return true', () => {
+ expect(isDev(version, dev)).toBe(true);
+ });
+ });
+});
diff --git a/test/commands/purge.spec/isPinned.spec.ts b/test/commands/purge.spec/isPinned.spec.ts
new file mode 100644
index 0000000..b4e4ee0
--- /dev/null
+++ b/test/commands/purge.spec/isPinned.spec.ts
@@ -0,0 +1,297 @@
+import { isPinned } from '../../../src/commands/purge';
+
+describe('when version = "v1.0.0"', () => {
+ const version = 'v1.0.0';
+
+ describe('when stable = "v1.0.0"', () => {
+ const stable = 'v1.0.0';
+
+ describe('when dev = "v1.0.0"', () => {
+ const dev = 'v1.0.0';
+
+ test('should return true', () => {
+ expect(isPinned(version, stable, dev)).toBe(true);
+ });
+ });
+
+ describe('when dev = "v2.0.0"', () => {
+ const dev = 'v2.0.0';
+
+ test('should return true', () => {
+ expect(isPinned(version, stable, dev)).toBe(true);
+ });
+ });
+
+ describe('when dev = "v0.1.0"', () => {
+ const dev = 'v0.1.0';
+
+ test('should return true', () => {
+ expect(isPinned(version, stable, dev)).toBe(true);
+ });
+ });
+
+ describe('when dev = "auto"', () => {
+ const dev = 'auto';
+
+ test('should return true', () => {
+ expect(isPinned(version, stable, dev)).toBe(true);
+ });
+ });
+ });
+
+ describe('when stable = "v2.0.0"', () => {
+ const stable = 'v2.0.0';
+
+ describe('when dev = "v1.0.0"', () => {
+ const dev = 'v1.0.0';
+
+ test('should return true', () => {
+ expect(isPinned(version, stable, dev)).toBe(true);
+ });
+ });
+
+ describe('when dev = "v2.0.0"', () => {
+ const dev = 'v2.0.0';
+
+ test('should return false', () => {
+ expect(isPinned(version, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "v0.1.0"', () => {
+ const dev = 'v0.1.0';
+
+ test('should return false', () => {
+ expect(isPinned(version, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "auto"', () => {
+ const dev = 'auto';
+
+ test('should return false', () => {
+ expect(isPinned(version, stable, dev)).toBe(false);
+ });
+ });
+ });
+
+ describe('when stable = "v0.1.0"', () => {
+ const stable = 'v0.1.0';
+
+ describe('when dev = "v1.0.0"', () => {
+ const dev = 'v1.0.0';
+
+ test('should return true', () => {
+ expect(isPinned(version, stable, dev)).toBe(true);
+ });
+ });
+
+ describe('when dev = "v2.0.0"', () => {
+ const dev = 'v2.0.0';
+
+ test('should return false', () => {
+ expect(isPinned(version, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "v0.1.0"', () => {
+ const dev = 'v0.1.0';
+
+ test('should return false', () => {
+ expect(isPinned(version, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "auto"', () => {
+ const dev = 'auto';
+
+ test('should return false', () => {
+ expect(isPinned(version, stable, dev)).toBe(false);
+ });
+ });
+ });
+
+ describe('when stable = "auto"', () => {
+ const stable = 'auto';
+
+ describe('when dev = "v1.0.0"', () => {
+ const dev = 'v1.0.0';
+
+ test('should return true', () => {
+ expect(isPinned(version, stable, dev)).toBe(true);
+ });
+ });
+
+ describe('when dev = "v2.0.0"', () => {
+ const dev = 'v2.0.0';
+
+ test('should return false', () => {
+ expect(isPinned(version, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "v0.1.0"', () => {
+ const dev = 'v0.1.0';
+
+ test('should return false', () => {
+ expect(isPinned(version, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "auto"', () => {
+ const dev = 'auto';
+
+ test('should return false', () => {
+ expect(isPinned(version, stable, dev)).toBe(false);
+ });
+ });
+ });
+});
+
+describe('when version = "v0.1.0"', () => {
+ const version = 'v0.1.0';
+
+ describe('when stable = "v1.0.0"', () => {
+ const stable = 'v1.0.0';
+
+ describe('when dev = "v1.0.0"', () => {
+ const dev = 'v1.0.0';
+
+ test('should return false', () => {
+ expect(isPinned(version, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "v2.0.0"', () => {
+ const dev = 'v2.0.0';
+
+ test('should return false', () => {
+ expect(isPinned(version, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "v0.1.0"', () => {
+ const dev = 'v0.1.0';
+
+ test('should return true', () => {
+ expect(isPinned(version, stable, dev)).toBe(true);
+ });
+ });
+
+ describe('when dev = "auto"', () => {
+ const dev = 'auto';
+
+ test('should return false', () => {
+ expect(isPinned(version, stable, dev)).toBe(false);
+ });
+ });
+ });
+
+ describe('when stable = "v2.0.0"', () => {
+ const stable = 'v2.0.0';
+
+ describe('when dev = "v1.0.0"', () => {
+ const dev = 'v1.0.0';
+
+ test('should return false', () => {
+ expect(isPinned(version, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "v2.0.0"', () => {
+ const dev = 'v2.0.0';
+
+ test('should return false', () => {
+ expect(isPinned(version, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "v0.1.0"', () => {
+ const dev = 'v0.1.0';
+
+ test('should return true', () => {
+ expect(isPinned(version, stable, dev)).toBe(true);
+ });
+ });
+
+ describe('when dev = "auto"', () => {
+ const dev = 'auto';
+
+ test('should return false', () => {
+ expect(isPinned(version, stable, dev)).toBe(false);
+ });
+ });
+ });
+
+ describe('when stable = "v0.1.0"', () => {
+ const stable = 'v0.1.0';
+
+ describe('when dev = "v1.0.0"', () => {
+ const dev = 'v1.0.0';
+
+ test('should return true', () => {
+ expect(isPinned(version, stable, dev)).toBe(true);
+ });
+ });
+
+ describe('when dev = "v2.0.0"', () => {
+ const dev = 'v2.0.0';
+
+ test('should return true', () => {
+ expect(isPinned(version, stable, dev)).toBe(true);
+ });
+ });
+
+ describe('when dev = "v0.1.0"', () => {
+ const dev = 'v0.1.0';
+
+ test('should return true', () => {
+ expect(isPinned(version, stable, dev)).toBe(true);
+ });
+ });
+
+ describe('when dev = "auto"', () => {
+ const dev = 'auto';
+
+ test('should return true', () => {
+ expect(isPinned(version, stable, dev)).toBe(true);
+ });
+ });
+ });
+
+ describe('when stable = "auto"', () => {
+ const stable = 'auto';
+
+ describe('when dev = "v1.0.0"', () => {
+ const dev = 'v1.0.0';
+
+ test('should return false', () => {
+ expect(isPinned(version, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "v2.0.0"', () => {
+ const dev = 'v2.0.0';
+
+ test('should return false', () => {
+ expect(isPinned(version, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "v0.1.0"', () => {
+ const dev = 'v0.1.0';
+
+ test('should return true', () => {
+ expect(isPinned(version, stable, dev)).toBe(true);
+ });
+ });
+
+ describe('when dev = "auto"', () => {
+ const dev = 'auto';
+
+ test('should return false', () => {
+ expect(isPinned(version, stable, dev)).toBe(false);
+ });
+ });
+ });
+});
diff --git a/test/commands/purge.spec/isStable.spec.ts b/test/commands/purge.spec/isStable.spec.ts
new file mode 100644
index 0000000..4bf59c4
--- /dev/null
+++ b/test/commands/purge.spec/isStable.spec.ts
@@ -0,0 +1,73 @@
+import { isStable } from '../../../src/commands/purge';
+
+describe('when version = "v1.0.0"', () => {
+ const version = 'v1.0.0';
+
+ describe('when stable = "v1.0.0"', () => {
+ const stable = 'v1.0.0';
+
+ test('should return true', () => {
+ expect(isStable(version, stable)).toBe(true);
+ });
+ });
+
+ describe('when stable = "v2.0.0"', () => {
+ const stable = 'v2.0.0';
+
+ test('should return true', () => {
+ expect(isStable(version, stable)).toBe(true);
+ });
+ });
+
+ describe('when stable = "v0.1.0"', () => {
+ const stable = 'v0.1.0';
+
+ test('should return true', () => {
+ expect(isStable(version, stable)).toBe(true);
+ });
+ });
+
+ describe('when stable = "auto"', () => {
+ const stable = 'auto';
+
+ test('should return true', () => {
+ expect(isStable(version, stable)).toBe(true);
+ });
+ });
+});
+
+describe('when version = "v0.1.0"', () => {
+ const version = 'v0.1.0';
+
+ describe('when stable = "v1.0.0"', () => {
+ const stable = 'v1.0.0';
+
+ test('should return false', () => {
+ expect(isStable(version, stable)).toBe(false);
+ });
+ });
+
+ describe('when stable = "v2.0.0"', () => {
+ const stable = 'v2.0.0';
+
+ test('should return false', () => {
+ expect(isStable(version, stable)).toBe(false);
+ });
+ });
+
+ describe('when stable = "v0.1.0"', () => {
+ const stable = 'v0.1.0';
+
+ test('should return true', () => {
+ expect(isStable(version, stable)).toBe(true);
+ });
+ });
+
+ describe('when stable = "auto"', () => {
+ const stable = 'auto';
+
+ test('should return false', () => {
+ expect(isStable(version, stable)).toBe(false);
+ });
+ });
+});
diff --git a/test/commands/purge.spec/isStale.spec.ts b/test/commands/purge.spec/isStale.spec.ts
new file mode 100644
index 0000000..3616bd7
--- /dev/null
+++ b/test/commands/purge.spec/isStale.spec.ts
@@ -0,0 +1,449 @@
+import { isStale } from '../../../src/commands/purge';
+
+describe('when versions = [ "v0.1.0", "v1.0.0-alpha.1", "v1.0.0" ]', () => {
+ const versions = ['v0.1.0', 'v1.0.0-alpha.1', 'v1.0.0'] as const;
+
+ describe('when version = "v0.1.0"', () => {
+ const version = 'v0.1.0';
+
+ describe('when stable = "v0.1.0"', () => {
+ const stable = 'v0.1.0';
+
+ describe('when dev = "v0.1.0"', () => {
+ const dev = 'v0.1.0';
+
+ test('should return false', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "v1.0.0-alpha.1"', () => {
+ const dev = 'v1.0.0-alpha.1';
+
+ test('should return false', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "v1.0.0"', () => {
+ const dev = 'v1.0.0';
+
+ test('should return false', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "auto"', () => {
+ const dev = 'auto';
+
+ test('should return false', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(false);
+ });
+ });
+ });
+
+ describe('when stable = "v1.0.0-alpha.1"', () => {
+ const stable = 'v1.0.0-alpha.1';
+
+ describe('when dev = "v0.1.0"', () => {
+ const dev = 'v0.1.0';
+
+ test('should return false', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "v1.0.0-alpha.1"', () => {
+ const dev = 'v1.0.0-alpha.1';
+
+ test('should return true', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(true);
+ });
+ });
+
+ describe('when dev = "v1.0.0"', () => {
+ const dev = 'v1.0.0';
+
+ test('should return true', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(true);
+ });
+ });
+
+ describe('when dev = "auto"', () => {
+ const dev = 'auto';
+
+ test('should return true', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(true);
+ });
+ });
+ });
+
+ describe('when stable = "v1.0.0"', () => {
+ const stable = 'v1.0.0';
+
+ describe('when dev = "v0.1.0"', () => {
+ const dev = 'v0.1.0';
+
+ test('should return false', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "v1.0.0-alpha.1"', () => {
+ const dev = 'v1.0.0-alpha.1';
+
+ test('should return true', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(true);
+ });
+ });
+
+ describe('when dev = "v1.0.0"', () => {
+ const dev = 'v1.0.0';
+
+ test('should return true', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(true);
+ });
+ });
+
+ describe('when dev = "auto"', () => {
+ const dev = 'auto';
+
+ test('should return true', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(true);
+ });
+ });
+ });
+
+ describe('when stable = "auto"', () => {
+ const stable = 'auto';
+
+ describe('when dev = "v0.1.0"', () => {
+ const dev = 'v0.1.0';
+
+ test('should return false', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "v1.0.0-alpha.1"', () => {
+ const dev = 'v1.0.0-alpha.1';
+
+ test('should return true', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(true);
+ });
+ });
+
+ describe('when dev = "v1.0.0"', () => {
+ const dev = 'v1.0.0';
+
+ test('should return true', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(true);
+ });
+ });
+
+ describe('when dev = "auto"', () => {
+ const dev = 'auto';
+
+ test('should return true', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(true);
+ });
+ });
+ });
+ });
+
+ describe('when version = "v1.0.0-alpha.1"', () => {
+ const version = 'v1.0.0-alpha.1';
+
+ describe('when stable = "v0.1.0"', () => {
+ const stable = 'v0.1.0';
+
+ describe('when dev = "v0.1.0"', () => {
+ const dev = 'v0.1.0';
+
+ test('should return true', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(true);
+ });
+ });
+
+ describe('when dev = "v1.0.0-alpha.1"', () => {
+ const dev = 'v1.0.0-alpha.1';
+
+ test('should return false', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "v1.0.0"', () => {
+ const dev = 'v1.0.0';
+
+ test('should return true', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(true);
+ });
+ });
+
+ describe('when dev = "auto"', () => {
+ const dev = 'auto';
+
+ test('should return true', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(true);
+ });
+ });
+ });
+
+ describe('when stable = "v1.0.0-alpha.1"', () => {
+ const stable = 'v1.0.0-alpha.1';
+
+ describe('when dev = "v0.1.0"', () => {
+ const dev = 'v0.1.0';
+
+ test('should return false', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "v1.0.0-alpha.1"', () => {
+ const dev = 'v1.0.0-alpha.1';
+
+ test('should return false', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "v1.0.0"', () => {
+ const dev = 'v1.0.0';
+
+ test('should return false', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "auto"', () => {
+ const dev = 'auto';
+
+ test('should return false', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(false);
+ });
+ });
+ });
+
+ describe('when stable = "v1.0.0"', () => {
+ const stable = 'v1.0.0';
+
+ describe('when dev = "v0.1.0"', () => {
+ const dev = 'v0.1.0';
+
+ test('should return true', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(true);
+ });
+ });
+
+ describe('when dev = "v1.0.0-alpha.1"', () => {
+ const dev = 'v1.0.0-alpha.1';
+
+ test('should return false', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "v1.0.0"', () => {
+ const dev = 'v1.0.0';
+
+ test('should return true', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(true);
+ });
+ });
+
+ describe('when dev = "auto"', () => {
+ const dev = 'auto';
+
+ test('should return true', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(true);
+ });
+ });
+ });
+
+ describe('when stable = "auto"', () => {
+ const stable = 'auto';
+
+ describe('when dev = "v0.1.0"', () => {
+ const dev = 'v0.1.0';
+
+ test('should return true', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(true);
+ });
+ });
+
+ describe('when dev = "v1.0.0-alpha.1"', () => {
+ const dev = 'v1.0.0-alpha.1';
+
+ test('should return false', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "v1.0.0"', () => {
+ const dev = 'v1.0.0';
+
+ test('should return true', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(true);
+ });
+ });
+
+ describe('when dev = "auto"', () => {
+ const dev = 'auto';
+
+ test('should return true', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(true);
+ });
+ });
+ });
+ });
+
+ describe('when version = "v1.0.0"', () => {
+ const version = 'v1.0.0';
+
+ describe('when stable = "v0.1.0"', () => {
+ const stable = 'v0.1.0';
+
+ describe('when dev = "v0.1.0"', () => {
+ const dev = 'v0.1.0';
+
+ test('should return false', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "v1.0.0-alpha.1"', () => {
+ const dev = 'v1.0.0-alpha.1';
+
+ test('should return false', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "v1.0.0"', () => {
+ const dev = 'v1.0.0';
+
+ test('should return false', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "auto"', () => {
+ const dev = 'auto';
+
+ test('should return false', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(false);
+ });
+ });
+ });
+
+ describe('when stable = "v1.0.0-alpha.1"', () => {
+ const stable = 'v1.0.0-alpha.1';
+
+ describe('when dev = "v0.1.0"', () => {
+ const dev = 'v0.1.0';
+
+ test('should return false', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "v1.0.0-alpha.1"', () => {
+ const dev = 'v1.0.0-alpha.1';
+
+ test('should return false', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "v1.0.0"', () => {
+ const dev = 'v1.0.0';
+
+ test('should return false', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "auto"', () => {
+ const dev = 'auto';
+
+ test('should return false', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(false);
+ });
+ });
+ });
+
+ describe('when stable = "v1.0.0"', () => {
+ const stable = 'v1.0.0';
+
+ describe('when dev = "v0.1.0"', () => {
+ const dev = 'v0.1.0';
+
+ test('should return false', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "v1.0.0-alpha.1"', () => {
+ const dev = 'v1.0.0-alpha.1';
+
+ test('should return false', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "v1.0.0"', () => {
+ const dev = 'v1.0.0';
+
+ test('should return false', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "auto"', () => {
+ const dev = 'auto';
+
+ test('should return false', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(false);
+ });
+ });
+ });
+
+ describe('when stable = "auto"', () => {
+ const stable = 'auto';
+
+ describe('when dev = "v0.1.0"', () => {
+ const dev = 'v0.1.0';
+
+ test('should return false', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "v1.0.0-alpha.1"', () => {
+ const dev = 'v1.0.0-alpha.1';
+
+ test('should return false', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "v1.0.0"', () => {
+ const dev = 'v1.0.0';
+
+ test('should return false', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(false);
+ });
+ });
+
+ describe('when dev = "auto"', () => {
+ const dev = 'auto';
+
+ test('should return false', () => {
+ expect(isStale(version, versions, stable, dev)).toBe(false);
+ });
+ });
+ });
+ });
+});
diff --git a/test/commands/purge.spec/isValid.spec.ts b/test/commands/purge.spec/isValid.spec.ts
new file mode 100644
index 0000000..24d8418
--- /dev/null
+++ b/test/commands/purge.spec/isValid.spec.ts
@@ -0,0 +1,36 @@
+import { isValid } from '../../../src/commands/purge';
+
+describe('when number = undefined', () => {
+ test('should return false', () => {
+ // @ts-expect-error: Intentionally passing undefined to test the typeof check
+ expect(isValid()).toBe(false);
+ });
+});
+
+describe('when number is infinite', () => {
+ test('should return false', () => {
+ expect(isValid(+Infinity)).toBe(false);
+ expect(isValid(-Infinity)).toBe(false);
+ });
+});
+
+describe('when number is NaN', () => {
+ test('should return false', () => {
+ expect(isValid(NaN)).toBe(false);
+ });
+});
+
+describe('when number < 0', () => {
+ test('should return false', () => {
+ expect(isValid(-1)).toBe(false);
+ expect(isValid(-0.0000000001)).toBe(false);
+ });
+});
+
+describe('when 0 <= number < Infinity', () => {
+ test('should return true', () => {
+ expect(isValid(0)).toBe(true);
+ expect(isValid(100.32)).toBe(true);
+ expect(isValid(3)).toBe(true);
+ });
+});
diff --git a/test/commands/purge.spec/shouldExclude.spec.ts b/test/commands/purge.spec/shouldExclude.spec.ts
new file mode 100644
index 0000000..f14f6ea
--- /dev/null
+++ b/test/commands/purge.spec/shouldExclude.spec.ts
@@ -0,0 +1,100 @@
+import { shouldExclude } from '../../../src/commands/purge';
+
+describe('when exclude is empty', () => {
+ test('should return false', () => {
+ expect(shouldExclude('v1.0.0')).toBe(false);
+ });
+});
+
+describe('when version is an exact match for an element of exclude', () => {
+ test('should return true', () => {
+ expect(
+ shouldExclude('v1.0.0', ['v1.0.0'], { includePrerelease: true })
+ ).toBe(true);
+ expect(
+ shouldExclude('v1.0.0', ['v2.0.0', '1.0.0'], {
+ includePrerelease: true,
+ })
+ ).toBe(true);
+ expect(
+ shouldExclude('v1.0.0', ['v1.0.0', 'v2.0.0'], {
+ includePrerelease: true,
+ })
+ ).toBe(true);
+ expect(
+ shouldExclude('v2.0.0', ['v1.0.0', 'v2.0.0'], {
+ includePrerelease: true,
+ })
+ ).toBe(true);
+ expect(
+ shouldExclude('v2.0.0', ['v2.0.0', 'v1.0.0'], {
+ includePrerelease: true,
+ })
+ ).toBe(true);
+ expect(
+ shouldExclude('v2.0.0', ['2.0.0'], { includePrerelease: true })
+ ).toBe(true);
+ });
+});
+
+describe('when exclude includes a minor version which matches', () => {
+ test('should return true', () => {
+ expect(
+ shouldExclude('v1.0.0', ['v1.0.x'], { includePrerelease: true })
+ ).toBe(true);
+ expect(
+ shouldExclude('v1.0.0', ['v2.0.0', '1.0.x'], {
+ includePrerelease: true,
+ })
+ ).toBe(true);
+ expect(
+ shouldExclude('v1.0.0', ['v1.0.x', 'v2.0.0'], {
+ includePrerelease: true,
+ })
+ ).toBe(true);
+ expect(
+ shouldExclude('v2.0.0', ['v1.0.0', 'v2.0.x'], {
+ includePrerelease: true,
+ })
+ ).toBe(true);
+ expect(
+ shouldExclude('v2.0.0', ['v2.0.x', 'v1.0.0'], {
+ includePrerelease: true,
+ })
+ ).toBe(true);
+ expect(
+ shouldExclude('v2.0.0', ['2.0.x'], { includePrerelease: true })
+ ).toBe(true);
+ });
+});
+
+describe('when exclude includes a major version which matches', () => {
+ test('should return true', () => {
+ expect(
+ shouldExclude('v1.0.0', ['v1.x.x'], { includePrerelease: true })
+ ).toBe(true);
+ expect(
+ shouldExclude('v1.0.0', ['v2.0.0', '1.x.x'], {
+ includePrerelease: true,
+ })
+ ).toBe(true);
+ expect(
+ shouldExclude('v1.0.0', ['v1.x', 'v2.0.0'], {
+ includePrerelease: true,
+ })
+ ).toBe(true);
+ expect(
+ shouldExclude('v2.0.0', ['v1.0.0', 'v2.x'], {
+ includePrerelease: true,
+ })
+ ).toBe(true);
+ expect(
+ shouldExclude('v2.0.0', ['v2.x.x', 'v1.0.0'], {
+ includePrerelease: true,
+ })
+ ).toBe(true);
+ expect(
+ shouldExclude('v2.0.0', ['2.x'], { includePrerelease: true })
+ ).toBe(true);
+ });
+});
diff --git a/test/commands/purge.spec/sliceValues.spec.ts b/test/commands/purge.spec/sliceValues.spec.ts
new file mode 100644
index 0000000..85adbe7
--- /dev/null
+++ b/test/commands/purge.spec/sliceValues.spec.ts
@@ -0,0 +1,26 @@
+import { sliceValues } from '../../../src/commands/purge';
+
+describe('when map = { foo: [ "1", "2", "1" ], bar: [ "4", "3", "2" ] }', () => {
+ const map = new Map(
+ Object.entries({ foo: ['1', '2', '1'], bar: ['4', '3', '2'] })
+ );
+
+ describe('when index = 0', () => {
+ const index = 0;
+
+ test('should return: [ [ "1", "2" ], [ "4", "3", "2" ] ]', () => {
+ expect(sliceValues(map, index)).toEqual([
+ ['1', '2'],
+ ['4', '3', '2'],
+ ]);
+ });
+ });
+
+ describe('when index = 2', () => {
+ const index = 2;
+
+ test('should return: [ [], [ "2" ] ]', () => {
+ expect(sliceValues(map, index)).toEqual([[], ['2']]);
+ });
+ });
+});
diff --git a/test/commands/synchronize.spec/getDiffs.spec.ts b/test/commands/synchronize.spec/getDiffs.spec.ts
new file mode 100644
index 0000000..4b1fa9c
--- /dev/null
+++ b/test/commands/synchronize.spec/getDiffs.spec.ts
@@ -0,0 +1,72 @@
+import { join, resolve } from 'node:path';
+import { ensureDir, rm, stat, writeFile } from 'fs-extra';
+import { metadata } from 'typedoc-plugin-versions';
+import { refreshedMetadata } from '../../../src';
+
+import { getDiffs } from '../../../src/commands/synchronize';
+import { each } from 'async';
+
+const testDir = resolve(join('test', '.commands.sync.getDiffs'));
+beforeAll(() => ensureDir(testDir));
+afterAll(() => rm(testDir, { recursive: true, force: true }));
+
+describe('when `out` points to an empty directory', () => {
+ const dir = resolve(join(testDir, 'foo'));
+ beforeAll(() => ensureDir(dir));
+
+ describe('when metadata = {} & refreshedMetadata = { versions: [ "v1.0.0" ], stable: "v1.0.0" }', () => {
+ const metadata: metadata = {};
+ const refreshedMetadata: refreshedMetadata = {
+ versions: ['v1.0.0'],
+ stable: 'v1.0.0',
+ };
+
+ test('should complete appropriately', async () => {
+ const diffs = getDiffs(dir, metadata, refreshedMetadata);
+ expect(diffs).resolves.not.toThrow();
+
+ await each(await diffs, async (diff) => {
+ expect(diff.save()).resolves.not.toThrow();
+ });
+
+ expect(
+ (await stat(join(dir, '.typedoc-plugin-versions'))).isFile()
+ ).toBe(true);
+ expect((await stat(join(dir, 'versions.js'))).isFile()).toBe(true);
+ expect((await stat(join(dir, 'index.html'))).isFile()).toBe(true);
+ });
+ });
+});
+
+describe('when `out` points to a directory containing metadata', () => {
+ const dir = resolve(join(testDir, 'bar'));
+ beforeAll(async () => {
+ await ensureDir(dir);
+ await writeFile(join(dir, '.typedoc-plugin-versions'), 'foo');
+ await writeFile(join(dir, 'versions.js'), 'foo');
+ await writeFile(join(dir, 'index.html'), 'foo');
+ });
+
+ describe('when metadata = {} & refreshedMetadata = { versions: [ "v1.0.0" ], stable: "v1.0.0" }', () => {
+ const metadata: metadata = {};
+ const refreshedMetadata: refreshedMetadata = {
+ versions: ['v1.0.0'],
+ stable: 'v1.0.0',
+ };
+
+ test('should complete appropriately', async () => {
+ const diffs = getDiffs(dir, metadata, refreshedMetadata);
+ expect(diffs).resolves.not.toThrow();
+
+ await each(await diffs, async (diff) => {
+ expect(diff.save()).resolves.not.toThrow();
+ });
+
+ expect(
+ (await stat(join(dir, '.typedoc-plugin-versions'))).isFile()
+ ).toBe(true);
+ expect((await stat(join(dir, 'versions.js'))).isFile()).toBe(true);
+ expect((await stat(join(dir, 'index.html'))).isFile()).toBe(true);
+ });
+ });
+});
diff --git a/test/commands/synchronize.spec/getMetadataDiff.spec.ts b/test/commands/synchronize.spec/getMetadataDiff.spec.ts
new file mode 100644
index 0000000..3fac288
--- /dev/null
+++ b/test/commands/synchronize.spec/getMetadataDiff.spec.ts
@@ -0,0 +1,15 @@
+import { getMetadataDiff } from '../../../src/commands/synchronize';
+
+describe('when left = {} & right = {}', () => {
+ test('should return empty', () => {
+ expect(getMetadataDiff({}, {})).toBe('');
+ });
+});
+
+describe('when left = {} & right = { stable: "v1.0.0" }', () => {
+ test('should return non-empty', () => {
+ expect(
+ getMetadataDiff({}, { stable: 'v1.0.0' }).length
+ ).toBeGreaterThan(0);
+ });
+});
diff --git a/test/commands/synchronize.spec/handler.spec.ts b/test/commands/synchronize.spec/handler.spec.ts
new file mode 100644
index 0000000..6c8ccef
--- /dev/null
+++ b/test/commands/synchronize.spec/handler.spec.ts
@@ -0,0 +1,166 @@
+import { EOL } from 'node:os';
+import { join, resolve } from 'node:path';
+import { ensureDir, rm } from 'fs-extra';
+import { inject } from 'prompts';
+import { getSemanticVersion } from 'typedoc-plugin-versions/src/etc/utils';
+
+import { cli } from '../../../src';
+
+const out = resolve(join('test', '.commands.sync.handler'));
+let consoleLogMock: jest.SpyInstance;
+let consoleErrorMock: jest.SpyInstance;
+let consoleTimeMock: jest.SpyInstance;
+let consoleTimeEndMock: jest.SpyInstance;
+
+beforeAll(() => ensureDir(out));
+afterAll(() => rm(out, { recursive: true, force: true }));
+
+describe('when `out` points to empty directory', () => {
+ beforeEach(
+ () =>
+ (consoleErrorMock = jest
+ .spyOn(console, 'error')
+ .mockImplementation())
+ );
+ afterEach(() => consoleErrorMock.mockRestore());
+
+ test('should error: Missing docs for package.json version...', async () => {
+ await cli().parse(`sync --out ${out}`);
+ expect(console.error).toHaveBeenCalledTimes(1);
+ expect(console.error).toHaveBeenCalledWith(
+ `Missing docs for package.json version: ${getSemanticVersion()}${EOL}Did you forget to run typedoc?`
+ );
+ });
+});
+
+describe('when `out` contains package.json version docs', () => {
+ const dir = join(out, 'foo');
+ const packageVersionDocs = resolve(join(dir, getSemanticVersion()));
+
+ beforeEach(() => ensureDir(packageVersionDocs));
+ afterEach(() => rm(dir, { recursive: true, force: true }));
+
+ describe('when metadata out of date', () => {
+ describe('when user chooses "no"', () => {
+ beforeEach(() => {
+ consoleLogMock = jest
+ .spyOn(console, 'log')
+ .mockImplementation();
+ consoleTimeMock = jest
+ .spyOn(console, 'time')
+ .mockImplementation();
+ consoleTimeEndMock = jest
+ .spyOn(console, 'timeEnd')
+ .mockImplementation();
+ });
+
+ afterEach(() => {
+ consoleLogMock.mockRestore();
+ consoleTimeMock.mockRestore();
+ consoleTimeEndMock.mockRestore();
+ });
+
+ test('should exit without making changes', async () => {
+ inject(['']);
+ await cli().parse(`sync --out ${dir}`);
+ expect(console.log).toHaveBeenCalledTimes(1);
+ expect(console.time).not.toHaveBeenCalled();
+ expect(console.timeEnd).not.toHaveBeenCalled();
+ });
+
+ describe('when user passes --symlinks', () => {
+ test('should exit fixing symlinks only', async () => {
+ await cli().parse(`sync --out ${dir} --symlinks`);
+ expect(console.log).toHaveBeenCalledTimes(1);
+ expect(console.time).toHaveBeenCalledTimes(1);
+ expect(console.time).toHaveBeenCalledWith('symlinks');
+ expect(console.timeEnd).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
+
+ describe('when user chooses "yes"', () => {
+ beforeEach(() => {
+ consoleLogMock = jest
+ .spyOn(console, 'log')
+ .mockImplementation();
+ consoleTimeMock = jest
+ .spyOn(console, 'time')
+ .mockImplementation();
+ consoleTimeEndMock = jest
+ .spyOn(console, 'timeEnd')
+ .mockImplementation();
+ });
+
+ afterEach(() => {
+ consoleLogMock.mockRestore();
+ consoleTimeMock.mockRestore();
+ consoleTimeEndMock.mockRestore();
+ });
+
+ test('should make all changes', async () => {
+ inject(['yes']);
+ await cli().parse(`sync --out ${dir}`);
+ expect(console.log).toHaveBeenCalledTimes(2);
+ expect(console.time).toHaveBeenCalledTimes(4);
+ expect(console.timeEnd).toHaveBeenCalledTimes(4);
+ expect(console.time).toHaveBeenCalledWith('symlinks');
+ });
+ });
+
+ describe('when user passes -y', () => {
+ beforeEach(() => {
+ consoleLogMock = jest
+ .spyOn(console, 'log')
+ .mockImplementation();
+ consoleTimeMock = jest
+ .spyOn(console, 'time')
+ .mockImplementation();
+ consoleTimeEndMock = jest
+ .spyOn(console, 'timeEnd')
+ .mockImplementation();
+ });
+
+ afterEach(() => {
+ consoleLogMock.mockRestore();
+ consoleTimeMock.mockRestore();
+ consoleTimeEndMock.mockRestore();
+ });
+
+ test('should make all changes', async () => {
+ await cli().parse(`sync --out ${dir} -y`);
+ expect(console.log).toHaveBeenCalledTimes(1);
+ expect(console.time).toHaveBeenCalledTimes(4);
+ expect(console.timeEnd).toHaveBeenCalledTimes(4);
+ expect(console.time).toHaveBeenCalledWith('symlinks');
+ });
+ });
+ });
+
+ describe('when metadata up-to-date', () => {
+ beforeEach(async () => {
+ consoleLogMock = jest.spyOn(console, 'log').mockImplementation();
+ consoleTimeMock = jest.spyOn(console, 'time').mockImplementation();
+ consoleTimeEndMock = jest
+ .spyOn(console, 'timeEnd')
+ .mockImplementation();
+ await cli().parse(`sync --out ${dir} -y`);
+ consoleLogMock.mockReset();
+ consoleTimeMock.mockReset();
+ consoleTimeEndMock.mockReset();
+ });
+
+ afterEach(() => {
+ consoleLogMock.mockRestore();
+ consoleTimeMock.mockRestore();
+ consoleTimeEndMock.mockRestore();
+ });
+
+ test('should log: "Already up-to-date."', async () => {
+ inject(['']);
+ await cli().parse(`sync --out ${dir}`);
+ expect(console.log).toHaveBeenCalledTimes(1);
+ expect(console.log).toHaveBeenCalledWith('Already up-to-date.');
+ });
+ });
+});
diff --git a/test/commands/synchronize.spec/makeSymlinks.spec.ts b/test/commands/synchronize.spec/makeSymlinks.spec.ts
new file mode 100644
index 0000000..21a3009
--- /dev/null
+++ b/test/commands/synchronize.spec/makeSymlinks.spec.ts
@@ -0,0 +1,70 @@
+import { join, resolve } from 'node:path';
+import { each } from 'async';
+import { ensureDir, rm, stat, lstat, readlink, pathExists } from 'fs-extra';
+import { refreshedMetadata } from '../../../src';
+
+import { makeSymlinks } from '../../../src/commands/synchronize';
+
+const testDir = resolve(join('test', '.commands.sync.makeSymlinks'));
+beforeAll(() => ensureDir(testDir));
+afterAll(() => rm(testDir, { recursive: true, force: true }));
+
+describe('when metadata = { versions: [ "v1.0.0" ], stable: "v1.0.0" }', () => {
+ const dir = resolve(join(testDir, 'foo'));
+ const metadata: refreshedMetadata = {
+ versions: ['v1.0.0'],
+ stable: 'v1.0.0',
+ };
+
+ beforeAll(() =>
+ each(
+ metadata.versions,
+ async (v) => await ensureDir(resolve(join(dir, v)))
+ )
+ );
+
+ test('should create symlinks appropriately', async () => {
+ makeSymlinks(dir, metadata);
+ expect((await stat(dir)).isDirectory()).toBe(true);
+
+ for (const version of metadata.versions) {
+ expect((await stat(join(dir, version))).isDirectory()).toBe(true);
+ }
+
+ for (const symlink of ['stable', 'dev', 'v1.0']) {
+ const path = join(dir, symlink);
+ expect((await lstat(path)).isSymbolicLink()).toBe(true);
+ expect(await pathExists(await readlink(path))).toBe(true);
+ }
+ });
+});
+
+describe('when metadata = { versions: [ "v0.1.0" ], dev: "v0.1.0" }', () => {
+ const dir = resolve(join(testDir, 'bar'));
+ const metadata: refreshedMetadata = {
+ versions: ['v0.1.0'],
+ dev: 'v0.1.0',
+ };
+
+ beforeAll(() =>
+ each(
+ metadata.versions,
+ async (v) => await ensureDir(resolve(join(dir, v)))
+ )
+ );
+
+ test('should create symlinks appropriately', async () => {
+ makeSymlinks(dir, metadata);
+ expect((await stat(dir)).isDirectory()).toBe(true);
+
+ for (const version of metadata.versions) {
+ expect((await stat(join(dir, version))).isDirectory()).toBe(true);
+ }
+
+ for (const symlink of ['stable', 'dev', 'v0.1']) {
+ const path = join(dir, symlink);
+ expect((await lstat(path)).isSymbolicLink()).toBe(true);
+ expect(await pathExists(await readlink(path))).toBe(true);
+ }
+ });
+});
diff --git a/test/commands/synchronize.spec/refreshIndexHtml.spec.ts b/test/commands/synchronize.spec/refreshIndexHtml.spec.ts
new file mode 100644
index 0000000..8d1deaf
--- /dev/null
+++ b/test/commands/synchronize.spec/refreshIndexHtml.spec.ts
@@ -0,0 +1,17 @@
+import { refreshIndexHtml } from '../../../src/commands/synchronize';
+
+describe('when metadata = { versions: [ "v1.0.0" ], stable: "v1.0.0" }', () => {
+ test('should redirect to stable', () => {
+ expect(
+ refreshIndexHtml({ versions: ['v1.0.0'], stable: 'v1.0.0' })
+ ).toBe('');
+ });
+});
+
+describe('when metadata = { versions: [ "v0.1.0" ], dev: "v0.1.0" }', () => {
+ test('should redirect to dev', () => {
+ expect(refreshIndexHtml({ versions: ['v0.1.0'], dev: 'v0.1.0' })).toBe(
+ ''
+ );
+ });
+});
diff --git a/test/commands/synchronize.spec/refreshVersionJs.spec.ts b/test/commands/synchronize.spec/refreshVersionJs.spec.ts
new file mode 100644
index 0000000..9ce8e07
--- /dev/null
+++ b/test/commands/synchronize.spec/refreshVersionJs.spec.ts
@@ -0,0 +1,19 @@
+import { refreshVersionJs } from '../../../src/commands/synchronize';
+
+describe('when metadata = { versions: [ "v1.0.0" ], stable: "v1.0.0" }', () => {
+ test('should complete appropriately', () => {
+ expect(
+ refreshVersionJs({ versions: ['v1.0.0'], stable: 'v1.0.0' })
+ ).toBe(
+ [
+ '"use strict"',
+ 'export const DOC_VERSIONS = [',
+ " 'stable',",
+ " 'v1.0',",
+ '];',
+ ]
+ .join('\n')
+ .concat('\n')
+ );
+ });
+});
diff --git a/test/utils.spec/coerceStringArray.spec.ts b/test/utils.spec/coerceStringArray.spec.ts
new file mode 100644
index 0000000..26ce931
--- /dev/null
+++ b/test/utils.spec/coerceStringArray.spec.ts
@@ -0,0 +1,25 @@
+import { coerceStringArray } from '../../src/utils';
+
+describe('when array = []', () => {
+ test('should return []', () => {
+ expect(coerceStringArray([])).toEqual([]);
+ });
+});
+
+describe('when array = ["foo", "bar"]', () => {
+ test('should return ["foo", "bar"]', () => {
+ expect(coerceStringArray(['foo', 'bar'])).toEqual(['foo', 'bar']);
+ });
+});
+
+describe('when array = ["`foo`", `"bar"`]', () => {
+ test('should return ["foo", "bar"]', () => {
+ expect(coerceStringArray(['`foo`', `"bar"`])).toEqual(['foo', 'bar']);
+ });
+});
+
+describe('when array = [`\'foo\'`, "bar"]', () => {
+ test('should return ["foo", "bar"]', () => {
+ expect(coerceStringArray([`'foo'`, 'bar'])).toEqual(['foo', 'bar']);
+ });
+});
diff --git a/test/utils.spec/drop.spec.ts b/test/utils.spec/drop.spec.ts
new file mode 100644
index 0000000..2cb23bc
--- /dev/null
+++ b/test/utils.spec/drop.spec.ts
@@ -0,0 +1,33 @@
+import { drop } from '../../src/utils';
+
+describe('using predicate', () => {
+ describe('when array = [-3, -2, -1, 0, 1, 2, 3] & predicate = (v) => v < 0)', () => {
+ test('should return [-3, -2, -1]', () => {
+ expect(drop([-3, -2, -1, 0, 1, 2, 3], (v) => v < 0)).toEqual([
+ -3, -2, -1,
+ ]);
+ });
+
+ test('after the operation, the array should equal [0, 1, 2, 3]', () => {
+ const arr = [-3, -2, -1, 0, 1, 2, 3];
+ expect(drop(arr, (v) => v < 0)).toEqual([-3, -2, -1]);
+ expect(arr).toEqual([0, 1, 2, 3]);
+ });
+ });
+});
+
+describe('using elements', () => {
+ describe('when array = [-3, -2, -1, 0, 1, 2, 3] & elements = [-2, 3, 0]', () => {
+ test('should return [-2, 0, 3]', () => {
+ expect(drop([-3, -2, -1, 0, 1, 2, 3], [-2, 3, 0])).toEqual([
+ -2, 0, 3,
+ ]);
+ });
+
+ test('after the operation, the array should equal [-3, -1, 1, 2]', () => {
+ const arr = [-3, -2, -1, 0, 1, 2, 3];
+ expect(drop(arr, [-2, 3, 0])).toEqual([-2, 0, 3]);
+ expect(arr).toEqual([-3, -1, 1, 2]);
+ });
+ });
+});
diff --git a/test/utils.spec/exclude.spec.ts b/test/utils.spec/exclude.spec.ts
new file mode 100644
index 0000000..7acd32f
--- /dev/null
+++ b/test/utils.spec/exclude.spec.ts
@@ -0,0 +1,21 @@
+import { exclude } from '../../src/utils';
+
+describe('using predicate', () => {
+ describe('when array = [-3, -2, -1, 0, 1, 2, 3] & predicate = (v) => v < 0)', () => {
+ test('should return [0, 1, 2, 3]', () => {
+ expect(exclude([-3, -2, -1, 0, 1, 2, 3], (v) => v < 0)).toEqual([
+ 0, 1, 2, 3,
+ ]);
+ });
+ });
+});
+
+describe('using elements', () => {
+ describe('when array = [-3, -2, -1, 0, 1, 2, 3] & elements = [-2, 3, 0]', () => {
+ test('should return [-3, -1, 1, 2]', () => {
+ expect(exclude([-3, -2, -1, 0, 1, 2, 3], [-2, 3, 0])).toEqual([
+ -3, -1, 1, 2,
+ ]);
+ });
+ });
+});
diff --git a/test/utils.spec/findFile.spec.ts b/test/utils.spec/findFile.spec.ts
new file mode 100644
index 0000000..e3e7a9f
--- /dev/null
+++ b/test/utils.spec/findFile.spec.ts
@@ -0,0 +1,65 @@
+import { join, resolve } from 'node:path';
+import { cwd } from 'node:process';
+
+import { findFile } from '../../src/utils';
+
+describe('when path = undefined & filePaths = undefined', () => {
+ test('should throw Error', async () => {
+ try {
+ await findFile();
+ fail('must throw');
+ } catch (e) {
+ expect(e).toEqual(
+ new Error(
+ 'filePaths must contain at least one file path when path is a directory!'
+ )
+ );
+ }
+ });
+});
+
+describe('when path = undefined & filePaths = ["typedoc.json"]', () => {
+ test('should find local package typedoc.json', async () => {
+ expect(await findFile(undefined, ['typedoc.json'])).toBe(
+ resolve(join(cwd(), 'typedoc.json'))
+ );
+ });
+});
+
+describe('when path = undefined & filePaths = ["foo.bar", "typedoc.json"]', () => {
+ test('should find local package typedoc.json', async () => {
+ expect(await findFile(undefined, ['foo.bar', 'typedoc.json'])).toBe(
+ resolve(join(cwd(), 'typedoc.json'))
+ );
+ });
+});
+
+describe('when path = undefined & filePaths = ["foo.bar"]', () => {
+ test('should throw Error', async () => {
+ try {
+ await findFile(undefined, ['foo.bar']);
+ fail('must throw');
+ } catch (e) {
+ expect(e).toEqual(new Error('A matching file could not be found.'));
+ }
+ });
+});
+
+describe('when path = "typedoc.json"', () => {
+ test('should find local package typedoc.json', async () => {
+ expect(await findFile('typedoc.json')).toBe(
+ resolve(join(cwd(), 'typedoc.json'))
+ );
+ });
+});
+
+describe('when path = "foo.bar"', () => {
+ test('should throw Error', async () => {
+ try {
+ await findFile('foo.bar');
+ fail('must throw');
+ } catch (e) {
+ expect(e).toEqual(new Error('Path does not exist: foo.bar'));
+ }
+ });
+});
diff --git a/test/utils.spec/findTsConfigFile.spec.ts b/test/utils.spec/findTsConfigFile.spec.ts
new file mode 100644
index 0000000..8a83fe3
--- /dev/null
+++ b/test/utils.spec/findTsConfigFile.spec.ts
@@ -0,0 +1,40 @@
+import { join, resolve } from 'node:path';
+import { cwd } from 'node:process';
+
+import { findTsConfigFile } from '../../src/utils';
+
+describe('when path = undefined', () => {
+ test('should find local package tsconfig.json', () => {
+ expect(findTsConfigFile()).toBe(resolve(join(cwd(), 'tsconfig.json')));
+ });
+});
+
+describe('when path = "./tsconfig.json"', () => {
+ test('should find local package tsconfig.json', () => {
+ expect(findTsConfigFile()).toBe(resolve(join(cwd(), 'tsconfig.json')));
+ });
+});
+
+describe('when path = "./foo"', () => {
+ test('should throw Error', () => {
+ try {
+ findTsConfigFile('./foo');
+ fail('must throw');
+ } catch (e) {
+ expect(e).toEqual(new Error('Path does not exist: foo'));
+ }
+ });
+});
+
+describe('when path = "../"', () => {
+ test('should throw Error', () => {
+ try {
+ findTsConfigFile('../');
+ fail('must throw');
+ } catch (e) {
+ expect(e).toEqual(
+ new Error('Cannot find a valid tsconfig at path: ..')
+ );
+ }
+ });
+});
diff --git a/test/utils.spec/getOptions.spec.ts b/test/utils.spec/getOptions.spec.ts
new file mode 100644
index 0000000..e704868
--- /dev/null
+++ b/test/utils.spec/getOptions.spec.ts
@@ -0,0 +1,221 @@
+import { join } from 'node:path';
+import { cwd } from 'node:process';
+import { cli, isOptions, Options } from '../../src';
+import { commonOptions } from '../../src/commands/builders';
+
+import { getOptions } from '../../src/utils';
+
+describe('local package', () => {
+ const dir = cwd();
+ let options: Options;
+
+ describe('when no cli arguments passed', () => {
+ beforeAll(async () => {
+ options = await getOptions(
+ await cli()
+ .options({ ...commonOptions })
+ .demandCommand(0)
+ .parse()
+ );
+ });
+
+ test('return should implement the Options interface', () => {
+ expect(isOptions(options)).toBe(true);
+ });
+
+ test('return.out should be "./docs"', () => {
+ expect(options.out).toBe(join(dir, 'docs'));
+ });
+
+ test('return.versions should equal {stable: "auto", dev: "auto", domLocation: "false"}', () => {
+ expect(options.versions).toEqual({
+ stable: 'auto',
+ dev: 'auto',
+ domLocation: 'false',
+ });
+ });
+ });
+
+ describe('when `--out docs`', () => {
+ beforeAll(async () => {
+ options = await getOptions(
+ await cli()
+ .options({ ...commonOptions })
+ .demandCommand(0)
+ .parse('--out docs')
+ );
+ });
+
+ test('return should implement the Options interface', () => {
+ expect(isOptions(options)).toBe(true);
+ });
+
+ test('return.out should be "./docs"', () => {
+ expect(options.out).toBe(join(dir, 'docs'));
+ });
+
+ test('return.versions should equal {stable: "auto", dev: "auto", domLocation: "false"}', () => {
+ expect(options.versions).toEqual({
+ stable: 'auto',
+ dev: 'auto',
+ domLocation: 'false',
+ });
+ });
+ });
+
+ describe('when `--out foo`', () => {
+ test('should throw Error', async () => {
+ try {
+ await getOptions(
+ await cli()
+ .options({ ...commonOptions })
+ .demandCommand(0)
+ .parse('--out foo')
+ );
+ fail('must throw');
+ } catch (e) {
+ expect(e).toEqual(new Error('Directory does not exist: foo'));
+ }
+ });
+ });
+
+ describe('when `--tsconfig .`', () => {
+ beforeAll(async () => {
+ options = await getOptions(
+ await cli()
+ .options({ ...commonOptions })
+ .demandCommand(0)
+ .parse('--tsconfig .')
+ );
+ });
+
+ test('return should implement the Options interface', () => {
+ expect(isOptions(options)).toBe(true);
+ });
+
+ test('return.out should be "./docs"', () => {
+ expect(options.out).toBe(join(dir, 'docs'));
+ });
+
+ test('return.versions should equal {stable: "auto", dev: "auto", domLocation: "false"}', () => {
+ expect(options.versions).toEqual({
+ stable: 'auto',
+ dev: 'auto',
+ domLocation: 'false',
+ });
+ });
+ });
+
+ describe('when `--tsconfig="tsconfig.json"`', () => {
+ beforeAll(async () => {
+ options = await getOptions(
+ await cli()
+ .options({ ...commonOptions })
+ .demandCommand(0)
+ .parse('--tsconfig="tsconfig.json"')
+ );
+ });
+
+ test('return should implement the Options interface', () => {
+ expect(isOptions(options)).toBe(true);
+ });
+
+ test('return.out should be "./docs"', () => {
+ expect(options.out).toBe(join(dir, 'docs'));
+ });
+
+ test('return.versions should equal {stable: "auto", dev: "auto", domLocation: "false"}', () => {
+ expect(options.versions).toEqual({
+ stable: 'auto',
+ dev: 'auto',
+ domLocation: 'false',
+ });
+ });
+ });
+
+ describe('when `--tsconfig ./foobar`', () => {
+ test('should throw Error', async () => {
+ try {
+ await getOptions(
+ await cli()
+ .options({ ...commonOptions })
+ .demandCommand(0)
+ .parse('--tsconfig ./foobar')
+ );
+ fail('must throw');
+ } catch (e) {
+ expect(e).toEqual(new Error('Path does not exist: foobar'));
+ }
+ });
+ });
+
+ describe('when `--typedoc .`', () => {
+ beforeAll(async () => {
+ options = await getOptions(
+ await cli()
+ .options({ ...commonOptions })
+ .demandCommand(0)
+ .parse('--typedoc .')
+ );
+ });
+
+ test('return should implement the Options interface', () => {
+ expect(isOptions(options)).toBe(true);
+ });
+
+ test('return.out should be "./docs"', () => {
+ expect(options.out).toBe(join(dir, 'docs'));
+ });
+
+ test('return.versions should equal {stable: "auto", dev: "auto", domLocation: "false"}', () => {
+ expect(options.versions).toEqual({
+ stable: 'auto',
+ dev: 'auto',
+ domLocation: 'false',
+ });
+ });
+ });
+
+ describe('when `--typedoc=typedoc.json`', () => {
+ beforeAll(async () => {
+ options = await getOptions(
+ await cli()
+ .options({ ...commonOptions })
+ .demandCommand(0)
+ .parse('--typedoc=typedoc.json')
+ );
+ });
+
+ test('return should implement the Options interface', () => {
+ expect(isOptions(options)).toBe(true);
+ });
+
+ test('return.out should be "./docs"', () => {
+ expect(options.out).toBe(join(dir, 'docs'));
+ });
+
+ test('return.versions should equal {stable: "auto", dev: "auto", domLocation: "false"}', () => {
+ expect(options.versions).toEqual({
+ stable: 'auto',
+ dev: 'auto',
+ domLocation: 'false',
+ });
+ });
+ });
+
+ describe('when `--typedoc ./bar`', () => {
+ test('should throw Error', async () => {
+ try {
+ await getOptions(
+ await cli()
+ .options({ ...commonOptions })
+ .demandCommand(0)
+ .parse('--typedoc ./bar')
+ );
+ fail('must throw');
+ } catch (e) {
+ expect(e).toEqual(new Error('Path does not exist: bar'));
+ }
+ });
+ });
+});
diff --git a/test/utils.spec/getOut.spec.ts b/test/utils.spec/getOut.spec.ts
new file mode 100644
index 0000000..e495f32
--- /dev/null
+++ b/test/utils.spec/getOut.spec.ts
@@ -0,0 +1,66 @@
+import { join, resolve } from 'node:path';
+import { cwd } from 'node:process';
+import { cli } from '../../src';
+import { commonOptions, out } from '../../src/commands/builders';
+
+import { getOut } from '../../src/utils';
+
+describe('local package', () => {
+ const dir = cwd();
+
+ describe('when no cli arguments passed', () => {
+ describe('when parsing `commonOptions`', () => {
+ test('return should be "./docs"', async () => {
+ expect(
+ await getOut(
+ await cli()
+ .options(commonOptions)
+ .demandCommand(0)
+ .parse()
+ )
+ ).toBe(resolve(join(dir, 'docs')));
+ });
+ });
+
+ describe('when parsing only `out`', () => {
+ test('should throw Error', async () => {
+ try {
+ await getOut(
+ await cli().options({ out }).demandCommand(0).parse()
+ );
+ fail('must throw');
+ } catch (e) {
+ expect(e).toEqual(new Error("Could not parse 'out'"));
+ }
+ });
+ });
+ });
+
+ describe('when `--out docs', () => {
+ describe('when parsing `commonOptions`', () => {
+ test('return should be "./docs"', async () => {
+ expect(
+ await getOut(
+ await cli()
+ .options(commonOptions)
+ .demandCommand(0)
+ .parse('--out docs')
+ )
+ ).toBe(resolve(join(dir, 'docs')));
+ });
+ });
+
+ describe('when parsing only `out`', () => {
+ test('return should be "./docs"', async () => {
+ expect(
+ await getOut(
+ await cli()
+ .options(commonOptions)
+ .demandCommand(0)
+ .parse('--out docs')
+ )
+ ).toBe(resolve(join(dir, 'docs')));
+ });
+ });
+ });
+});
diff --git a/test/utils.spec/isBrokenSymlink.spec.ts b/test/utils.spec/isBrokenSymlink.spec.ts
new file mode 100644
index 0000000..486946a
--- /dev/null
+++ b/test/utils.spec/isBrokenSymlink.spec.ts
@@ -0,0 +1,49 @@
+import { join, resolve } from 'node:path';
+import { ensureSymlink, ensureDir, rm } from 'fs-extra';
+
+import { isBrokenSymlink } from '../../src/utils';
+
+const testDir = resolve(join('test', '.utils.isBrokenSymlink'));
+beforeAll(() => ensureDir(testDir));
+afterAll(() => rm(testDir, { recursive: true, force: true }));
+
+describe('when path points to a file', () => {
+ test('should return false', async () => {
+ expect(await isBrokenSymlink(resolve('tsconfig.json'))).toBe(false);
+ });
+});
+
+describe('when path points to a non-existant path', () => {
+ test('should return false', async () => {
+ expect(await isBrokenSymlink(resolve('foo.bar'))).toBe(false);
+ });
+});
+
+describe('when path points to a valid symbolic link', () => {
+ const src = resolve(join(testDir, 'foo'));
+ const target = resolve(join(testDir, 'bar'));
+
+ beforeAll(async () => {
+ await ensureDir(src);
+ await ensureSymlink(src, target, 'junction');
+ });
+
+ test('should return false', async () => {
+ expect(await isBrokenSymlink(target)).toBe(false);
+ });
+});
+
+describe('when path points to a broken symbolic link', () => {
+ const src = resolve(join(testDir, 'foobar'));
+ const target = resolve(join(testDir, 'barfoo'));
+
+ beforeAll(async () => {
+ await ensureDir(src);
+ await ensureSymlink(src, target, 'junction');
+ await rm(src, { recursive: true, force: true });
+ });
+
+ test('should return false', async () => {
+ expect(await isBrokenSymlink(target)).toBe(true);
+ });
+});
diff --git a/test/utils.spec/isSymlink.spec.ts b/test/utils.spec/isSymlink.spec.ts
new file mode 100644
index 0000000..f3f9fe5
--- /dev/null
+++ b/test/utils.spec/isSymlink.spec.ts
@@ -0,0 +1,49 @@
+import { join, resolve } from 'node:path';
+import { ensureSymlink, ensureDir, rm } from 'fs-extra';
+
+import { isSymlink } from '../../src/utils';
+
+const testDir = resolve(join('test', '.utils.isSymlink'));
+beforeAll(() => ensureDir(testDir));
+afterAll(() => rm(testDir, { recursive: true, force: true }));
+
+describe('when path points to a file', () => {
+ test('should return false', async () => {
+ expect(await isSymlink(resolve('tsconfig.json'))).toBe(false);
+ });
+});
+
+describe('when path points to a non-existant path', () => {
+ test('should return false', async () => {
+ expect(await isSymlink(resolve('foo.bar'))).toBe(false);
+ });
+});
+
+describe('when path points to a valid symbolic link', () => {
+ const src = resolve(join(testDir, 'foo'));
+ const target = resolve(join(testDir, 'bar'));
+
+ beforeAll(async () => {
+ await ensureDir(src);
+ await ensureSymlink(src, target, 'junction');
+ });
+
+ test('should return false', async () => {
+ expect(await isSymlink(target)).toBe(true);
+ });
+});
+
+describe('when path points to a broken symbolic link', () => {
+ const src = resolve(join(testDir, 'foobar'));
+ const target = resolve(join(testDir, 'barfoo'));
+
+ beforeAll(async () => {
+ await ensureDir(src);
+ await ensureSymlink(src, target, 'junction');
+ await rm(src, { recursive: true, force: true });
+ });
+
+ test('should return false', async () => {
+ expect(await isSymlink(target)).toBe(true);
+ });
+});
diff --git a/test/utils.spec/unlinkBrokenSymlinks.spec.ts b/test/utils.spec/unlinkBrokenSymlinks.spec.ts
new file mode 100644
index 0000000..9410654
--- /dev/null
+++ b/test/utils.spec/unlinkBrokenSymlinks.spec.ts
@@ -0,0 +1,66 @@
+import { join, resolve } from 'node:path';
+import { ensureSymlink, ensureDir, rm, stat, lstat, readlink } from 'fs-extra';
+
+import { unlinkBrokenSymlinks } from '../../src/utils';
+
+const testDir = resolve(join('test', '.utils.unlinkBrokenSymlinks'));
+beforeAll(() => ensureDir(testDir));
+afterAll(() => rm(testDir, { recursive: true, force: true }));
+
+describe('when dir points to a file', () => {
+ test('should throw error', async () => {
+ try {
+ await unlinkBrokenSymlinks(resolve('tsconfig.json'));
+ fail('must throw');
+ } catch (e) {
+ expect(e).toHaveProperty('code', 'ENOTDIR');
+ }
+ });
+});
+
+describe('when dir points to an empty directory', () => {
+ const dir = resolve(join(testDir, 'foo'));
+ beforeAll(() => ensureDir(dir));
+
+ test('should complete normally', () => {
+ expect(unlinkBrokenSymlinks(testDir)).resolves.not.toThrow();
+ });
+});
+
+describe('when dir points to a directory containing valid symlinks', () => {
+ const dir = resolve(join(testDir, 'bar'));
+ const src = resolve(join(dir, 'src'));
+ const target = resolve(join(dir, 'target'));
+
+ beforeAll(async () => {
+ await ensureDir(src);
+ await ensureSymlink(src, target, 'junction');
+ });
+
+ test('should complete normally without deleting any symlinks', async () => {
+ await unlinkBrokenSymlinks(dir);
+ expect((await stat(dir)).isDirectory()).toBe(true);
+ expect((await stat(src)).isDirectory()).toBe(true);
+ expect((await lstat(target)).isSymbolicLink()).toBe(true);
+ expect(resolve(await readlink(target))).toBe(src);
+ });
+});
+
+describe('when dir points to a directory containing invalid symlinks', () => {
+ const dir = resolve(join(testDir, 'foobar'));
+ const src = resolve(join(dir, 'src'));
+ const target = resolve(join(dir, 'target'));
+
+ beforeAll(async () => {
+ await ensureDir(src);
+ await ensureSymlink(src, target, 'junction');
+ await rm(src, { recursive: true, force: true });
+ });
+
+ test('should complete and to have removed the broken symlink', async () => {
+ await unlinkBrokenSymlinks(dir);
+ expect((await stat(dir)).isDirectory()).toBe(true);
+ expect(stat(src)).rejects.toThrow();
+ expect(lstat(target)).rejects.toThrow();
+ });
+});
diff --git a/typedoc.json b/typedoc.json
index 53e4b1e..126b5dd 100644
--- a/typedoc.json
+++ b/typedoc.json
@@ -1,5 +1,41 @@
{
- "entryPoints": "src",
+ "$schema": "https://typedoc.org/schema.json",
+ "entryPoints": ["src"],
"entryPointStrategy": "expand",
- "includeVersion": true
+ "includeVersion": true,
+ "externalSymbolLinkMappings": {
+ "typescript": {
+ "Error": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error",
+ "Infinity": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Infinity",
+ "Map": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map",
+ "Promise": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise",
+ "Required": "https://www.typescriptlang.org/docs/handbook/utility-types.html#requiredtype"
+ },
+ "node:process": {
+ "cwd": "https://nodejs.org/docs/latest/api/process.html#process_process_cwd"
+ },
+ "cli-diff": {
+ "diff": "https://github.com/j-f1/cli-diff#usage"
+ },
+ "@types/semver": {
+ "RangeOptions": "https://github.com/npm/node-semver#readme"
+ },
+ "typedoc-plugin-versions": {
+ "metadata": "https://citkane.github.io/typedoc-plugin-versions/stable/types/metadata.html",
+ "version": "https://citkane.github.io/typedoc-plugin-versions/dev/types/version.html",
+ "versionsOptions": "https://citkane.github.io/typedoc-plugin-versions/stable/interfaces/versionsOptions.html",
+ "versionsOptions.stable": "https://citkane.github.io/typedoc-plugin-versions/dev/interfaces/versionsOptions.html#stable",
+ "versionsOptions.dev": "https://citkane.github.io/typedoc-plugin-versions/dev/interfaces/versionsOptions.html#dev"
+ },
+ "typedoc-plugin-versions/etc/utils": {
+ "refreshMetadata": "https://citkane.github.io/typedoc-plugin-versions/dev/functions/utils.refreshMetadata.html"
+ },
+ "@types/yargs": {
+ "yargs.Argv": "https://yargs.js.org/docs/#api-reference-argv",
+ "yargs.ArgumentsCamelCase": "https://yargs.js.org/docs/#api-reference-argv",
+ "yargs.Options": "https://yargs.js.org/docs/#api-reference-optionskey-opt",
+ "yargs.InferredOptionTypes": "https://yargs.js.org/docs/#api-reference-optionskey-opt"
+ }
+ },
+ "exclude": ["src/cli.ts"]
}