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"] }