diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index a33dddcd..00000000 --- a/.eslintrc +++ /dev/null @@ -1,19 +0,0 @@ -{ - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 2020, - "sourceType": "module" - }, - "extends": [ - "plugin:@typescript-eslint/recommended", - "prettier", - "plugin:prettier/recommended" - ], - - "rules": { - "padding-line-between-statements": [ - "error", - { "blankLine": "always", "prev": "multiline-expression", "next": "multiline-expression" } - ] - } -} diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..78edaa76 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,15 @@ +{ + "extends": [ + "plugin:@typescript-eslint/recommended", + "oclif", "oclif-typescript", + "prettier", + "plugin:prettier/recommended" + ], + "rules": { + "camelcase": "off", + "padding-line-between-statements": [ + "error", + { "blankLine": "always", "prev": "multiline-expression", "next": "multiline-expression" } + ] + } +} diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 543af27f..331393c3 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -16,7 +16,7 @@ jobs: - ubuntu-latest - macos-latest - windows-latest - node_version: [ '14', '16', '18' ] + node_version: [ '18', '20', '23' ] architecture: [ 'x64' ] # an extra windows-x86 run: # include: diff --git a/.gitignore b/.gitignore index da5a2d2a..6f224ff3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,15 @@ *-debug.log *-error.log /.nyc_output +**/.DS_Store +/.idea /dist -/lib -*oclif.manifest.json /tmp -/yarn.lock -node_modules +/node_modules +oclif.manifest.json + + + +yarn.lock +pnpm-lock.yaml + diff --git a/.mocharc.js b/.mocharc.js deleted file mode 100644 index 2b5aa1da..00000000 --- a/.mocharc.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -module.exports = { - exit: true, - bail: true, - timeout: 60000, - recursive: true, - reporter: 'spec', - require: [ - 'test/helpers/init.js', - 'ts-node/register', - 'source-map-support/register', - ], - 'watch-files': ['src/**/*.ts', 'tests/**/*.ts'], -}; diff --git a/.mocharc.json b/.mocharc.json new file mode 100644 index 00000000..187ba7aa --- /dev/null +++ b/.mocharc.json @@ -0,0 +1,17 @@ +{ + "require": [ + "ts-node/register", + "./test/helpers/init.ts" + ], + "watch-extensions": [ + "ts" + ], + "recursive": true, + "reporter": "spec", + "timeout": 60000, + "node-option": [ + "loader=ts-node/esm", + "experimental-specifier-resolution=node", + "no-warnings=ExperimentalWarning" + ] +} diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index ca0e0d1b..00000000 --- a/.prettierrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "endOfLine": "auto", - "semi": true, - "trailingComma": "all", - "singleQuote": true, - "printWidth": 90, - "tabWidth": 2 -} diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..63143357 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1 @@ +"@oclif/prettier-config" diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..b4ac0dac --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "attach", + "name": "Attach", + "port": 9229, + "skipFiles": ["/**"] + }, + { + "type": "node", + "request": "launch", + "name": "Execute Command", + "skipFiles": ["/**"], + "runtimeExecutable": "node", + "runtimeArgs": ["--loader", "ts-node/esm", "--no-warnings=ExperimentalWarning"], + "program": "${workspaceFolder}/bin/dev.js", + "args": ["hello", "world"] + } + ] +} diff --git a/bin/dev.cmd b/bin/dev.cmd new file mode 100644 index 00000000..cec553be --- /dev/null +++ b/bin/dev.cmd @@ -0,0 +1,3 @@ +@echo off + +node --loader ts-node/esm --no-warnings=ExperimentalWarning "%~dp0\dev" %* diff --git a/bin/dev.js b/bin/dev.js new file mode 100755 index 00000000..dd748048 --- /dev/null +++ b/bin/dev.js @@ -0,0 +1,6 @@ +#!/usr/bin/env -S node --loader ts-node/esm --disable-warning=ExperimentalWarning + +// eslint-disable-next-line n/shebang +import {execute} from '@oclif/core' + +await execute({development: true, dir: import.meta.url}) diff --git a/bin/run b/bin/run deleted file mode 100755 index 30b14e17..00000000 --- a/bin/run +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env node - -require('@oclif/command').run() -.then(require('@oclif/command/flush')) -.catch(require('@oclif/errors/handle')) diff --git a/bin/run.js b/bin/run.js new file mode 100755 index 00000000..dd50271f --- /dev/null +++ b/bin/run.js @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +import {execute} from '@oclif/core' + +await execute({dir: import.meta.url}) diff --git a/package-lock.json b/package-lock.json index e5735264..1cde6598 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,13 +16,13 @@ "@oclif/plugin-help": "^6", "@oclif/plugin-warn-if-update-available": "^3.1.20", "@stoplight/yaml": "^4.3.0", + "async-mutex": "^0.5.0", "axios": "^1.7.7", "chalk": "^5.3.0", "debug": "^4.3.7", "jsonpath": "^1.1.1", "mergician": "^2.0.2", - "oas-schemas": "git+https://git@github.com/OAI/OpenAPI-Specification.git#3.1.1", - "object-treeify": "^4.0.1", + "oas-schemas": "git+https://git@github.com/OAI/OpenAPI-Specification.git#882d1caedb0bff825a1fd10728e7e3dc43912d37", "open": "^10.1.0" }, "bin": { @@ -36,16 +36,21 @@ "@types/jsonpath": "^0.2.4", "@types/mocha": "^10", "@types/node": "^18", + "@types/sinon": "^17.0.3", "@typescript-eslint/eslint-plugin": "^8.13.0", - "chai": "^4", + "chai": "^4.5.0", "eslint": "^8", "eslint-config-oclif": "^5", "eslint-config-oclif-typescript": "^3", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.1", "mocha": "^10", + "mock-stdin": "^1.0.0", + "nock": "^14.0.0-beta.16", + "nyc": "^17.1.0", "oclif": "^4", "shx": "^0.3.3", + "sinon": "^19.0.2", "ts-node": "^10", "typescript": "^5" }, @@ -53,6 +58,29 @@ "node": ">=18.0.0" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@ampproject/remapping/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@apidevtools/json-schema-ref-parser": { "version": "11.7.2", "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.7.2.tgz", @@ -1056,6 +1084,171 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/compat-data": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.2.tgz", + "integrity": "sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", + "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.0", + "@babel/generator": "^7.26.0", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.0", + "@babel/parser": "^7.26.0", + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.26.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz", + "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.26.2", + "@babel/types": "^7.26.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", + "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.25.9", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-validator-identifier": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", @@ -1064,6 +1257,97 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", + "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", + "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.26.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", + "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/generator": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/template": "^7.25.9", + "@babel/types": "^7.25.9", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", + "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@clack/core": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/@clack/core/-/core-0.3.4.tgz", @@ -1089,7 +1373,6 @@ }, "node_modules/@clack/prompts/node_modules/is-unicode-supported": { "version": "1.3.0", - "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -2012,90 +2295,276 @@ "node": ">=18" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, "engines": { - "node": ">=6.0.0" + "node": ">=8" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "sprintf-js": "~1.0.2" } }, - "node_modules/@jsdevtools/ono": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", - "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==" + "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "node_modules/@istanbuljs/load-nyc-config/node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" }, "engines": { - "node": ">= 8" + "node": ">=4" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, "engines": { - "node": ">= 8" + "node": ">=8" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "argparse": "^1.0.7", + "esprima": "^4.0.0" }, - "engines": { - "node": ">= 8" + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/@nolyfill/is-core-module": { - "version": "1.0.39", - "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", - "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, "engines": { - "node": ">=12.4.0" + "node": ">=8" } }, - "node_modules/@oclif/core": { - "version": "4.0.31", - "resolved": "https://registry.npmjs.org/@oclif/core/-/core-4.0.31.tgz", - "integrity": "sha512-7oyIZv/C1TP+fPc2tSzVPYqG1zU+nel1QvJxjAWyVhud0J8B5SpKZnryedxs3nlSVPJ6K1MT31C9esupCBYgZw==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, "dependencies": { - "ansi-escapes": "^4.3.2", - "ansis": "^3.3.2", - "clean-stack": "^3.0.1", - "cli-spinners": "^2.9.2", - "debug": "^4.3.7", - "ejs": "^3.1.10", - "get-package-type": "^0.1.0", - "globby": "^11.1.0", + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==" + }, + "node_modules/@mswjs/interceptors": { + "version": "0.36.10", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.36.10.tgz", + "integrity": "sha512-GXrJgakgJW3DWKueebkvtYgGKkxA7s0u5B0P5syJM5rvQUnrpLPigvci8Hukl7yEM+sU06l+er2Fgvx/gmiRgg==", + "dev": true, + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@oclif/core": { + "version": "4.0.31", + "resolved": "https://registry.npmjs.org/@oclif/core/-/core-4.0.31.tgz", + "integrity": "sha512-7oyIZv/C1TP+fPc2tSzVPYqG1zU+nel1QvJxjAWyVhud0J8B5SpKZnryedxs3nlSVPJ6K1MT31C9esupCBYgZw==", + "dependencies": { + "ansi-escapes": "^4.3.2", + "ansis": "^3.3.2", + "clean-stack": "^3.0.1", + "cli-spinners": "^2.9.2", + "debug": "^4.3.7", + "ejs": "^3.1.10", + "get-package-type": "^0.1.0", + "globby": "^11.1.0", "indent-string": "^4.0.0", "is-wsl": "^2.2.0", "lilconfig": "^3.1.2", @@ -2175,6 +2644,28 @@ "@oclif/core": ">= 3.0.0" } }, + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true + }, "node_modules/@pkgr/core": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", @@ -2301,6 +2792,50 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/commons/node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz", + "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "lodash.get": "^4.4.2", + "type-detect": "^4.1.0" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", + "dev": true + }, "node_modules/@smithy/abort-controller": { "version": "3.1.6", "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.6.tgz", @@ -3164,6 +3699,21 @@ "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, + "node_modules/@types/sinon": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz", + "integrity": "sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==", + "dev": true, + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true + }, "node_modules/@types/wrap-ansi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", @@ -3703,6 +4253,28 @@ "node": ">= 14" } }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aggregate-error/node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -3785,6 +4357,24 @@ "node": ">= 8" } }, + "node_modules/append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "dependencies": { + "default-require-extensions": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", + "dev": true + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -3943,6 +4533,14 @@ "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" }, + "node_modules/async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/async-retry": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", @@ -4108,6 +4706,38 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "node_modules/browserslist": { + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", + "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001669", + "electron-to-chromium": "^1.5.41", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -4201,6 +4831,21 @@ "node": ">=14.16" } }, + "node_modules/caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "dependencies": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -4250,6 +4895,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001680", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001680.tgz", + "integrity": "sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, "node_modules/capital-case": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", @@ -4533,6 +5198,12 @@ "node": ">= 0.8" } }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -4572,6 +5243,12 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, "node_modules/cosmiconfig": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", @@ -4621,9 +5298,9 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "dependencies": { "path-key": "^3.1.0", @@ -4634,27 +5311,6 @@ "node": ">= 8" } }, - "node_modules/cross-spawn/node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/cross-spawn/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/css-select": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", @@ -4838,6 +5494,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/default-require-extensions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", + "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==", + "dev": true, + "dependencies": { + "strip-bom": "^4.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-require-extensions/node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", @@ -5100,6 +5780,12 @@ "node": ">=0.10.0" } }, + "node_modules/electron-to-chromium": { + "version": "1.5.58", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.58.tgz", + "integrity": "sha512-al2l4r+24ZFL7WzyPTlyD0fC33LLzvxqLCwurtBibVPghRGO9hSTl+tis8t1kD7biPiH/en4U0I7o/nQbYeoVA==", + "dev": true + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -5317,6 +6003,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -6435,6 +7127,23 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -6517,6 +7226,22 @@ "is-callable": "^1.1.3" } }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", @@ -6547,6 +7272,26 @@ "node": ">= 0.6" } }, + "node_modules/fromentries": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", + "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -6617,6 +7362,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -6997,6 +7751,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, + "dependencies": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hasha/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -7048,6 +7827,12 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "node_modules/htmlparser2": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", @@ -7493,6 +8278,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -7632,6 +8423,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -7656,6 +8453,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -7673,6 +8479,143 @@ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "dev": true, + "dependencies": { + "append-transform": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-processinfo": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", + "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", + "dev": true, + "dependencies": { + "archy": "^1.0.0", + "cross-spawn": "^7.0.3", + "istanbul-lib-coverage": "^3.2.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-processinfo/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jake": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", @@ -7797,6 +8740,12 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, "node_modules/json5": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", @@ -7828,6 +8777,12 @@ "underscore": "1.12.1" } }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -7894,6 +8849,18 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", + "dev": true + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -7980,6 +8947,30 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -8172,6 +9163,13 @@ "node": ">=10" } }, + "node_modules/mock-stdin": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mock-stdin/-/mock-stdin-1.0.0.tgz", + "integrity": "sha512-tukRdb9Beu27t6dN+XztSRHq9J0B/CoAOySGzHfn8UTfmqipA5yNT/sDUEyYdAV3Hpka6Wx6kOMxuObdOex60Q==", + "dev": true, + "license": "MIT" + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -8214,6 +9212,19 @@ "node": ">= 0.4.0" } }, + "node_modules/nise": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz", + "integrity": "sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.1", + "@sinonjs/text-encoding": "^0.7.3", + "just-extend": "^6.2.0", + "path-to-regexp": "^8.1.0" + } + }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -8224,6 +9235,38 @@ "tslib": "^2.0.3" } }, + "node_modules/nock": { + "version": "14.0.0-beta.16", + "resolved": "https://registry.npmjs.org/nock/-/nock-14.0.0-beta.16.tgz", + "integrity": "sha512-H6ZyT+Naz9wfy0gNrhD0m+VIkCq9li/eaNQPEUEjXg06gsLR3/jDctROt44Z+iT3gFnkTQ0wXtwKJPdvbueBbg==", + "dev": true, + "dependencies": { + "@mswjs/interceptors": "^0.36.6", + "json-stringify-safe": "^5.0.1", + "propagate": "^2.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "dependencies": { + "process-on-spawn": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true + }, "node_modules/normalize-package-data": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", @@ -8270,9 +9313,246 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/nyc": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-17.1.0.tgz", + "integrity": "sha512-U42vQ4czpKa0QdI1hu950XuNhYqgoM+ZF1HT+VuUHL9hPfDPVvNQyltmMqdE9bUHMVa+8yNbc3QKTj8zQhlVxQ==", + "dev": true, + "dependencies": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^3.3.0", + "get-package-type": "^0.1.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^6.0.2", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "yargs": "^15.0.2" + }, + "bin": { + "nyc": "bin/nyc.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nyc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/nyc/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/nyc/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/nyc/node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/nyc/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/nyc/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nyc/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/nyc/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, + "node_modules/nyc/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/oas-schemas": { "version": "2.0.0", - "resolved": "git+https://git@github.com/OAI/OpenAPI-Specification.git#69d8b7953c3259e243cf746388a0951b89649763", + "resolved": "git+https://git@github.com/OAI/OpenAPI-Specification.git#882d1caedb0bff825a1fd10728e7e3dc43912d37", + "integrity": "sha512-Hng5mVCTj5iqhlK6Aus0B/eS+O6fbayliHO0/p1+bI7aTpYorp11b9jlvGnXmdscrTPWY5AbvQmazCT4I/0Z+g==", + "license": "Apache-2.0", "dependencies": { "cheerio": "^1.0.0-rc.5", "highlight.js": "^11.10.0", @@ -8335,17 +9615,9 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object-treeify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/object-treeify/-/object-treeify-4.0.1.tgz", - "integrity": "sha512-Y6tg5rHfsefSkfKujv2SwHulInROy/rCL5F4w0QOWxut8AnxYxf0YmNhTh95Zfyxpsudo66uqkux0ACFnyMSgQ==", + "dev": true, "engines": { - "node": ">= 16" + "node": ">= 0.4" } }, "node_modules/object.assign": { @@ -8563,6 +9835,12 @@ "node": ">=0.10.0" } }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true + }, "node_modules/p-cancelable": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", @@ -8602,6 +9880,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -8641,6 +9931,21 @@ "node": ">= 14" } }, + "node_modules/package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -8769,6 +10074,15 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -8807,6 +10121,70 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", @@ -8862,6 +10240,18 @@ "node": ">=6.0.0" } }, + "node_modules/process-on-spawn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.1.0.tgz", + "integrity": "sha512-JOnOPQ/8TZgjs1JIH/m9ni7FfimjNa/PRx7y/Wb5qdItsnhO0jE4AT7fC0HjC28DUQWDr50dwSYZLdRMlqDq3Q==", + "dev": true, + "dependencies": { + "fromentries": "^1.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -8870,6 +10260,15 @@ "node": ">=0.4.0" } }, + "node_modules/propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -9269,6 +10668,18 @@ "jsesc": "bin/jsesc" } }, + "node_modules/release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==", + "dev": true, + "dependencies": { + "es6-error": "^4.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -9277,6 +10688,12 @@ "node": ">=0.10.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -9625,6 +11042,12 @@ "node": ">= 0.8.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -9789,6 +11212,45 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/sinon": { + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.2.tgz", + "integrity": "sha512-euuToqM+PjO4UgXeLETsfQiuoyPXlqFezr6YZDFwHR3t4qaX0fZUe1MfPMznTL5f8BWrVS89KduLdMUsxFCO6g==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.2", + "@sinonjs/samsam": "^8.0.1", + "diff": "^7.0.0", + "nise": "^6.1.1", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -9907,11 +11369,47 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "optional": true, + "devOptional": true, "engines": { "node": ">=0.10.0" } }, + "node_modules/spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "dependencies": { + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/spawn-wrap/node_modules/foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/spawn-wrap/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", @@ -9978,6 +11476,12 @@ "bare-events": "^2.2.0" } }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -10164,6 +11668,63 @@ "streamx": "^2.15.0" } }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/text-decoder": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.1.tgz", @@ -10419,6 +11980,15 @@ "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==" }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, "node_modules/typescript": { "version": "5.6.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", @@ -10497,6 +12067,36 @@ "node": ">= 0.8" } }, + "node_modules/update-browserslist-db": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/upper-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz", @@ -10605,6 +12205,21 @@ "node": ">=18" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/which-boxed-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", @@ -10621,6 +12236,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true + }, "node_modules/which-typed-array": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", @@ -10691,6 +12312,24 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "node_modules/ws": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", @@ -10719,6 +12358,12 @@ "node": ">=10" } }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, "node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", diff --git a/package.json b/package.json index 91be07b9..ab373148 100644 --- a/package.json +++ b/package.json @@ -7,23 +7,6 @@ "bump": "./bin/run.js" }, "bugs": "https://github.com/bump-sh/cli/issues", - "dependencies": { - "@apidevtools/json-schema-ref-parser": "^11.7.2", - "@asyncapi/specs": "^6.8.0", - "@clack/prompts": "^0.7.0", - "@oclif/core": "^4", - "@oclif/plugin-help": "^6", - "@oclif/plugin-warn-if-update-available": "^3.1.20", - "@stoplight/yaml": "^4.3.0", - "axios": "^1.7.7", - "chalk": "^5.3.0", - "debug": "^4.3.7", - "jsonpath": "^1.1.1", - "mergician": "^2.0.2", - "oas-schemas": "git+https://git@github.com/OAI/OpenAPI-Specification.git#3.1.1", - "object-treeify": "^4.0.1", - "open": "^10.1.0" - }, "devDependencies": { "@oclif/prettier-config": "^0.2.1", "@oclif/test": "^4", @@ -32,16 +15,21 @@ "@types/jsonpath": "^0.2.4", "@types/mocha": "^10", "@types/node": "^18", + "@types/sinon": "^17.0.3", "@typescript-eslint/eslint-plugin": "^8.13.0", - "chai": "^4", + "chai": "^4.5.0", "eslint": "^8", "eslint-config-oclif": "^5", "eslint-config-oclif-typescript": "^3", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.1", "mocha": "^10", + "mock-stdin": "^1.0.0", + "nock": "^14.0.0-beta.16", + "nyc": "^17.1.0", "oclif": "^4", "shx": "^0.3.3", + "sinon": "^19.0.2", "ts-node": "^10", "typescript": "^5" }, @@ -90,11 +78,30 @@ "fmt": "eslint --fix . --ext .ts", "postpack": "shx rm -f oclif.manifest.json", "posttest": "npm run lint", - "prepack": "oclif manifest && oclif readme", - "pretest": "npm run clean && npm run build && npm run lint", + "prepack": "npm run clean && npm run build && oclif manifest && oclif readme", + "pretest": "npm run clean && npm run build", "release": "np --no-release-draft", "test": "mocha --forbid-only \"test/**/*.test.ts\"", + "test-coverage": "nyc npm run test", + "test-integration": "node ./test/integration.cjs", "version": "oclif readme && git add README.md" }, - "types": "dist/index.d.ts" + "types": "dist/index.d.ts", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^11.7.2", + "@asyncapi/specs": "^6.8.0", + "@clack/prompts": "^0.7.0", + "@oclif/core": "^4", + "@oclif/plugin-help": "^6", + "@oclif/plugin-warn-if-update-available": "^3.1.20", + "@stoplight/yaml": "^4.3.0", + "async-mutex": "^0.5.0", + "axios": "^1.7.7", + "chalk": "^5.3.0", + "debug": "^4.3.7", + "jsonpath": "^1.1.1", + "mergician": "^2.0.2", + "oas-schemas": "git+https://git@github.com/OAI/OpenAPI-Specification.git#882d1caedb0bff825a1fd10728e7e3dc43912d37", + "open": "^10.1.0" + } } diff --git a/src/api/error.ts b/src/api/error.ts index 1bc465af..a0048e9b 100644 --- a/src/api/error.ts +++ b/src/api/error.ts @@ -1,54 +1,96 @@ -import { CLIError } from '@oclif/errors'; -import chalk from 'chalk'; -import { AxiosError } from 'axios'; -import d from 'debug'; +import {CLIError} from '@oclif/core/errors' +import {AxiosError} from 'axios' +import chalk from 'chalk' +import d from 'debug' -import { InvalidDefinitionError } from './models'; +import {InvalidDefinitionError} from './models.js' -type MessagesAndExitCode = [string[], number]; -const debug = d('bump-cli:api-client'); +type MessagesAndExitCode = [string[], number] +const debug = d('bump-cli:api-client') export default class APIError extends CLIError { - http: AxiosError; - exitCode: number; - status?: number; - - constructor(httpError: AxiosError, info: string[] = [], exit = 100) { - const status = httpError?.response?.status; - debug(httpError); - - switch (httpError?.response?.status) { - case 422: - [info, exit] = APIError.invalidDefinition( - httpError.response.data as InvalidDefinitionError, - ); - break; - case 401: - [info, exit] = APIError.unauthenticated(); - break; - case 404: - case 400: - [info, exit] = APIError.notFound(httpError.response.data as Error); - break; + constructor(httpError?: AxiosError | undefined, info: string[] = [], exit = 100) { + const status = httpError?.response?.status + debug(httpError) + + if (httpError) { + switch (status) { + case 422: { + ;[info, exit] = APIError.invalidDefinition(httpError.response?.data as InvalidDefinitionError) + break + } + + case 401: { + ;[info, exit] = APIError.unauthenticated() + break + } + + case 404: + case 400: { + ;[info, exit] = APIError.notFound(httpError.response?.data as Error) + break + } + } + + if (info.length > 0) { + super(info.join('\n'), {exit}) + } else { + super(`Unhandled API error (status: ${status}) (error: ${httpError})`, {exit}) + } + } else { + super('Unhandled API error', {exit}) + } + } + + static humanAttributeError(attribute: string, messages: unknown): string[] { + let info: string[] = [] + + if (Array.isArray(messages)) { + const allMessages = (messages as unknown[]) + .map((message, idx) => { + if (message instanceof Object) { + return this.humanAttributeError(idx.toString(), message) + } + + return message + }) + .join(', ') + info.push(`${chalk.underline(attribute)} ${allMessages}`) + } else if (messages instanceof Object) { + for (const [child, childMessages] of Object.entries(messages)) { + const childErrors = this.humanAttributeError(`${attribute}.${child}`, childMessages) + info = [...info, ...childErrors] + } + } else if (messages) { + info.push(`${chalk.underline(attribute)} ${messages}`) } - if (info.length) { - super(info.join('\n')); + return info + } + + static invalidDefinition(error: InvalidDefinitionError): MessagesAndExitCode { + let info: string[] = [] + const genericMessage = error.message || 'Invalid definition file' + const exit = 122 + + if (error && 'errors' in error) { + for (const [attr, message] of Object.entries(error.errors)) { + const humanErrors = APIError.humanAttributeError(attr, message) + info = [...info, ...humanErrors] + } } else { - super(`Unhandled API error (status: ${status}) (error: ${httpError})`); + info.push(genericMessage) } - this.exitCode = exit; - this.http = httpError; + return [info, exit] } static is(error: Error): error is APIError { - return error instanceof CLIError && 'http' in error; + return error instanceof CLIError && 'http' in error } static notFound(error: Error): MessagesAndExitCode { - const genericMessage = - error.message || "It seems the documentation provided doesn't exist."; + const genericMessage = error.message || "It seems the documentation provided doesn't exist." return [ [ @@ -60,59 +102,13 @@ export default class APIError extends CLIError { )} or ${chalk.dim('--hub')} flags`, ], 104, - ]; - } - - static invalidDefinition(error: InvalidDefinitionError): MessagesAndExitCode { - let info: string[] = []; - const genericMessage = error.message || 'Invalid definition file'; - const exit = 122; - - if (error && 'errors' in error) { - for (const [attr, message] of Object.entries(error.errors)) { - info = info.concat(APIError.humanAttributeError(attr, message)); - } - } else { - info.push(genericMessage); - } - - return [info, exit]; - } - - static humanAttributeError(attribute: string, messages: unknown): string[] { - let info: string[] = []; - - if (messages instanceof Array) { - const allMessages = (messages as unknown[]) - .map((message, idx) => { - if (message instanceof Object) { - return this.humanAttributeError(idx.toString(), message); - } else { - return message; - } - }) - .join(', '); - info.push(`${chalk.underline(attribute)} ${allMessages}`); - } else if (messages instanceof Object) { - for (const [child, child_messages] of Object.entries(messages)) { - info = info.concat( - this.humanAttributeError(`${attribute}.${child}`, child_messages), - ); - } - } else if (messages) { - info.push(`${chalk.underline(attribute)} ${messages}`); - } - - return info; + ] } static unauthenticated(): MessagesAndExitCode { return [ - [ - 'You are not allowed to deploy to this documentation.', - 'please check your --token flag or BUMP_TOKEN variable', - ], + ['You are not allowed to deploy to this documentation.', 'please check your --token flag or BUMP_TOKEN variable'], 101, - ]; + ] } } diff --git a/src/api/index.ts b/src/api/index.ts index e7ad93af..f8ddb546 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,104 +1,80 @@ -import * as Config from '@oclif/config'; -import axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios'; +import {Config} from '@oclif/core' +import axios, {AxiosError, AxiosInstance, AxiosResponse} from 'axios' +import APIError from './error.js' import { + DiffRequest, + DiffResponse, PingResponse, PreviewRequest, PreviewResponse, VersionRequest, VersionResponse, - DiffRequest, - DiffResponse, WithDiff, -} from './models'; -import { vars } from './vars'; -import APIError from './error'; +} from './models.js' +import {vars} from './vars.js' class BumpApi { - protected readonly client: AxiosInstance; + protected readonly client: AxiosInstance - // Check https://oclif.io/docs/config for details about Config.IConfig - public constructor(protected config: Config.IConfig) { - const baseURL = `${vars.apiUrl}${vars.apiBasePath}`; - const headers: { 'User-Agent': string; Authorization?: string } = { - 'User-Agent': vars.apiUserAgent(config.userAgent), - }; + public getDiff = (diffId: string, format: string): Promise> => + this.client.get(`/diffs/${diffId}`, { + params: {formats: [format]}, + }) - this.client = axios.create({ - baseURL, - headers, - }); + public getPing = (): Promise> => this.client.get('/ping') - this.initializeResponseInterceptor(); - } + public getVersion = (versionId: string, token: string): Promise> => + this.client.get(`/versions/${versionId}`, { + headers: this.authorizationHeader(token), + }) - public getPing = (): Promise> => { - return this.client.get('/ping'); - }; + public postDiff = (body: DiffRequest): Promise> => + this.client.post('/diffs', body) - public getVersion = ( - versionId: string, - token: string, - ): Promise> => { - return this.client.get(`/versions/${versionId}`, { - headers: this.authorizationHeader(token), - }); - }; - - public postPreview = ( - body?: PreviewRequest, - ): Promise> => { - return this.client.post('/previews', body); - }; - - public putPreview = ( - versionId: string, - body?: PreviewRequest, - ): Promise> => { - return this.client.put(`/previews/${versionId}`, body); - }; - - public postVersion = ( - body: VersionRequest, - token: string, - ): Promise> => { - return this.client.post('/versions', body, { + public postPreview = (body?: PreviewRequest): Promise> => + this.client.post('/previews', body) + + public postValidation = (body: VersionRequest, token: string): Promise> => + this.client.post('/validations', body, { headers: this.authorizationHeader(token), - }); - }; - - public postDiff = (body: DiffRequest): Promise> => { - return this.client.post('/diffs', body); - }; - - public getDiff = ( - diffId: string, - format: string, - ): Promise> => { - return this.client.get(`/diffs/${diffId}`, { - params: { formats: [format] }, - }); - }; - - public postValidation = ( - body: VersionRequest, - token: string, - ): Promise> => { - return this.client.post('/validations', body, { + }) + + public postVersion = (body: VersionRequest, token: string): Promise> => + this.client.post('/versions', body, { headers: this.authorizationHeader(token), - }); - }; + }) + + public putPreview = (versionId: string, body?: PreviewRequest): Promise> => + this.client.put(`/previews/${versionId}`, body) + + private authorizationHeader = (token: string) => ({ + Authorization: `Basic ${Buffer.from(token).toString('base64')}`, + }) + + private handleError = (error: AxiosError) => Promise.reject(new APIError(error)) private initializeResponseInterceptor = () => { - this.client.interceptors.response.use((data) => data, this.handleError); - }; + this.client.interceptors.response.use((data) => data, this.handleError) + } - private handleError = (error: AxiosError) => Promise.reject(new APIError(error)); + // Check https://oclif.io/docs/config for details about Config.IConfig + public constructor(protected config: Config) { + const baseURL = `${vars.apiUrl}${vars.apiBasePath}` + const headers: {Authorization?: string; 'User-Agent': string} = { + 'User-Agent': vars.apiUserAgent(config.userAgent), + } - private authorizationHeader = (token: string) => { - return { Authorization: `Basic ${Buffer.from(token).toString('base64')}` }; - }; + this.client = axios.create({ + baseURL, + headers, + }) + + this.initializeResponseInterceptor() + } } -export * from './models'; -export { BumpApi, APIError }; +export {default as APIError} from './error.js' +export {BumpApi} + +export * from './models.js' diff --git a/src/api/models.ts b/src/api/models.ts index 0ea82f91..73f4000c 100644 --- a/src/api/models.ts +++ b/src/api/models.ts @@ -3,78 +3,78 @@ The types defined here should align with the API definition */ export interface PingResponse { - pong?: string; + pong?: string } export interface PreviewResponse { - id: string; - expires_at?: string; - public_url?: string; + expires_at?: string + id: string + public_url?: string } export interface InvalidDefinitionError { - message?: string; - errors: { [keys: string]: unknown }; + errors: {[keys: string]: unknown} + message?: string } export interface PreviewRequest { - definition: string; - references?: Reference[]; + definition: string + references?: Reference[] } export interface Reference { - location?: string; - content?: string; + content?: string + location?: string } export interface VersionRequest { - documentation: string; - definition: string; - hub?: string; - documentation_name?: string; - auto_create_documentation?: boolean; - references?: Reference[]; - unpublished?: boolean; - previous_version_id?: string; - branch_name?: string; + auto_create_documentation?: boolean + branch_name?: string + definition: string + documentation: string + documentation_name?: string + hub?: string + previous_version_id?: string + references?: Reference[] + unpublished?: boolean } export interface VersionResponse { - id: string; - doc_public_url?: string; + doc_public_url?: string + id: string } export interface WithDiff { - diff_public_url?: string; - diff_summary?: string; - diff_markdown?: string; - diff_details?: DiffItem[]; - diff_breaking?: boolean; + diff_breaking?: boolean + diff_details?: DiffItem[] + diff_markdown?: string + diff_public_url?: string + diff_summary?: string } export interface DiffRequest { - definition: string; - references?: Reference[]; - previous_definition: string; - previous_references?: Reference[]; - expires_at?: string; + definition: string + expires_at?: string + previous_definition: string + previous_references?: Reference[] + references?: Reference[] } export interface DiffResponse { - id: string; - public_url?: string; - breaking?: boolean; - text?: string; - markdown?: string; - details?: DiffItem[]; - html?: string; + breaking?: boolean + details?: DiffItem[] + html?: string + id: string + markdown?: string + public_url?: string + text?: string } export interface DiffItem { - id: string; - name: string; - status: string; - type: string; - breaking: boolean; - children: DiffItem[]; + breaking: boolean + children: DiffItem[] + id: string + name: string + status: string + type: string } diff --git a/src/api/vars.ts b/src/api/vars.ts index 7506aaaa..4f8684f9 100644 --- a/src/api/vars.ts +++ b/src/api/vars.ts @@ -1,24 +1,24 @@ export class Vars { - get host(): string { - return this.envHost || 'bump.sh'; + get apiBasePath(): string { + return '/api/v1' } - get envHost(): string | undefined { - return process.env.BUMP_HOST; + get apiUrl(): string { + return this.host.startsWith('http') ? this.host : `https://${this.host}` } - apiUserAgent(base: string): string { - const content = [base, process.env.BUMP_USER_AGENT].filter(Boolean); - return content.join(' '); + get envHost(): string | undefined { + return process.env.BUMP_HOST } - get apiUrl(): string { - return this.host.startsWith('http') ? this.host : `https://${this.host}`; + get host(): string { + return this.envHost || 'bump.sh' } - get apiBasePath(): string { - return '/api/v1'; + apiUserAgent(base: string): string { + const content = [base, process.env.BUMP_USER_AGENT].filter(Boolean) + return content.join(' ') } } -export const vars = new Vars(); +export const vars = new Vars() diff --git a/src/args.ts b/src/args.ts index 6acfa6e0..75eac62a 100644 --- a/src/args.ts +++ b/src/args.ts @@ -1,19 +1,21 @@ -const fileArg = { - name: 'FILE', - required: true, +import {Args} from '@oclif/core' + +const fileArg = Args.string({ description: 'Path or URL to your API documentation file. OpenAPI (2.0 to 3.1.0) and AsyncAPI (2.x) specifications are currently supported.\nPath can also be a directory when deploying to a Hub.', -}; + name: 'FILE', + required: true, +}) -const otherFileArg = { - name: 'FILE2', +const otherFileArg = Args.string({ description: 'Path or URL to a second API documentation file to compute its diff', -}; + name: 'FILE2', +}) -const overlayFileArg = { +const overlayFileArg = Args.string({ + description: 'Path or URL to an overlay file', name: 'OVERLAY_FILE', required: true, - description: 'Path or URL to an overlay file', -}; +}) -export { fileArg, otherFileArg, overlayFileArg }; +export {fileArg, otherFileArg, overlayFileArg} diff --git a/src/base-command.ts b/src/base-command.ts new file mode 100644 index 00000000..9a46c2e9 --- /dev/null +++ b/src/base-command.ts @@ -0,0 +1,21 @@ +import {Command} from '@oclif/core' +import debug from 'debug' + +import {BumpApi} from './api/index.js' + +/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ +export abstract class BaseCommand extends Command { + protected _bump!: BumpApi + + protected get bump(): BumpApi { + if (!this._bump) this._bump = new BumpApi(this.config) + return this._bump + } + + // Function signature type taken from @types/debug + // Debugger(formatter: any, ...args: any[]): void; + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + protected d(formatter: any, ...args: any[]): void { + return debug(`bump-cli:command:${this.constructor.name.toLowerCase()}`)(formatter, ...args) + } +} diff --git a/src/cli/index.ts b/src/cli/index.ts deleted file mode 100644 index 736ff916..00000000 --- a/src/cli/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { CliUx } from '@oclif/core'; - -import success from './styled/success'; - -type Levels = 'info' | 'debug'; - -if (process.env.BUMP_LOG_LEVEL) { - const logLevel: string = process.env.BUMP_LOG_LEVEL; - CliUx.config['outputLevel'] = logLevel as Levels; -} -const cli = { - ...CliUx.ux, - get styledSuccess(): typeof success { - return success; - }, -}; - -export { cli }; diff --git a/src/cli/styled/success.ts b/src/cli/styled/success.ts deleted file mode 100644 index 994d6ef5..00000000 --- a/src/cli/styled/success.ts +++ /dev/null @@ -1,8 +0,0 @@ -import chalk from 'chalk'; - -export default function styledSuccess(message: string): void { - const lines = message.split('\n'); - for (let i = 0; i < lines.length; i++) { - process.stdout.write(chalk.green(`* ${lines[i]}\n`)); - } -} diff --git a/src/command.ts b/src/command.ts deleted file mode 100644 index 251c86c0..00000000 --- a/src/command.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Command as Base } from '@oclif/command'; -import debug from 'debug'; - -import { BumpApi, APIError } from './api'; -import pjson from '../package.json'; - -export default abstract class Command extends Base { - private base = `${pjson.name}@${pjson.version}`; - _bump!: BumpApi; - - get bump(): BumpApi { - if (!this._bump) this._bump = new BumpApi(this.config); - return this._bump; - } - - async catch(error?: Error): Promise { - if (error && APIError.is(error)) { - this.error(error.message, { exit: error.exitCode }); - } - - throw error; - } - - // Function signature type taken from @types/debug - // Debugger(formatter: any, ...args: any[]): void; - /* eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any */ - d(formatter: any, ...args: any[]): void { - return debug(`bump-cli:command:${this.constructor.name.toLowerCase()}`)( - formatter, - ...args, - ); - } -} diff --git a/src/commands/deploy.ts b/src/commands/deploy.ts index 566136de..062e2919 100644 --- a/src/commands/deploy.ts +++ b/src/commands/deploy.ts @@ -1,21 +1,21 @@ -import chalk from 'chalk'; -import { RequiredFlagError } from '@oclif/parser/lib/errors'; -import { CLIError } from '@oclif/errors'; +import {ux} from '@oclif/core' +import {CLIError} from '@oclif/core/errors' +import chalk from 'chalk' -import Command from '../command'; -import * as flagsBuilder from '../flags'; -import { DefinitionDirectory } from '../core/definition_directory'; -import { Deploy as CoreDeploy } from '../core/deploy'; -import { confirm as promptConfirm } from '../core/utils/prompts'; -import { isDir } from '../core/utils/file'; -import { fileArg } from '../args'; -import { cli } from '../cli'; -import { VersionResponse } from '../api/models'; -import { API } from '../definition'; +import {VersionResponse} from '../api/models.js' +import {fileArg} from '../args.js' +import {BaseCommand} from '../base-command.js' +import {DefinitionDirectory} from '../core/definition-directory.js' +import {Deploy as CoreDeploy} from '../core/deploy.js' +import {isDir} from '../core/utils/file.js' +import {confirm as promptConfirm} from '../core/utils/prompts.js' +import {API} from '../definition.js' +import * as flagsBuilder from '../flags.js' -export default class Deploy extends Command { - static description = - 'Create a new version of your documentation from the given file or URL.'; +export default class Deploy extends BaseCommand { + static args = {file: fileArg} + + static description = 'Create a new version of your documentation from the given file or URL.' static examples = [ `Deploy a new version of ${chalk.underline('an existing documentation')} @@ -24,21 +24,15 @@ ${chalk.dim('$ bump deploy FILE --doc --token --hub --token ', -)} +${chalk.dim('$ bump deploy FILE --doc --hub --token ')} * Let's deploy a new documentation version on Bump... done * Your new documentation version will soon be ready `, `Deploy a whole directory of ${chalk.underline('API definitions files to a hub')} -${chalk.dim( - '$ bump deploy DIR --filename-pattern *-{slug}-api --hub --token ', -)} +${chalk.dim('$ bump deploy DIR --filename-pattern *-{slug}-api --hub --token ')} We've found 2 valid API definitions to deploy └─ DIR └─ source-my-service-api.yml (OpenAPI spec version 3.1.0) @@ -58,102 +52,24 @@ ${chalk.dim('$ bump deploy FILE --dry-run --doc --token { - const { args, flags } = this.parse(Deploy); - - const [ - dryRun, - documentation, - token, - hub, - autoCreate, - interactive, - filenamePattern, - documentationName, - branch, - overlay, - ] = [ - flags['dry-run'], - flags.doc, - /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ - flags.token!, - flags.hub, - flags['auto-create'], - flags.interactive, - /* Flags.filenamePattern has a default value, so it's always defined. But - * oclif types doesn't detect it */ - /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ - flags['filename-pattern']!, - flags['doc-name'], - flags.branch, - flags.overlay, - ]; - - if (isDir(args.FILE)) { - if (hub) { - await this.deployDirectory( - args.FILE, - dryRun, - token, - hub, - autoCreate, - interactive, - filenamePattern, - documentationName, - branch, - ); - } else { - throw new RequiredFlagError({ flag: Deploy.flags.hub, parse: {} }); - } - } else { - if (documentation) { - const api = await API.load(args.FILE); - this.d(`${args.FILE} looks like an ${api.specName} spec version ${api.version}`); - - await this.deploySingleFile( - api, - dryRun, - documentation, - token, - hub, - autoCreate, - documentationName, - branch, - overlay, - ); - } else { - throw new RequiredFlagError({ flag: Deploy.flags.doc, parse: {} }); - } - } - - return; + token: flagsBuilder.token(), } - private async deployDirectory( + protected async deployDirectory( dir: string, dryRun: boolean, token: string, @@ -164,38 +80,45 @@ ${chalk.dim('$ bump deploy FILE --dry-run --doc --token { - const definitionDirectory = new DefinitionDirectory(dir, filenamePattern); - const action = dryRun ? 'validate' : 'deploy'; + const definitionDirectory = new DefinitionDirectory(dir, filenamePattern) - await definitionDirectory.readDefinitions(); + await definitionDirectory.readDefinitions() - // In “interactive” mode we ask the user if he wants to add more - // definitions to deploy. He is thus presented a form to select - // some files from the target directory. - if (interactive) { - let confirm = true; - if (definitionDirectory.definitionsExists()) { - await promptConfirm('Do you want to add more files to deploy?').catch(() => { - confirm = false; - }); - } - if (confirm) { - await definitionDirectory.interactiveSelection(); + await ux.action.pauseAsync(async () => { + definitionDirectory.stdoutDefinitions() + + // In “interactive” mode we ask the user if he wants to add more + // definitions to deploy. He is thus presented a form to select + // some files from the target directory. + if (interactive) { + let confirm = true + if (definitionDirectory.definitionsExists()) { + await promptConfirm('Do you want to add more files to deploy?').catch(() => { + confirm = false + }) + } + + if (confirm) { + await ux.action.pauseAsync(async () => { + await definitionDirectory.interactiveSelection() + }) + } } - } + }) if (definitionDirectory.definitionsExists()) { - cli.info( - chalk.underline( - `Let's ${action} those documentations to your ${hub} hub on Bump.sh`, - ), - ); + await ux.action.pauseAsync(async () => { + definitionDirectory.stdoutDefinitions() - await definitionDirectory.sequentialMap(async (definition) => { if (interactive) { - await definitionDirectory.renameToConvention(definition); + await definitionDirectory.sequentialMap(async (definition) => { + await definitionDirectory.renameToConvention(definition) + }) } + }) + ux.action.status = `...to your ${hub} hub on Bump.sh` + await definitionDirectory.sequentialMap(async (definition) => { await this.deploySingleFile( definition.definition, dryRun, @@ -205,10 +128,8 @@ ${chalk.dim('$ bump deploy FILE --dry-run --doc --token --token --token { - const action = dryRun ? 'validate' : 'deploy'; - cli.action.start( - `Let's ${action} a new version to your ${documentation} documentation on Bump.sh`, - ); + ux.action.status = `...a new version to your ${documentation} documentation` - const response: VersionResponse | undefined = await new CoreDeploy(this.config).run( + const response: VersionResponse | undefined = await new CoreDeploy(this.bump).run( api, dryRun, documentation, @@ -248,22 +164,94 @@ ${chalk.dim('$ bump deploy FILE --dry-run --doc --token { + const {args, flags} = await this.parse(Deploy) + + const [ + dryRun, + documentation, + token, + hub, + autoCreate, + interactive, + filenamePattern, + documentationName, + branch, + overlay, + ] = [ + flags['dry-run'], + flags.doc, + + flags.token!, + flags.hub, + flags['auto-create'], + flags.interactive, + /* Flags.filenamePattern has a default value, so it's always defined. But + * oclif types doesn't detect it */ + + flags['filename-pattern']!, + flags['doc-name'], + flags.branch, + flags.overlay, + ] + + const action = dryRun ? 'validate' : 'deploy' + ux.action.start(`Let's ${action} on Bump.sh`) + + if (isDir(args.file)) { + if (hub) { + await this.deployDirectory( + args.file, + dryRun, + token, + hub, + autoCreate, + interactive, + filenamePattern, + documentationName, + branch, + ) } else { - await cli.warn('Your documentation has not changed'); + throw new CLIError('Missing required flag --hub when deploying an entire directory') } - } + } else if (documentation) { + const api = await API.load(args.file) + this.d(`${args.file} looks like an ${api.specName} spec version ${api.version}`) - cli.action.stop(); + await this.deploySingleFile( + api, + dryRun, + documentation, + token, + hub, + autoCreate, + documentationName, + branch, + overlay, + ) + } else { + throw new CLIError('Missing required flag --doc=') + } - return; + ux.action.stop() } } diff --git a/src/commands/diff.ts b/src/commands/diff.ts index 544d2f4b..a43351bc 100644 --- a/src/commands/diff.ts +++ b/src/commands/diff.ts @@ -1,17 +1,21 @@ -import { CLIError } from '@oclif/errors'; - -import Command from '../command'; -import * as flagsBuilder from '../flags'; -import { Diff as CoreDiff } from '../core/diff'; -import { fileArg, otherFileArg } from '../args'; -import { cli } from '../cli'; -import { DiffResponse } from '../api/models'; +import {ux} from '@oclif/core' +import {CLIError} from '@oclif/core/errors' + +import {DiffResponse} from '../api/models.js' +import {fileArg, otherFileArg} from '../args.js' +import {BaseCommand} from '../base-command.js' +import {Diff as CoreDiff} from '../core/diff.js' +import * as flagsBuilder from '../flags.js' + +export default class Diff extends BaseCommand { + static override args = { + file: fileArg, + otherFile: otherFileArg, + } -export default class Diff extends Command { - static description = - 'Get a comparison diff with your documentation from the given file or URL.'; + static override description = 'Get a comparison diff with your documentation from the given file or URL.' - static examples = [ + static override examples = [ `Compare a potential new version with the currently published one: $ bump diff FILE --doc --token @@ -41,21 +45,50 @@ export default class Diff extends Command { Updated: POST /versions Body attribute added: previous_version_id `, - ]; + ] - static flags = { - help: flagsBuilder.help({ char: 'h' }), - doc: flagsBuilder.doc(), - hub: flagsBuilder.hub(), + static override flags = { + // TODO: re-enable --help flag plugin if available for Oclif v4 branch: flagsBuilder.branch(), - token: flagsBuilder.token({ required: false }), - open: flagsBuilder.open({ description: 'Open the visual diff in your browser' }), + // help: flagsBuilder.help({ char: 'h' }), + doc: flagsBuilder.doc(), + expires: flagsBuilder.expires(), 'fail-on-breaking': flagsBuilder.failOnBreaking(), format: flagsBuilder.format(), - expires: flagsBuilder.expires(), - }; + hub: flagsBuilder.hub(), + open: flagsBuilder.open({description: 'Open the visual diff in your browser'}), + token: flagsBuilder.token({required: false}), + } + + async displayCompareResult( + result: DiffResponse, + format: string, + open: boolean, + failOnBreaking: boolean, + ): Promise { + if (format === 'text' && result.text) { + ux.stdout(result.text) + } else if (format === 'markdown' && result.markdown) { + ux.stdout(result.markdown) + } else if (format === 'json' && result.details) { + ux.stdout(JSON.stringify(result.details, null, 2)) + } else if (format === 'html' && result.html) { + ux.stdout(result.html) + } else { + ux.stdout('No structural changes detected.') + } - static args = [fileArg, otherFileArg]; + if (open && result.public_url) { + // TODO: find a way to use the `open` node module, for now it + // doesn't work for an obscure reason + // + // await cli.open(result.public_url); + } + + if (failOnBreaking && result.breaking) { + this.exit(1) + } + } /* Oclif doesn't type parsed args & flags correctly and especially @@ -64,86 +97,46 @@ export default class Diff extends Command { See https://github.com/oclif/oclif/issues/301 for details */ async run(): Promise { - const { args, flags } = this.parse(Diff); - /* Flags.format has a default value, so it's always defined. But - * oclif types doesn't detect it */ + const {args, flags} = await this.parse(Diff) const [documentation, hub, branch, token, format, expires] = [ flags.doc, flags.hub, flags.branch, flags.token, - /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ - flags.format!, + flags.format, flags.expires, - ]; - + ] if (format === 'text') { - if (args.FILE2) { - cli.action.start('* Comparing the two given definition files'); + if (args.otherFile) { + ux.action.start('* Comparing the two given definition files') } else { - cli.action.start( - '* Comparing the given definition file with the currently deployed one', - ); + ux.action.start('* Comparing the given definition file with the currently deployed one') } } - if (!args.FILE2 && (!documentation || !token)) { - throw new CLIError( - 'Please provide a second file argument or login with an existing token', - ); + if (!args.otherFile && (!documentation || !token)) { + throw new CLIError('Please provide a second file argument or login with an existing token') } - const diff: DiffResponse | undefined = await new CoreDiff(this.config).run( - args.FILE, - args.FILE2, + ux.action.status = '...diff on Bump.sh in progress' + + const diff: DiffResponse | undefined = await new CoreDiff(this.bump).run( + args.file, + args.otherFile, documentation, hub, branch, token, format, expires, - ); + ) - cli.action.stop(); + ux.action.stop() if (diff) { - await this.displayCompareResult( - diff, - format, - flags.open, - flags['fail-on-breaking'], - ); + await this.displayCompareResult(diff, format, flags.open, flags['fail-on-breaking']) } else { - await cli.log('No changes detected.'); - } - - return; - } - - async displayCompareResult( - result: DiffResponse, - format: string, - open: boolean, - failOnBreaking: boolean, - ): Promise { - if (format == 'text' && result.text) { - await cli.log(result.text); - } else if (format == 'markdown' && result.markdown) { - await cli.log(result.markdown); - } else if (format == 'json' && result.details) { - await cli.log(JSON.stringify(result.details, null, 2)); - } else if (format == 'html' && result.html) { - await cli.log(result.html); - } else { - await cli.log('No structural changes detected.'); - } - - if (open && result.public_url) { - await cli.open(result.public_url); - } - - if (failOnBreaking && result.breaking) { - this.exit(1); + ux.stdout('No changes detected.') } } } diff --git a/src/commands/overlay.ts b/src/commands/overlay.ts index bed7448a..db008f2c 100644 --- a/src/commands/overlay.ts +++ b/src/commands/overlay.ts @@ -1,19 +1,24 @@ -import chalk from 'chalk'; -import { mkdir, writeFile } from 'node:fs/promises'; -import { existsSync } from 'node:fs'; -import { dirname } from 'path'; +import {ux} from '@oclif/core' +import chalk from 'chalk' +import {existsSync} from 'node:fs' +import {mkdir, writeFile} from 'node:fs/promises' +import {dirname} from 'node:path' -import { API } from '../definition'; -import { confirm as promptConfirm } from '../core/utils/prompts'; -import Command from '../command'; -import * as flagsBuilder from '../flags'; -import { fileArg, overlayFileArg } from '../args'; -import { cli } from '../cli'; +import {fileArg, overlayFileArg} from '../args.js' +import {BaseCommand} from '../base-command.js' +import {confirm as promptConfirm} from '../core/utils/prompts.js' +import {API} from '../definition.js' +import * as flagsBuilder from '../flags.js' -export default class Overlay extends Command { - static description = 'Apply an OpenAPI specified overlay to your API definition.'; +export default class Overlay extends BaseCommand { + static override args = { + file: fileArg, + overlay: overlayFileArg, + } + + static override description = 'Apply an OpenAPI specified overlay to your API definition.' - static examples = [ + static override examples = [ `Apply the OVERLAY_FILE to the existing DEFINITION_FILE. The resulting definition is output on stdout meaning you can redirect it to a new file. @@ -21,45 +26,45 @@ file. ${chalk.dim('$ bump overlay DEFINITION_FILE OVERLAY_FILE > destination/file.json')} * Let's apply the overlay to the main definition... done `, - ]; + ] - static flags = { - help: flagsBuilder.help({ char: 'h' }), + static override flags = { + // TODO: re-enable --help flag plugin if available for Oclif v4 + // help: flagsBuilder.help({ char: 'h' }), out: flagsBuilder.out(), - }; + } - static args = [fileArg, overlayFileArg]; + public async run(): Promise { + const {args, flags} = await this.parse(Overlay) + const outputPath = flags.out - async run(): Promise { - const { args, flags } = this.parse(Overlay); - const outputPath = flags.out; + ux.action.start("* Let's apply the overlay to the main definition") - cli.action.start("* Let's apply the overlay to the main definition"); + ux.action.status = '...loading definition file' - const api = await API.load(args.FILE); + const api = await API.load(args.file) - await api.applyOverlay(args.OVERLAY_FILE); - const [overlayedDefinition] = api.extractDefinition(outputPath); + ux.action.status = '...applying overlay' - cli.action.stop(); + await api.applyOverlay(args.overlay) + const [overlayedDefinition] = api.extractDefinition(outputPath) + + ux.action.stop() if (outputPath) { - await mkdir(dirname(outputPath), { recursive: true }); - let confirm = true; + await mkdir(dirname(outputPath), {recursive: true}) + let confirm = true if (existsSync(outputPath)) { - await promptConfirm( - `Do you want to override the existing destination file? (${outputPath})`, - ).catch(() => { - confirm = false; - }); + await promptConfirm(`Do you want to override the existing destination file? (${outputPath})`).catch(() => { + confirm = false + }) } + if (confirm) { - await writeFile(outputPath, overlayedDefinition); + await writeFile(outputPath, overlayedDefinition) } } else { - cli.log(overlayedDefinition); + ux.stdout(overlayedDefinition) } - - return; } } diff --git a/src/commands/preview.ts b/src/commands/preview.ts index 87b64e38..55c6c8b4 100644 --- a/src/commands/preview.ts +++ b/src/commands/preview.ts @@ -1,105 +1,116 @@ -import { API } from '../definition'; -import Command from '../command'; -import * as flagsBuilder from '../flags'; -import { fileArg } from '../args'; -import { cli } from '../cli'; -import { PreviewResponse, PreviewRequest } from '../api/models'; +import {ux} from '@oclif/core' -import { watch } from 'fs'; -import { Mutex } from 'async-mutex'; +import {PreviewRequest, PreviewResponse} from '../api/models.js' +import {fileArg} from '../args.js' +import {BaseCommand} from '../base-command.js' +import {API} from '../definition.js' +import * as flagsBuilder from '../flags.js' +// import * as openBrowser from 'open'; -export default class Preview extends Command { - static description = 'Create a documentation preview from the given file or URL.'; +import {Mutex} from 'async-mutex' +import {watch} from 'node:fs' - static examples = [ - `$ bump preview FILE +export default class Preview extends BaseCommand { + static override args = { + file: fileArg, + } + + static override description = 'Create a documentation preview from the given file or URL.' + + static override examples = [ + `$ <%= config.bin %> <%= command.id %> FILE * Your preview is visible at: https://bump.sh/preview/45807371-9a32-48a7-b6e4-1cb7088b5b9b `, - ]; + ] - static flags = { - help: flagsBuilder.help({ char: 'h' }), + static override flags = { + // TODO: re-enable --help flag plugin if available for Oclif v4 + // help: flagsBuilder.help({ char: 'h' }), live: flagsBuilder.live({ description: 'Generate a preview each time you save the given file', }), open: flagsBuilder.open({ description: 'Open the generated preview URL in your browser', }), - }; + } + + public async run(): Promise { + const {args, flags} = await this.parse(Preview) - static args = [fileArg]; + ux.action.start("* Let's render a preview on Bump.sh") - async run(): Promise { - const { args, flags } = this.parse(Preview); - const currentPreview: PreviewResponse = await this.preview(args.FILE, flags.open); + const currentPreview: PreviewResponse = await this.preview(args.file, flags.open) if (flags.live) { - await this.waitForChanges(args.FILE, currentPreview); + await this.waitForChanges(args.file, currentPreview) } - return; + ux.action.stop() } - async preview( + private async preview( file: string, open = false, currentPreview: PreviewResponse | undefined = undefined, ): Promise { - const api = await API.load(file); - const [definition, references] = api.extractDefinition(); + const api = await API.load(file) + const [definition, references] = api.extractDefinition() - this.d(`${file} looks like an ${api.specName} spec version ${api.version}`); - - if (!currentPreview) { - cli.action.start("* Let's render a preview on Bump.sh"); - } + this.d(`${file} looks like an ${api.specName} spec version ${api.version}`) const request: PreviewRequest = { definition, references, - }; - const response: { data: PreviewResponse } = currentPreview + } + ux.action.status = '...in progress' + const response: {data: PreviewResponse} = currentPreview ? await this.bump.putPreview(currentPreview.id, request) - : await this.bump.postPreview(request); + : await this.bump.postPreview(request) if (!currentPreview) { - cli.action.stop(); - cli.styledSuccess( - `Your preview is visible at: ${response.data.public_url} (Expires at ${response.data.expires_at})`, - ); + ux.action.status = '...done' + ux.stdout( + ux.colorize( + 'green', + `Your preview is visible at: ${response.data.public_url} (Expires at ${response.data.expires_at})`, + ), + ) } if (open && response.data.public_url) { - await cli.open(response.data.public_url); + // TODO: find a way to use the `open` node module, for now it + // doesn't work for an obscure reason + // + // await openBrowser(response.data.public_url); } - return response.data; + return response.data } - async waitForChanges(file: string, preview: PreviewResponse): Promise { - const mutex = new Mutex(); - let currentPreview: PreviewResponse = preview; + private async waitForChanges(file: string, preview: PreviewResponse): Promise { + const mutex = new Mutex() + let currentPreview: PreviewResponse = preview - cli.action.start(`Waiting for changes on file ${file}...`); + ux.action.status = `Waiting for changes on file ${file}...` watch(file, async () => { if (!mutex.isLocked()) { - const release = await mutex.acquire(); + const release = await mutex.acquire() this.preview(file, false, currentPreview) .then((preview) => { - currentPreview = preview; - cli.action.start(`Waiting for changes on file ${file}`); + currentPreview = preview + ux.action.status = `Waiting for changes on file ${file}` }) - .catch((err) => { - this.warn(err); + .catch((error) => { + this.warn(error) }) .finally(() => { setTimeout(() => { - release(); - }, 1000); // Prevent previewing faster than once per second - }); + release() + }, 1000) // Prevent previewing faster than once per second + }) } - }); + }) } } diff --git a/src/core/definition-directory.ts b/src/core/definition-directory.ts new file mode 100644 index 00000000..876932f5 --- /dev/null +++ b/src/core/definition-directory.ts @@ -0,0 +1,194 @@ +import * as p from '@clack/prompts' +import {ux} from '@oclif/core' +import {CLIError, ExitError} from '@oclif/core/errors' +import chalk from 'chalk' +import debug from 'debug' +import {rename} from 'node:fs' +import {basename, dirname, extname, join, resolve} from 'node:path' + +import {confirm as promptConfirm} from '../core/utils/prompts.js' +import {API} from '../definition.js' +import {File} from './utils/file.js' + +export type DefinitionConfig = { + definition: API + file: string + slug: string +} + +export class DefinitionDirectory { + protected buildNewFilename: (slug: string) => string + protected readonly definitions: DefinitionConfig[] + + protected readonly filenamePattern: RegExp + protected readonly humanFilenamePattern: string + protected readonly path: string + + public constructor(directory: string, filenamePattern: string) { + this.path = resolve(directory) + this.definitions = [] + // // Transform basic patterns '*' or '{text}' into a real RegExp + this.filenamePattern = new RegExp('^' + filenamePattern.replace('*', '.*?').replace(/{.*?}/, '(?.+?)') + '$') + + this.buildNewFilename = (slug) => filenamePattern.replace('*', '').replace(/{.*?}/, slug) + + this.humanFilenamePattern = filenamePattern.replace(/{(.*?)}/, `${chalk.inverse('{$1}')}`) + } + + // Function signature type taken from @types/debug + // Debugger(formatter: any, ...args: any[]): void; + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + d(formatter: any, ...args: any[]): void { + return debug(`bump-cli:core:interactive`)(formatter, ...args) + } + + public definitionsExists(): boolean { + return this.definitions.length > 0 + } + + public async interactiveSelection(): Promise { + p.intro( + `This interactive form will help you rename your API contrat files to follow the expected naming convention.\n${chalk.gray( + '│ ', + )}Once finished, the selected files will be deployed to Bump.sh.\n${chalk.gray( + '│ ', + )}\n${chalk.gray('│ ')}File naming convention: ${this.humanFilenamePattern}${chalk.dim('.[json|yml|yaml]')}\n`, + ) + + const fileOptions = File.listInvalidConventionFiles(this.path, this.filenamePattern) + if (fileOptions.length === 0) { + throw new CLIError( + `No JSON or YAML files needing a rename were found in ${ + this.path + }.\nAre you sure you need the ${chalk.dim('--interactive')} flag?`, + ) + } + + let shouldContinue = true + + while (shouldContinue) { + const filePrompt = { + fileName: () => + p.select({ + message: `Which file do you want to deploy from ${chalk.dim(this.path)}?`, + options: fileOptions, + }), + } + const groupPrompt = { + /* Results type should be taken from the previous prompts + * defined with clack/prompt */ + slug: ({results}: {results: {fileName?: unknown}}) => + p.text({ + message: `What is the ${chalk.inverse( + 'documentation slug', + )} for this ${chalk.dim(results.fileName as string)} file?`, + }), + } + // eslint-disable-next-line no-await-in-loop + const prompt = await p.group( + { + ...filePrompt, + ...groupPrompt, + shouldContinue: () => p.confirm({message: 'Do you want to select another file?'}), + }, + { + onCancel() { + p.cancel('Deploy cancelled.') + throw new ExitError(1) + }, + }, + ) + + const file = join(this.path, prompt.fileName as string) + // eslint-disable-next-line no-await-in-loop + const definition = await API.load(file) + this.d(`${file} looks like an ${definition.specName} spec version ${definition.version}`) + + this.definitions.push({ + definition, + file, + slug: prompt.slug, + }) + shouldContinue = prompt.shouldContinue + } + + p.outro(`You're all set. Your deployments will start soon.`) + + return this.definitions + } + + public async map(callback: (definition: DefinitionConfig) => Promise): Promise { + return Promise.all(this.definitions.map((definition) => callback(definition))) + } + + public async readDefinitions(): Promise { + for await (const {filename, value} of File.listValidConventionFiles(this.path, this.filenamePattern)) { + const file = join(this.path, value) + /* We already check the filenamePattern match inside the + `File.listValidConventionFiles` method so we are sure the group + matched exists. */ + + const slug = filename.match(this.filenamePattern)!.groups!.slug! + const definition = await API.load(file) + this.definitions.push({ + definition, + file, + slug, + }) + } + + return this.definitions + } + + public async renameToConvention(documentation: DefinitionConfig): Promise { + const {file, slug} = documentation + if (this.filenamePattern.test(basename(file, extname(file)))) return + + // Default convention is defined in the flags.ts file for the + // 'filenamePattern' flag. + const newFilename = this.buildNewFilename(slug) + const newFile = `${dirname(file)}/${newFilename}${extname(file)}` + + let confirm = true + await promptConfirm(`Do you want to rename ${file} to ${newFile} (for later deployments)?`).catch(() => { + confirm = false + }) + + if (confirm) { + await rename(file, newFile, (err) => { + if (err) throw err + ux.stdout(ux.colorize('green', `Renamed ${file} to ${newFile}.`)) + }) + } + } + + public async sequentialMap(callback: (definition: DefinitionConfig) => Promise): Promise { + for (const definition of this.definitions) { + // We explicitly need a sequential run of promises, so the await + // in loop is needed. + /* eslint-disable-next-line no-await-in-loop */ + await callback(definition) + } + + return this.definitions + } + + public stdoutDefinitions(): void { + if (this.definitions.length > 0) { + ux.stdout(chalk.underline(`We've found ${this.definitions.length} valid API definitions to deploy`)) + ux.stdout(`└─ ${this.path}`) + let iterations = this.definitions.length + for (const {definition, file} of this.definitions) { + const filename: string = `${basename(file)} (${definition.specName} spec version ${definition.version})` + iterations -= 1 + if (iterations) { + ux.stdout(` ├─ ${filename}`) + } else { + ux.stdout(` └─ ${filename}`) + } + } + + ux.stdout('') + } + } +} diff --git a/src/core/definition_directory.ts b/src/core/definition_directory.ts deleted file mode 100644 index c2378eee..00000000 --- a/src/core/definition_directory.ts +++ /dev/null @@ -1,204 +0,0 @@ -import chalk from 'chalk'; -import debug from 'debug'; -import { rename } from 'fs'; -import { join, extname, dirname, resolve, basename } from 'node:path'; -import * as p from '@clack/prompts'; -import { CLIError } from '@oclif/errors'; - -import { cli } from '../cli'; -import { API } from '../definition'; -import { File } from './utils/file'; -import { confirm as promptConfirm } from '../core/utils/prompts'; - -export type DefinitionConfig = { - definition: API; - file: string; - slug: string; -}; - -export class DefinitionDirectory { - protected readonly path: string; - protected readonly definitions: DefinitionConfig[]; - - protected readonly filenamePattern: RegExp; - protected readonly humanFilenamePattern: string; - protected buildNewFilename: (slug: string) => string; - - public constructor(directory: string, filenamePattern: string) { - this.path = resolve(directory); - this.definitions = []; - // Transform basic patterns '*' or '{text}' into a real RegExp - this.filenamePattern = new RegExp( - '^' + filenamePattern.replace('*', '.*?').replace(/{.*?}/, '(?.+?)') + '$', - ); - - this.buildNewFilename = (slug) => - filenamePattern.replace('*', '').replace(/{.*?}/, slug); - - this.humanFilenamePattern = filenamePattern.replace( - /{(.*?)}/, - `${chalk.inverse('{$1}')}`, - ); - } - - public async readDefinitions(): Promise { - for await (const { value, filename } of File.listValidConventionFiles( - this.path, - this.filenamePattern, - )) { - const file = join(this.path, value); - /* We already check the filenamePattern match inside the - `File.listValidConventionFiles` method so we are sure the group - matched exists. */ - /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ - const slug = filename.match(this.filenamePattern)!.groups!.slug!; - const definition = await API.load(file); - this.definitions.push({ - file, - definition, - slug, - }); - } - - if (this.definitions.length) { - cli.info( - chalk.underline( - `We've found ${this.definitions.length} valid API definitions to deploy`, - ), - ); - const subtree = cli.tree(); - this.definitions.forEach(({ file, definition }) => - subtree.insert( - `${basename(file)} (${definition.specName} spec version ${definition.version})`, - ), - ); - const tree = cli.tree(); - tree.insert(this.path, subtree); - - tree.display(); - cli.info(''); - } - - return this.definitions; - } - - public definitionsExists(): boolean { - return !!this.definitions.length; - } - - public async sequentialMap( - callback: (definition: DefinitionConfig) => Promise, - ): Promise { - for (const definition of this.definitions) { - await callback(definition); - } - - return; - } - - public async renameToConvention(documentation: DefinitionConfig): Promise { - const { file, slug } = documentation; - if (basename(file, extname(file)).match(this.filenamePattern)) return; - - // Default convention is defined in the flags.ts file for the - // 'filenamePattern' flag. - const newFilename = this.buildNewFilename(slug); - const newFile = `${dirname(file)}/${newFilename}${extname(file)}`; - - let confirm = true; - await promptConfirm( - `Do you want to rename ${file} to ${newFile} (for later deployments)?`, - ).catch(() => { - confirm = false; - }); - - if (confirm) { - await rename(file, newFile, (err) => { - if (err) throw err; - cli.styledSuccess(`Renamed ${file} to ${newFile}.`); - }); - } - - return; - } - - public async interactiveSelection(): Promise { - p.intro( - `This interactive form will help you rename your API contrat files to follow the expected naming convention.\n${chalk.gray( - '│ ', - )}Once finished, the selected files will be deployed to Bump.sh.\n${chalk.gray( - '│ ', - )}\n${chalk.gray('│ ')}File naming convention: ${ - this.humanFilenamePattern - }${chalk.dim('.[json|yml|yaml]')}\n`, - ); - - const fileOptions = File.listInvalidConventionFiles(this.path, this.filenamePattern); - if (!fileOptions.length) { - throw new CLIError( - `No JSON or YAML files needing a rename were found in ${ - this.path - }.\nAre you sure you need the ${chalk.dim('--interactive')} flag?`, - ); - } - let shouldContinue = true; - - while (shouldContinue) { - const filePrompt = { - fileName: () => - p.select({ - message: `Which file do you want to deploy from ${chalk.dim(this.path)}?`, - options: fileOptions, - }), - }; - const groupPrompt = { - /* Results type should be taken from the previous prompts - * defined with clack/prompt */ - slug: ({ results }: { results: { fileName?: unknown } }) => - p.text({ - message: `What is the ${chalk.inverse( - 'documentation slug', - )} for this ${chalk.dim(results.fileName as string)} file?`, - }), - }; - const prompt = await p.group( - { - ...filePrompt, - ...groupPrompt, - shouldContinue: () => - p.confirm({ message: 'Do you want to select another file?' }), - }, - { - onCancel: () => { - p.cancel('Deploy cancelled.'); - process.exit(0); - }, - }, - ); - - const file = join(this.path, prompt.fileName as string); - const definition = await API.load(file); - this.d( - `${file} looks like an ${definition.specName} spec version ${definition.version}`, - ); - - this.definitions.push({ - file, - definition, - slug: prompt.slug, - }); - shouldContinue = prompt.shouldContinue; - } - - p.outro(`You're all set. Your deployments will start soon.`); - - return this.definitions; - } - - // Function signature type taken from @types/debug - // Debugger(formatter: any, ...args: any[]): void; - /* eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any */ - d(formatter: any, ...args: any[]): void { - return debug(`bump-cli:core:interactive`)(formatter, ...args); - } -} diff --git a/src/core/deploy.ts b/src/core/deploy.ts index db39a559..25b1c045 100644 --- a/src/core/deploy.ts +++ b/src/core/deploy.ts @@ -1,16 +1,42 @@ -import * as Config from '@oclif/config'; -import debug from 'debug'; +import debug from 'debug' -import { API } from '../definition'; -import { BumpApi } from '../api'; -import { VersionRequest, VersionResponse } from '../api/models'; +import {BumpApi} from '../api/index.js' +import {VersionRequest, VersionResponse} from '../api/models.js' +import {API} from '../definition.js' export class Deploy { - _bump!: BumpApi; - _config: Config.IConfig; + private _bump!: BumpApi - public constructor(config: Config.IConfig) { - this._config = config; + public constructor(bumpClient: BumpApi) { + this._bump = bumpClient + } + + protected async createVersion(request: VersionRequest, token: string): Promise { + const response = await this._bump.postVersion(request, token) + let version: VersionResponse | undefined + + switch (response.status) { + case 204: { + break + } + + case 201: { + version = response.data ?? {doc_public_url: 'https://bump.sh', id: ''} + break + } + + default: { + this.d(`API status response was ${response.status}. Expected 201 or 204.`) + throw new Error('Unexpected server response. Please contact support at https://bump.sh if this error persists') + } + } + + return version + } + + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + d(formatter: any, ...args: any[]): void { + return debug(`bump-cli:core:deploy`)(formatter, ...args) } public async run( @@ -24,81 +50,45 @@ export class Deploy { branch: string | undefined, overlay?: string | undefined, ): Promise { - let version: VersionResponse | undefined = undefined; + let version: VersionResponse | undefined if (overlay) { - await api.applyOverlay(overlay); + await api.applyOverlay(overlay) } - const [definition, references] = api.extractDefinition(); + + const [definition, references] = api.extractDefinition() const request: VersionRequest = { - documentation, - hub, - documentation_name: documentationName, auto_create_documentation: autoCreate && !dryRun, + branch_name: branch, definition, + documentation, + documentation_name: documentationName, + hub, references, - branch_name: branch, - }; - + } if (dryRun) { - await this.validateVersion(request, token); + await this.validateVersion(request, token) } else { - version = await this.createVersion(request, token); - } - - return version; - } - - get bumpClient(): BumpApi { - if (!this._bump) this._bump = new BumpApi(this._config); - return this._bump; - } - - async createVersion( - request: VersionRequest, - token: string, - ): Promise { - const response = await this.bumpClient.postVersion(request, token); - let version: VersionResponse | undefined = undefined; - - switch (response.status) { - case 204: - break; - case 201: - version = response.data - ? response.data - : { id: '', doc_public_url: 'https://bump.sh' }; - break; - default: - this.d(`API status response was ${response.status}. Expected 201 or 204.`); - throw new Error( - 'Unexpected server response. Please contact support at https://bump.sh if this error persists', - ); + version = await this.createVersion(request, token) } - return version; + return version } + // Function signature type taken from @types/debug + // Debugger(formatter: any, ...args: any[]): void; async validateVersion(version: VersionRequest, token: string): Promise { - const response = await this.bumpClient.postValidation(version, token); + const response = await this._bump.postValidation(version, token) switch (response.status) { - case 200: - break; - default: - this.d(`API status response was ${response.status}. Expected 200.`); - throw new Error( - 'Unexpected server response. Please contact support at https://bump.sh if this error persists', - ); - } - - return; - } + case 200: { + break + } - // Function signature type taken from @types/debug - // Debugger(formatter: any, ...args: any[]): void; - /* eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any */ - d(formatter: any, ...args: any[]): void { - return debug(`bump-cli:core:deploy`)(formatter, ...args); + default: { + this.d(`API status response was ${response.status}. Expected 200.`) + throw new Error('Unexpected server response. Please contact support at https://bump.sh if this error persists') + } + } } } diff --git a/src/core/diff.ts b/src/core/diff.ts index 3798077a..f138eb28 100644 --- a/src/core/diff.ts +++ b/src/core/diff.ts @@ -1,112 +1,51 @@ -import { CLIError } from '@oclif/errors'; -import * as Config from '@oclif/config'; -import debug from 'debug'; - -import { API } from '../definition'; -import { BumpApi } from '../api'; -import { - VersionRequest, - VersionResponse, - WithDiff, - DiffRequest, - DiffResponse, -} from '../api/models'; +import {CLIError} from '@oclif/core/errors' +import debug from 'debug' + +import {BumpApi} from '../api/index.js' +import {DiffRequest, DiffResponse, VersionRequest, VersionResponse, WithDiff} from '../api/models.js' +import {API} from '../definition.js' export class Diff { // 120 seconds = 2 minutes - static readonly TIMEOUT = 120; - - _bump!: BumpApi; - _config: Config.IConfig; - - public constructor(config: Config.IConfig) { - this._config = config; - } - - public async run( - file1: string, - file2: string | undefined, - documentation: string | undefined, - hub: string | undefined, - branch: string | undefined, - token: string | undefined, - format: string, - expires: string | undefined, - ): Promise { - let diffVersion: VersionResponse | DiffResponse | undefined = undefined; - - if (file2 && (!documentation || !token)) { - diffVersion = await this.createDiff(file1, file2, expires); - } else { - if (!documentation || !token) { - throw new Error( - 'Please login to bump (with documentation & token) when using a single file argument', - ); - } + static readonly TIMEOUT = 120 - diffVersion = await this.createVersion(file1, documentation, token, hub, branch); + private _bump!: BumpApi - if (file2) { - diffVersion = await this.createVersion( - file2, - documentation, - token, - hub, - branch, - diffVersion && diffVersion.id, - ); - } - } - - if (diffVersion) { - return await this.waitResult(diffVersion, token, { - timeout: Diff.TIMEOUT, - format, - }); - } else { - return undefined; - } - } - - get bumpClient(): BumpApi { - if (!this._bump) this._bump = new BumpApi(this._config); - return this._bump; + public constructor(bumpClient: BumpApi) { + this._bump = bumpClient } get pollingPeriod(): number { - return 1000; + return process.env.BUMP_POLLING_PERIOD ? Number(process.env.BUMP_POLLING_PERIOD) : 1000 } - async createDiff( - file1: string, - file2: string, - expires: string | undefined, - ): Promise { - const api = await API.load(file1); - const [previous_definition, previous_references] = api.extractDefinition(); - const api2 = await API.load(file2); - const [definition, references] = api2.extractDefinition(); + async createDiff(file1: string, file2: string, expires: string | undefined): Promise { + const api = await API.load(file1) + const [previous_definition, previous_references] = api.extractDefinition() + const api2 = await API.load(file2) + const [definition, references] = api2.extractDefinition() const request: DiffRequest = { + definition, + expires_at: expires, previous_definition, previous_references, - definition, references, - expires_at: expires, - }; + } - const response = await this.bumpClient.postDiff(request); + const response = await this._bump.postDiff(request) switch (response.status) { - case 201: - this.d(`Diff created with ID ${response.data.id}`); - this.d(response.data); - return response.data; - break; - case 204: - break; - } + case 201: { + this.d(`Diff created with ID ${response.data.id}`) + this.d(response.data) + return response.data + break + } - return; + case 204: { + break + } + } } async createVersion( @@ -117,112 +56,147 @@ export class Diff { branch_name: string | undefined, previous_version_id: string | undefined = undefined, ): Promise { - const api = await API.load(file); - const [definition, references] = api.extractDefinition(); + const api = await API.load(file) + const [definition, references] = api.extractDefinition() const request: VersionRequest = { + branch_name, + definition, documentation, hub, - definition, + previous_version_id, references, unpublished: true, - previous_version_id, - branch_name, - }; + } - const response = await this.bumpClient.postVersion(request, token); + const response = await this._bump.postVersion(request, token) switch (response.status) { - case 201: - this.d(`Unpublished version created with ID ${response.data.id}`); - return response.data; - break; - case 204: - break; + case 201: { + this.d(`Unpublished version created with ID ${response.data.id}`) + return response.data + break + } + + case 204: { + break + } } + } - return; + // Function signature type taken from @types/debug + // Debugger(formatter: any, ...args: any[]): void; + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + d(formatter: any, ...args: any[]): void { + return debug(`bump-cli:core:diff`)(formatter, ...args) } - async waitResult( - result: VersionResponse | DiffResponse, + extractDiff(versionWithDiff: VersionResponse & WithDiff): DiffResponse { + // TODO: return a real diff_id in the GET /version API + return { + breaking: versionWithDiff.diff_breaking, + details: versionWithDiff.diff_details, + id: versionWithDiff.id, + markdown: versionWithDiff.diff_markdown, + public_url: versionWithDiff.diff_public_url, + text: versionWithDiff.diff_summary, + } + } + + isVersion(result: DiffResponse | VersionResponse): result is VersionResponse { + return (result as VersionResponse).doc_public_url !== undefined + } + + isVersionWithDiff(result: DiffResponse | (VersionResponse & WithDiff)): result is VersionResponse & WithDiff { + const {diff_details, diff_markdown, diff_summary} = result as VersionResponse & WithDiff + return (diff_summary || diff_markdown || diff_details) !== undefined + } + + async pollingDelay(): Promise { + await this.delay(this.pollingPeriod) + } + + public async run( + file1: string, + file2: string | undefined, + documentation: string | undefined, + hub: string | undefined, + branch: string | undefined, token: string | undefined, - opts: { timeout: number; format: string }, - ): Promise { - let pollingResponse = undefined; + format: string, + expires: string | undefined, + ): Promise { + let diffVersion: DiffResponse | VersionResponse | undefined - if (this.isVersion(result) && token) { - pollingResponse = await this.bumpClient.getVersion(result.id, token); + if (file2 && (!documentation || !token)) { + diffVersion = await this.createDiff(file1, file2, expires) } else { - pollingResponse = await this.bumpClient.getDiff(result.id, opts.format); + if (!documentation || !token) { + throw new Error('Please login to bump (with documentation & token) when using a single file argument') + } + + diffVersion = await this.createVersion(file1, documentation, token, hub, branch) + + if (file2) { + diffVersion = await this.createVersion(file2, documentation, token, hub, branch, diffVersion && diffVersion.id) + } + } + + if (diffVersion) { + return this.waitResult(diffVersion, token, { + format, + timeout: Diff.TIMEOUT, + }) } + return undefined + } + + async waitResult( + result: DiffResponse | VersionResponse, + token: string | undefined, + opts: {format: string; timeout: number}, + ): Promise { + const pollingResponse = await (this.isVersion(result) && token + ? this._bump.getVersion(result.id, token) + : this._bump.getDiff(result.id, opts.format)) + if (opts.timeout <= 0) { throw new CLIError( 'We were unable to compute your documentation diff. Sorry about that. Please try again later. If the error persists, please contact support at https://bump.sh.', - ); + ) } switch (pollingResponse.status) { - case 200: - let diff: (VersionResponse & WithDiff) | DiffResponse = pollingResponse.data; + case 200: { + let diff: DiffResponse | (VersionResponse & WithDiff) = pollingResponse.data if (this.isVersionWithDiff(diff)) { - diff = this.extractDiff(diff); + diff = this.extractDiff(diff) } - this.d('Received diff:'); - this.d(diff); - return diff; - break; - case 202: - this.d('Waiting 1 sec before next poll'); - await this.pollingDelay(); - return await this.waitResult(result, token, { - timeout: opts.timeout - 1, + this.d('Received diff:') + this.d(diff) + return diff + break + } + + case 202: { + this.d('Waiting 1 sec before next poll') + await this.pollingDelay() + return this.waitResult(result, token, { format: opts.format, - }); - break; + timeout: opts.timeout - 1, + }) + break + } } - return {} as DiffResponse; - } - - async pollingDelay(): Promise { - return await this.delay(this.pollingPeriod); + return {} as DiffResponse } private async delay(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); - } - - // Function signature type taken from @types/debug - // Debugger(formatter: any, ...args: any[]): void; - /* eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any */ - d(formatter: any, ...args: any[]): void { - return debug(`bump-cli:core:diff`)(formatter, ...args); - } - - isVersion(result: VersionResponse | DiffResponse): result is VersionResponse { - return (result as VersionResponse).doc_public_url !== undefined; - } - - isVersionWithDiff( - result: (VersionResponse & WithDiff) | DiffResponse, - ): result is VersionResponse & WithDiff { - const { diff_summary, diff_markdown, diff_details } = result as VersionResponse & - WithDiff; - return (diff_summary || diff_markdown || diff_details) !== undefined; - } - - extractDiff(versionWithDiff: VersionResponse & WithDiff): DiffResponse { - // TODO: return a real diff_id in the GET /version API - return { - id: versionWithDiff.id, - public_url: versionWithDiff.diff_public_url, - text: versionWithDiff.diff_summary, - markdown: versionWithDiff.diff_markdown, - details: versionWithDiff.diff_details, - breaking: versionWithDiff.diff_breaking, - }; + return new Promise((resolve) => { + setTimeout(resolve, ms) + }) } } diff --git a/src/core/overlay.ts b/src/core/overlay.ts index c3a04101..b33706d9 100644 --- a/src/core/overlay.ts +++ b/src/core/overlay.ts @@ -1,9 +1,10 @@ -import debug from 'debug'; -import jsonpath from 'jsonpath'; -import mergician from 'mergician'; -import { JSONSchema4Object } from 'json-schema'; +import debug from 'debug' +import {JSONSchema4Object} from 'json-schema' +/* eslint-disable-next-line import/default */ +import jsonpath from 'jsonpath' +import {mergician} from 'mergician' -import { APIDefinition, OpenAPIOverlay } from '../definition'; +import {APIDefinition, OpenAPIOverlay} from '../definition.js' export class Overlay { // WIP @github.com/lornajane/openapi-overlays-js @@ -13,69 +14,74 @@ export class Overlay { // from github.com/lornajane/openapi-overlays-js and has been // adapted to make our Typescript build happy. // + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + d(formatter: any, ...args: any[]): void { + return debug(`bump-cli:core:overlay`)(formatter, ...args) + } + + // Function signature type taken from @types/debug + // Debugger(formatter: any, ...args: any[]): void; // If you make any changes here, PLEASE ALSO MAKE THEM UPSTREAM. public run(spec: APIDefinition, overlay: OpenAPIOverlay): APIDefinition { // Use jsonpath.apply to do the changes - if (overlay.actions && overlay.actions.length >= 1) - overlay.actions.forEach((a) => { - const action = a as JSONSchema4Object; + if (overlay.actions && overlay.actions.length > 0) + for (const a of overlay.actions) { + const action = a as JSONSchema4Object if (!action.target) { - process.stderr.write('Action with a missing target\n'); - return; + process.stderr.write('Action with a missing target\n') + continue } - const target = action.target as string; + + const target = action.target as string // Is it a remove? - if (action.hasOwnProperty('remove')) { + if (Object.hasOwn(action, 'remove')) { + /* eslint-disable-next-line no-constant-condition */ while (true) { - const path = jsonpath.paths(spec, target); - if (path.length == 0) { - break; + const path = jsonpath.paths(spec, target) + if (path.length === 0) { + break } - const parent = jsonpath.parent(spec, target); - const thingToRemove = path[0][path[0].length - 1]; - if (Array.isArray(parent)) { - parent.splice(thingToRemove as number, 1); - } else { - delete parent[thingToRemove]; + + const parent = jsonpath.parent(spec, target) + const thingToRemove = path[0].at(-1) + if (thingToRemove !== undefined) { + if (Array.isArray(parent)) { + parent.splice(thingToRemove as number, 1) + } else { + delete parent[thingToRemove] + } } } } else { try { // It must be an update // Deep merge objects using a module (built-in spread operator is only shallow) - const merger = mergician({ appendArrays: true }); + const merger = mergician({appendArrays: true}) if (target === '$') { // You can't actually merge an update on a root object // target with the jsonpath lib, this is just us merging // the given update with the whole spec. - spec = merger(spec, action.update); + spec = merger(spec, action.update) } else { jsonpath.apply(spec, target, (chunk) => { if (typeof chunk === 'object' && typeof action.update === 'object') { if (Array.isArray(chunk) && Array.isArray(action.update)) { - return chunk.concat(action.update); - } else { - return merger(chunk, action.update); + return [...chunk, ...action.update] } - } else { - return action.update; + + return merger(chunk, action.update) } - }); + + return action.update + }) } - } catch (ex) { - process.stderr.write(`Error applying overlay: ${(ex as Error).message}\n`); - //return chunk + } catch (error) { + process.stderr.write(`Error applying overlay: ${(error as Error).message}\n`) + // return chunk } } - }); - - return spec; - } + } - // Function signature type taken from @types/debug - // Debugger(formatter: any, ...args: any[]): void; - /* eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any */ - d(formatter: any, ...args: any[]): void { - return debug(`bump-cli:core:overlay`)(formatter, ...args); + return spec } } diff --git a/src/core/utils/file.ts b/src/core/utils/file.ts index fb14a298..96868e8e 100644 --- a/src/core/utils/file.ts +++ b/src/core/utils/file.ts @@ -1,45 +1,42 @@ -import { readdirSync, statSync } from 'fs'; -import { extname, basename } from 'path'; +import {readdirSync, statSync} from 'node:fs' +import {basename, extname} from 'node:path' -type FileDescription = { value: string; label: string; filename: string }; +type FileDescription = {filename: string; label: string; value: string} export const isDir = (path: string): boolean => { try { - return statSync(path).isDirectory(); - } catch (e) { - return false; + return statSync(path).isDirectory() + } catch { + return false } -}; +} export class File { - protected static readonly supportedFormats = ['.yml', '.yaml', '.json']; + protected static readonly supportedFormats = ['.yml', '.yaml', '.json'] - public static listValidConventionFiles(path: string, regex: RegExp): FileDescription[] { - return File.listValidFormatFiles(path).filter(({ filename }) => { - return filename.match(regex); - }); + public static listInvalidConventionFiles(path: string, regex: RegExp): FileDescription[] { + return File.listValidFormatFiles(path).filter(({filename}) => { + return !regex.test(filename) + }) } - public static listInvalidConventionFiles( - path: string, - regex: RegExp, - ): FileDescription[] { - return File.listValidFormatFiles(path).filter(({ filename }) => { - return !filename.match(regex); - }); + public static listValidConventionFiles(path: string, regex: RegExp): FileDescription[] { + return File.listValidFormatFiles(path).filter(({filename}) => { + return regex.test(filename) + }) } private static listValidFormatFiles(path: string): FileDescription[] { return readdirSync(path) .filter((file) => { - return File.supportedFormats.includes(extname(file)); + return File.supportedFormats.includes(extname(file)) }) .map((file) => { return { - value: file, - label: basename(file), filename: basename(file, extname(file)), - }; - }); + label: basename(file), + value: file, + } + }) } } diff --git a/src/core/utils/prompts.ts b/src/core/utils/prompts.ts index f1387587..6ddf9253 100644 --- a/src/core/utils/prompts.ts +++ b/src/core/utils/prompts.ts @@ -1,22 +1,20 @@ -import { CLIError } from '@oclif/errors'; -import * as p from '@clack/prompts'; +import * as p from '@clack/prompts' +import {CLIError, ExitError} from '@oclif/core/errors' export const confirm = async (message = 'Continue?'): Promise => { const prompt = await p.group( { - shouldContinue: () => p.confirm({ message: message }), + shouldContinue: () => p.confirm({message}), }, { - onCancel: () => { - p.cancel('Cancelled.'); - process.exit(0); + onCancel() { + p.cancel('Cancelled.') + throw new ExitError(1) }, }, - ); + ) if (!prompt.shouldContinue) { - throw new CLIError(`Cancelled`); + throw new CLIError(`Cancelled`) } - - return; -}; +} diff --git a/src/definition.ts b/src/definition.ts index f74ae7fa..ef546e02 100644 --- a/src/definition.ts +++ b/src/definition.ts @@ -1,28 +1,26 @@ -import { CLIError } from '@oclif/errors'; -import $RefParser from '@apidevtools/json-schema-ref-parser'; -import { defaults } from '@apidevtools/json-schema-ref-parser/lib/options'; -import asyncapi from '@asyncapi/specs'; +import {default as $RefParser, getJsonSchemaRefParserDefaultOptions} from '@apidevtools/json-schema-ref-parser' +import asyncapi from '@asyncapi/specs' +import {CLIError} from '@oclif/core/errors' +import {safeStringify} from '@stoplight/yaml' import { JSONSchema4, - JSONSchema4Object, JSONSchema4Array, + JSONSchema4Object, JSONSchema6, JSONSchema6Object, JSONSchema7, -} from 'json-schema'; -import path from 'path'; -import { safeStringify } from '@stoplight/yaml'; +} from 'json-schema' +import {createRequire} from 'node:module' +import path from 'node:path' + +// Used to require JSON files +const require = createRequire(import.meta.url) -import { Overlay } from './core/overlay'; +import {Overlay} from './core/overlay.js' -type SpecSchema = JSONSchema4 | JSONSchema6 | JSONSchema7; +type SpecSchema = JSONSchema4 | JSONSchema6 | JSONSchema7 class SupportedFormat { - static readonly openapi: Record = { - '2.0': require('oas-schemas/schemas/v2.0/schema.json'), - '3.0': require('oas-schemas/schemas/v3.0/schema.json'), - '3.1': require('oas-schemas/schemas/v3.1/schema.json'), - }; static readonly asyncapi: Record = { '2.0': asyncapi.schemas['2.0.0'], '2.1': asyncapi.schemas['2.1.0'], @@ -31,296 +29,293 @@ class SupportedFormat { '2.4': asyncapi.schemas['2.4.0'], '2.5': asyncapi.schemas['2.5.0'], '2.6': asyncapi.schemas['2.6.0'], - }; + } + + static readonly openapi: Record = { + '2.0': require('oas-schemas/schemas/v2.0/schema.json'), + '3.0': require('oas-schemas/schemas/v3.0/schema.json'), + '3.1': require('oas-schemas/schemas/v3.1/schema.json'), + } } class UnsupportedFormat extends CLIError { constructor(message = '') { - const compatOpenAPI = Object.keys(SupportedFormat.openapi).join(', '); - const compatAsyncAPI = Object.keys(SupportedFormat.asyncapi).join(', '); + const compatOpenAPI = Object.keys(SupportedFormat.openapi).join(', ') + const compatAsyncAPI = Object.keys(SupportedFormat.asyncapi).join(', ') const errorMsgs = [ `Unsupported API specification (${message})`, `Please try again with an OpenAPI ${compatOpenAPI} or AsyncAPI ${compatAsyncAPI} file.`, - ]; + ] - super(errorMsgs.join('\n')); + super(errorMsgs.join('\n')) } } class API { - readonly location: string; - readonly rawDefinition: string; - readonly definition: APIDefinition; - overlayedDefinition: APIDefinition | undefined; - readonly references: APIReference[]; - readonly version: string; - readonly specName: string; - readonly spec?: SpecSchema; - - constructor(location: string, $refs: $RefParser.$Refs) { - this.location = location; - this.references = []; - - const [raw, parsed] = this.resolveContent($refs); - this.rawDefinition = raw as string; - - this.definition = parsed; - this.specName = this.getSpecName(parsed); - this.version = this.getVersion(parsed); - this.spec = this.getSpec(parsed); + readonly definition: APIDefinition + readonly location: string + overlayedDefinition: APIDefinition | undefined + readonly rawDefinition: string + readonly references: APIReference[] + readonly spec?: SpecSchema + readonly specName: string + readonly version: string + + constructor(location: string, values: SpecSchema) { + this.location = location + this.references = [] + + const [raw, parsed] = this.resolveContent(values) + this.rawDefinition = raw as string + + this.definition = parsed + this.specName = this.getSpecName(parsed) + this.version = this.getVersion(parsed) + this.spec = this.getSpec(parsed) if (this.spec === undefined) { - throw new UnsupportedFormat(`${this.specName} ${this.version}`); + throw new UnsupportedFormat(`${this.specName} ${this.version}`) + } + } + + static isAsyncAPI(definition: JSONSchema4Object | JSONSchema6Object): definition is AsyncAPI { + return 'asyncapi' in definition + } + + static isOpenAPI(definition: JSONSchema4Object | JSONSchema6Object): definition is OpenAPI { + return typeof definition.openapi === 'string' || typeof definition.swagger === 'string' + } + + static isOpenAPIOverlay(definition: JSONSchema4Object | JSONSchema6Object): definition is OpenAPIOverlay { + return 'overlay' in definition + } + + static async load(path: string): Promise { + const {json, text, yaml} = getJsonSchemaRefParserDefaultOptions().parse + // Not sure why the lib types the parser as potentially + // “undefined”, hence the forced typing in the following consts. + const TextParser = text as $RefParser.Plugin + const JSONParser = json as $RefParser.Plugin + const YAMLParser = yaml as $RefParser.Plugin + // We override the default parsers from $RefParser to be able + // to keep the raw content of the files parsed + const withRawTextParser = (parser: $RefParser.Plugin): $RefParser.Plugin => ({ + ...parser, + async parse(file: $RefParser.FileInfo): Promise { + if (typeof parser.parse === 'function' && typeof TextParser.parse === 'function') { + const parsed = (await parser.parse(file)) as JSONSchema4 | JSONSchema6 + return {parsed, raw: TextParser.parse(file) as string} + } + + // Not sure why the lib states that Plugin.parse can be a + // scalar number | string (on not only a callable function) + return {} + }, + }) + + return $RefParser + .resolve(path, { + dereference: {circular: false}, + parse: { + json: withRawTextParser(JSONParser), + text: { + ...TextParser, + canParse: ['.md', '.markdown'], + encoding: 'utf8', + async parse(file: $RefParser.FileInfo): Promise { + if (typeof TextParser.parse === 'function') { + const parsed = (await TextParser.parse(file)) as string + return {parsed, raw: parsed} + } + + // Not sure why the lib states that Plugin.parse can be a + // scalar number | string (on not only a callable function) + return {} + }, + }, + yaml: withRawTextParser(YAMLParser), + }, + }) + .then(($refs) => { + const values = $refs.values() + return new API(path, values) + }) + .catch((error: Error) => { + throw new CLIError(error) + }) + } + + public async applyOverlay(overlayPath: string): Promise { + const overlay = await API.load(overlayPath) + const overlayDefinition = overlay.definition + + if (!API.isOpenAPIOverlay(overlayDefinition)) { + throw new Error(`${overlayPath} does not look like an OpenAPI overlay`) + } + + this.overlayedDefinition = await new Overlay().run(this.definition, overlayDefinition) + } + + public extractDefinition(outputPath?: string): [string, APIReference[]] { + const references = [] + + for (let i = 0; i < this.references.length; i++) { + const reference = this.references[i] + references.push({ + content: reference.content, + location: reference.location, + }) } + + return [this.serializeDefinition(outputPath), references] } getSpec(definition: APIDefinition): SpecSchema { if (API.isAsyncAPI(definition)) { - return SupportedFormat.asyncapi[this.versionWithoutPatch()]; - } else if (API.isOpenAPIOverlay(definition)) { - return { overlay: { type: 'string' } }; - } else { - return SupportedFormat.openapi[this.versionWithoutPatch()]; + return SupportedFormat.asyncapi[this.versionWithoutPatch()] + } + + if (API.isOpenAPIOverlay(definition)) { + return {overlay: {type: 'string'}} } + + return SupportedFormat.openapi[this.versionWithoutPatch()] } getSpecName(definition: APIDefinition): string { if (API.isAsyncAPI(definition)) { - return 'AsyncAPI'; - } else { - return 'OpenAPI'; + return 'AsyncAPI' } + + return 'OpenAPI' } getVersion(definition: APIDefinition): string { if (API.isAsyncAPI(definition)) { - return definition.asyncapi; - } else { - return (definition.openapi || definition.swagger) as string; + return definition.asyncapi } - } - guessFormat(output?: string): string { - return (output || this.location).endsWith('.json') ? 'json' : 'yaml'; + return (definition.openapi || definition.swagger) as string } - versionWithoutPatch(): string { - const [major, minor] = this.version.split('.', 3); - - return `${major}.${minor}`; - } - - /* Resolve reference absolute paths to the main api location when possible */ - resolveRelativeLocation(absPath: string): string { - const url = (location: string): Location | { hostname: string } => { - try { - return new URL(location); - } catch { - return { hostname: '' }; - } - }; - const definitionUrl = url(this.location); - const refUrl = url(absPath); - - if ( - absPath.match(/^\//) || // Unix style filesystem path - absPath.match(/^[a-zA-Z]+\:\\/) || // Windows style filesystem path - (absPath.match(/^https?:\/\//) && definitionUrl.hostname === refUrl.hostname) // Same domain URLs - ) { - return path.relative(path.dirname(this.location), absPath); - } else { - return absPath; - } + guessFormat(output?: string): string { + return (output || this.location).endsWith('.json') ? 'json' : 'yaml' } - resolveContent($refs: $RefParser.$Refs): [string, APIDefinition] { - const values = $refs.values(); - let mainReference: JSONSchemaWithRaw = { parsed: {}, raw: '' }; + resolveContent(values: SpecSchema): [string, APIDefinition] { + let mainReference: JSONSchemaWithRaw = {parsed: {}, raw: ''} for (const [absPath, reference] of Object.entries(values)) { if (absPath === this.location || absPath === path.resolve(this.location)) { // $refs.values is not properly typed so we need to force it // with the resulting type of our custom defined parser - mainReference = reference as JSONSchemaWithRaw; + mainReference = reference as JSONSchemaWithRaw } else { // $refs.values is not properly typed so we need to force it // with the resulting type of our custom defined parser - const { raw } = reference as JSONSchemaWithRaw; + const {raw} = reference as JSONSchemaWithRaw if (!raw) { - throw new UnsupportedFormat('Reference ${absPath} is empty'); + throw new UnsupportedFormat(`Reference ${absPath} is empty`) } this.references.push({ - location: this.resolveRelativeLocation(absPath), content: raw, - }); + location: this.resolveRelativeLocation(absPath), + }) } } - const { raw, parsed } = mainReference; + const {parsed, raw} = mainReference - if (!parsed || !(parsed instanceof Object) || !('info' in parsed)) { - throw new UnsupportedFormat( - "Definition needs to be an object with at least an 'info' key", - ); + if (!parsed || !raw || !(parsed instanceof Object) || !('info' in parsed)) { + throw new UnsupportedFormat("Definition needs to be an object with at least an 'info' key") } - if ( - !API.isOpenAPI(parsed) && - !API.isAsyncAPI(parsed) && - !API.isOpenAPIOverlay(parsed) - ) { - throw new UnsupportedFormat(); + if (!API.isOpenAPI(parsed) && !API.isAsyncAPI(parsed) && !API.isOpenAPIOverlay(parsed)) { + throw new UnsupportedFormat() } - return [raw, parsed]; + return [raw, parsed] } - serializeDefinition(outputPath?: string): string { - if (this.overlayedDefinition) { - let serializedDefinition: string; - - if (this.guessFormat(outputPath) == 'json') { - serializedDefinition = JSON.stringify(this.overlayedDefinition); - } else { - serializedDefinition = safeStringify(this.overlayedDefinition); - } + /* Resolve reference absolute paths to the main api location when possible */ + resolveRelativeLocation(absPath: string): string { + const definitionUrl = this.url() + const refUrl = this.url(absPath) - return serializedDefinition; - } else { - return this.rawDefinition; + if ( + /^\//.test(absPath) || // Unix style filesystem path + /^[A-Za-z]+:\\/.test(absPath) || // Windows style filesystem path + (/^https?:\/\//.test(absPath) && definitionUrl.hostname === refUrl.hostname) // Same domain URLs + ) { + return path.relative(path.dirname(this.location), absPath) } - } - static isOpenAPI( - definition: JSONSchema4Object | JSONSchema6Object, - ): definition is OpenAPI { - return ( - typeof definition.openapi === 'string' || typeof definition.swagger === 'string' - ); + return absPath } - static isAsyncAPI( - definition: JSONSchema4Object | JSONSchema6Object, - ): definition is AsyncAPI { - return 'asyncapi' in definition; - } - - static isOpenAPIOverlay( - definition: JSONSchema4Object | JSONSchema6Object, - ): definition is OpenAPIOverlay { - return 'overlay' in definition; - } - - public extractDefinition(outputPath?: string): [string, APIReference[]] { - const references = []; - - for (let i = 0; i < this.references.length; i++) { - const reference = this.references[i]; - references.push({ - location: reference.location, - content: reference.content, - }); + serializeDefinition(outputPath?: string): string { + if (this.overlayedDefinition) { + return this.guessFormat(outputPath) === 'json' + ? JSON.stringify(this.overlayedDefinition) + : safeStringify(this.overlayedDefinition) } - return [this.serializeDefinition(outputPath), references]; + return this.rawDefinition } - public async applyOverlay(overlayPath: string): Promise { - const overlay = await API.load(overlayPath); - const overlayDefinition = overlay.definition; - - if (!API.isOpenAPIOverlay(overlayDefinition)) { - throw new Error(`${overlayPath} does not look like an OpenAPI overlay`); - } + versionWithoutPatch(): string { + const [major, minor] = this.version.split('.', 3) - this.overlayedDefinition = await new Overlay().run( - this.definition, - overlayDefinition, - ); + return `${major}.${minor}` } - static async load(path: string): Promise { - const JSONParser = defaults.parse.json; - const YAMLParser = defaults.parse.yaml; - const TextParser = defaults.parse.text; - // We override the default parsers from $RefParser to be able - // to keep the raw content of the files parsed - const withRawTextParser = ( - parser: $RefParser.ParserOptions, - ): $RefParser.ParserOptions => { - return { - ...parser, - parse: async (file: $RefParser.FileInfo): Promise => { - const parsed = (await parser.parse(file)) as JSONSchema4 | JSONSchema6; - return { parsed, raw: TextParser.parse(file) }; - }, - }; - }; - - return $RefParser - .resolve(path, { - parse: { - json: withRawTextParser(JSONParser), - yaml: withRawTextParser(YAMLParser), - text: { - ...TextParser, - parse: async (file: $RefParser.FileInfo): Promise => { - const parsed = await TextParser.parse(file); - return { parsed, raw: parsed }; - }, - canParse: ['.md', '.markdown'], - encoding: 'utf8', - }, - }, - dereference: { circular: false }, - }) - .then(($refs) => { - return new API(path, $refs); - }) - .catch((err: Error) => { - throw new CLIError(err); - }); + private url(location: string = this.location): {hostname: string} | Location { + try { + return new URL(location) + } catch { + return {hostname: ''} + } } } type JSONSchemaWithRaw = { - readonly parsed: JSONSchema4 | JSONSchema6; - readonly raw: string; -}; + readonly parsed?: JSONSchema4 | JSONSchema6 | string + readonly raw?: string +} type APIReference = { - location: string; - content: string; -}; + content: string + location: string +} -type APIDefinition = OpenAPI | AsyncAPI | OpenAPIOverlay; +type APIDefinition = AsyncAPI | OpenAPI | OpenAPIOverlay type InfoObject = { - readonly title: string; - readonly version: string; - readonly description?: string; -}; + readonly description?: string + readonly title: string + readonly version: string +} // http://spec.openapis.org/oas/v3.1.0#oasObject -type OpenAPI = JSONSchema4Object & { - readonly openapi?: string; - readonly swagger?: string; - readonly info: InfoObject; -}; - -type OpenAPIOverlay = JSONSchema4Object & { - readonly overlay: string; - readonly info: InfoObject; - readonly actions: JSONSchema4Array; -}; +type OpenAPI = { + readonly info: InfoObject + readonly openapi?: string + readonly swagger?: string +} & JSONSchema4Object + +type OpenAPIOverlay = { + readonly actions: JSONSchema4Array + readonly info: InfoObject + readonly overlay: string +} & JSONSchema4Object // https://www.asyncapi.com/docs/specifications/2.0.0#A2SObject -type AsyncAPI = JSONSchema4Object & { - readonly asyncapi: string; - readonly info: InfoObject; -}; +type AsyncAPI = { + readonly asyncapi: string + readonly info: InfoObject +} & JSONSchema4Object -export { API, APIDefinition, OpenAPIOverlay, SupportedFormat }; +export {API, APIDefinition, OpenAPIOverlay, SupportedFormat} diff --git a/src/flags.ts b/src/flags.ts index 3d1739c7..692bb18d 100644 --- a/src/flags.ts +++ b/src/flags.ts @@ -1,158 +1,156 @@ -import { flags } from '@oclif/command'; -import * as Parser from '@oclif/parser'; +import {Flags, Interfaces} from '@oclif/core' +// import * as Parser from '@oclif/parser'; // Re-export oclif flags https://oclif.io/docs/flags -export * from '@oclif/command/lib/flags'; +// export * from '@oclif/command/lib/flags'; // Custom flags for bump-cli -const doc = flags.build({ +const doc = Flags.custom({ char: 'd', - description: - 'Documentation public id or slug. Can be provided via BUMP_ID environment variable', - default: () => { - const envDoc = process.env.BUMP_ID; - if (envDoc) return envDoc; + async default(): Promise { + const envDoc = process.env.BUMP_ID + if (envDoc) return envDoc // Search doc id in .bump/config.json file? }, -}); + description: 'Documentation public id or slug. Can be provided via BUMP_ID environment variable', +}) -const docName = flags.build({ +const docName = Flags.custom({ char: 'n', - description: 'Documentation name. Used with --auto-create flag.', dependsOn: ['auto-create'], -}); + description: 'Documentation name. Used with --auto-create flag.', +}) -const hub = flags.build({ +const hub = Flags.custom({ char: 'b', - description: 'Hub id or slug. Can be provided via BUMP_HUB_ID environment variable', - default: () => { - const envHub = process.env.BUMP_HUB_ID; - if (envHub) return envHub; + async default(): Promise { + const envHub = process.env.BUMP_HUB_ID + if (envHub) return envHub // Search hub id in .bump/config.json file? }, -}); + description: 'Hub id or slug. Can be provided via BUMP_HUB_ID environment variable', +}) -const filenamePattern = flags.build({ - description: `Pattern to extract the documentation slug from filenames when deploying a DIRECTORY. Pattern uses only '*' and '{slug}' as special characters to extract the slug from a filename without extension. Used with --hub flag only.`, +const filenamePattern = Flags.custom({ default: '{slug}-api', -}); + description: `Pattern to extract the documentation slug from filenames when deploying a DIRECTORY. Pattern uses only '*' and '{slug}' as special characters to extract the slug from a filename without extension. Used with --hub flag only.`, +}) -const branch = flags.build({ +const branch = Flags.custom({ char: 'B', - description: 'Branch name. Can be provided via BUMP_BRANCH_NAME environment variable', - default: () => { - const envBranch = process.env.BUMP_BRANCH_NAME; - if (envBranch) return envBranch; + async default(): Promise { + const envBranch = process.env.BUMP_BRANCH_NAME + if (envBranch) return envBranch }, -}); + description: 'Branch name. Can be provided via BUMP_BRANCH_NAME environment variable', +}) -const token = flags.build({ +const token = Flags.custom({ char: 't', - required: true, - description: - 'Documentation or Hub token. Can be provided via BUMP_TOKEN environment variable', - default: () => { - const envToken = process.env.BUMP_TOKEN; - if (envToken) return envToken; + async default(): Promise { + const envToken = process.env.BUMP_TOKEN + if (envToken) return envToken }, -}); + description: 'Documentation or Hub token. Can be provided via BUMP_TOKEN environment variable', + required: true, +}) -const autoCreate = (options = {}): Parser.flags.IBooleanFlag => { - return flags.boolean({ +const autoCreate = (opts: Partial> = {}) => { + return Flags.boolean({ + ...opts, + dependsOn: ['hub'], description: 'Automatically create the documentation if needed (only available with a --hub flag). Documentation name can be provided with --doc-name flag. Default: false', - dependsOn: ['hub'], - ...options, - }); -}; + }) +} -const interactive = (options = {}): Parser.flags.IBooleanFlag => { - return flags.boolean({ +const interactive = (opts: Partial> = {}) => { + return Flags.boolean({ + ...opts, + dependsOn: ['hub'], description: "Interactively create a configuration file to deploy a Hub (only available with a --hub flag). This will start an interactive process if you don't have a CLI configuration file. Default: false", - dependsOn: ['hub'], - ...options, - }); -}; + }) +} -const dryRun = (options = {}): Parser.flags.IBooleanFlag => { - return flags.boolean({ +const dryRun = (opts: Partial> = {}) => { + return Flags.boolean({ + ...opts, description: 'Validate a new documentation version. Does everything a normal deploy would do except publishing the new version. Useful in automated environments such as test platforms or continuous integration. Default: false', - ...options, - }); -}; + }) +} -const open = (options = {}): Parser.flags.IBooleanFlag => { - return flags.boolean({ +const open = (opts: Partial> = {}) => { + return Flags.boolean({ + ...opts, char: 'o', default: false, - ...options, - }); -}; + }) +} -const failOnBreaking = (options = {}): Parser.flags.IBooleanFlag => { - return flags.boolean({ +const failOnBreaking = (opts: Partial> = {}) => { + return Flags.boolean({ + ...opts, char: 'F', - description: 'Fail when diff contains a breaking change', - default: () => { - const envCi = process.env.CI; + async default(): Promise { + const envCi = process.env.CI if (envCi) { - return true; - } else { - return false; + return true } + + return false }, - ...options, - }); -}; + description: 'Fail when diff contains a breaking change', + }) +} -const live = (options = {}): Parser.flags.IBooleanFlag => { - return flags.boolean({ +const live = (opts: Partial> = {}) => { + return Flags.boolean({ + ...opts, char: 'l', default: false, - ...options, - }); -}; + }) +} -const format = flags.build({ +const format = Flags.custom({ char: 'f', - description: 'Format in which to provide the diff result', default: 'text', + description: 'Format in which to provide the diff result', options: ['text', 'markdown', 'json', 'html'], -}); +}) -const expires = flags.build({ +const expires = Flags.custom({ char: 'e', description: "Specify a longer expiration date for public diffs (defaults to 1 day). Use iso8601 format to provide a date, or you can use `--expires 'never'` to keep the result live indefinitely.", -}); +}) -const out = flags.build({ +const out = Flags.custom({ char: 'o', description: 'Output file path', -}); +}) -const overlay = flags.build({ +const overlay = Flags.custom({ char: 'o', description: 'Path or URL of an overlay file to apply before deploying', -}); +}) export { + autoCreate, + branch, doc, docName, - hub, - branch, - token, - autoCreate, - interactive, - filenamePattern, dryRun, - open, + expires, failOnBreaking, - live, + filenamePattern, format, - expires, + hub, + interactive, + live, + open, out, overlay, -}; + token, +} diff --git a/src/index.ts b/src/index.ts index 96f27793..779f2943 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,20 @@ -import { run } from '@oclif/command'; -import { Diff } from './core/diff'; -import { Overlay } from './core/overlay'; -import Deploy from './commands/deploy'; -import Preview from './commands/preview'; +import Deploy from './commands/deploy.js' +import Diff from './commands/diff.js' +import Overlay from './commands/overlay.js' +import Preview from './commands/preview.js' -export { VersionResponse, PreviewResponse, DiffResponse, WithDiff } from './api/models'; +export const COMMANDS = { + deploy: Deploy, + diff: Diff, + overlay: Overlay, + preview: Preview, +} -export { run, Deploy, Diff, Preview, Overlay }; +export {DiffResponse, PreviewResponse, VersionResponse, WithDiff} from './api/models.js' + +export {default as Deploy} from './commands/deploy.js' +export {default as Preview} from './commands/preview.js' +export * as Diff from './core/diff.js' + +export * as Overlay from './core/overlay.js' +export {run} from '@oclif/core' diff --git a/test/commands/deploy.test.ts b/test/commands/deploy.test.ts index 8aba4244..067fa602 100644 --- a/test/commands/deploy.test.ts +++ b/test/commands/deploy.test.ts @@ -1,245 +1,214 @@ -import base, { expect } from '@oclif/test'; -import nock from 'nock'; +import {runCommand} from '@oclif/test' +import {expect} from 'chai' +import nock from 'nock' +import {stub} from 'sinon' -nock.disableNetConnect(); +nock.disableNetConnect() -const test = base.env({ BUMP_TOKEN: 'BAR' }); +process.env.BUMP_TOKEN = process.env.BUMP_TOKEN || 'BAR' describe('deploy subcommand', () => { describe('Successful runs', () => { - test - .nock('https://bump.sh', (api) => - api - .post( - '/api/v1/versions', - (body) => body.documentation === 'coucou' && !body.branch_name, - ) - .reply(201, { doc_public_url: 'http://localhost/doc/1' }), + it('sends new version to Bump', async () => { + nock('https://bump.sh') + .post('/api/v1/versions', (body) => body.documentation === 'coucou' && !body.branch_name) + .reply(201, {doc_public_url: 'http://localhost/doc/1'}) + + const {stderr, stdout} = await runCommand( + ['deploy', 'examples/valid/openapi.v3.json', '--doc', 'coucou'].join(' '), ) - .stdout() - .stderr() - .command(['deploy', 'examples/valid/openapi.v3.json', '--doc', 'coucou']) - .it('sends new version to Bump', ({ stdout }) => { - expect(stdout).to.contain( - 'Your new documentation version will soon be ready at http://localhost/doc/1', - ); - }); - - test - .nock('https://bump.sh', (api) => - api - .post( - '/api/v1/versions', - (body) => body.documentation === 'coucou' && body.branch_name === 'next', - ) - .reply(201, { doc_public_url: 'http://localhost/doc/1/next' }), + expect(stderr).to.contain("Let's deploy on Bump.sh... done\n") + expect(stdout).to.contain( + 'Your coucou documentation...has received a new deployment which will soon be ready at:', ) - .stdout() - .stderr() - .command([ - 'deploy', - 'examples/valid/openapi.v3.json', - '--doc', - 'coucou', - '--branch', - 'next', - ]) - .it('sends new version to Bump on given branch', ({ stdout }) => { - expect(stdout).to.contain( - 'Your new documentation version will soon be ready at http://localhost/doc/1/next', - ); - }); - - test - .nock('https://bump.sh', (api) => api.post('/api/v1/versions').reply(204)) - .stdout() - .stderr() - .command(['deploy', 'examples/valid/openapi.v3.json', '--doc', 'coucou']) - .it('sends unchanged version to Bump', ({ stderr }) => { - expect(stderr).to.contain("Let's deploy a new version"); - expect(stderr).to.contain('Your documentation has not changed'); - }); - - test - .env({ BUMP_ID: 'coucou' }) - .nock('https://bump.sh', (api) => api.post('/api/v1/versions').reply(201)) - .stdout() - .stderr() - .command(['deploy', 'examples/valid/openapi.v3.json']) - .it('sends version to Bump with doc read from env variable', ({ stdout }) => { - expect(stdout).to.contain('Your new documentation version will soon be ready'); - }); + expect(stdout).to.contain('http://localhost/doc/1') + }) + + it('sends new version to Bump on given branch', async () => { + nock('https://bump.sh') + .post('/api/v1/versions', (body) => body.documentation === 'coucou' && body.branch_name === 'next') + .reply(201, {doc_public_url: 'http://localhost/doc/1/next'}) + + const {stdout} = await runCommand( + ['deploy', 'examples/valid/openapi.v3.json', '--doc', 'coucou', '--branch', 'next'].join(' '), + ) + expect(stdout).to.contain( + 'Your coucou documentation...has received a new deployment which will soon be ready at:\nhttp://localhost/doc/1/next', + ) + }) + + it('sends unchanged version to Bump', async () => { + nock('https://bump.sh').post('/api/v1/versions').reply(204) + + const {stderr, stdout} = await runCommand( + ['deploy', 'examples/valid/openapi.v3.json', '--doc', 'coucou'].join(' '), + ) + expect(stdout).to.equal('') + expect(stderr).to.contain("Let's deploy on Bump.sh... done\n") + expect(stderr).to.contain(' › Warning: Your coucou documentation has not changed\n') + }) + + it('sends version to Bump with doc read from env variable', async () => { + // Mock env variables BUMP_ID + process.env.BUMP_ID = process.env.BUMP_ID || '' + const stubs = [] + stubs.push(stub(process.env, 'BUMP_ID').value('coucou')) + nock('https://bump.sh') + .post('/api/v1/versions', (body) => body.documentation === 'coucou' && !body.branch_name) + .reply(201, {doc_public_url: 'http://localhost/doc/1'}) + + const {stdout} = await runCommand(['deploy', 'examples/valid/openapi.v3.json'].join(' ')) + expect(stdout).to.contain('Your coucou documentation...has received a new deployment which will soon be ready') + + stubs.map((s) => s.restore()) + }) describe('Successful dry-run deploy', () => { - test - .nock('https://bump.sh', (api) => api.post('/api/v1/validations').reply(200)) - .stdout() - .stderr() - .command([ - 'deploy', - 'examples/valid/openapi.v3.json', - '--doc', - 'coucou', - '--dry-run', - ]) - .it('sends validation to Bump', ({ stdout }) => { - expect(stdout).to.contain('Definition is valid'); - }); - - test - .nock('https://bump.sh', (api) => - api - .post('/api/v1/validations', (body) => !body.auto_create_documentation) - .reply(200), + it('sends validation to Bump', async () => { + nock('https://bump.sh').post('/api/v1/validations').reply(200) + + const {stdout} = await runCommand( + ['deploy', 'examples/valid/openapi.v3.json', '--doc', 'coucou', '--dry-run'].join(' '), + ) + expect(stdout).to.contain('Definition is valid') + }) + + it("doesn't try to auto create a documentation", async () => { + nock('https://bump.sh') + .post('/api/v1/validations', (body) => !body.auto_create_documentation) + .reply(200) + + await runCommand( + [ + 'deploy', + 'examples/valid/openapi.v3.json', + '--doc', + 'coucou', + '--hub', + 'coucou', + '--dry-run', + '--auto-create', + ].join(' '), ) - .stdout() - .stderr() - .command([ - 'deploy', - 'examples/valid/openapi.v3.json', - '--doc', - 'coucou', - '--hub', - 'coucou', - '--dry-run', - '--auto-create', - ]) - .it("doesn't try to auto create a documentation"); - }); - }); + }) + }) + }) describe('Successful runs on a directory', () => { - test - .nock('https://bump.sh', (api) => - api - .post( - '/api/v1/versions', - (body) => - // The “bump” slug is taken from the filename convention - // in “bump-api.json” - body.documentation === 'bump' && body.hub === 'my-hub' && !body.branch_name, - ) - .reply(201, { doc_public_url: 'http://localhost/doc/1' }), + it('sends new version to Bump', async () => { + nock('https://bump.sh') + .post( + '/api/v1/versions', + (body) => + // The “bump” slug is taken from the filename convention + // in “bump-api.json” + body.documentation === 'bump' && body.hub === 'my-hub' && !body.branch_name, + ) + .reply(201, {doc_public_url: 'http://localhost/doc/1'}) + + const {stdout} = await runCommand(['deploy', 'examples/valid/', '--hub', 'my-hub'].join(' ')) + expect(stdout).to.contain("We've found 1 valid API definitions to deploy") + expect(stdout).to.contain(' └─ bump-api.json (OpenAPI spec version 3.0.2)') + expect(stdout).to.contain( + 'Your bump documentation...has received a new deployment which will soon be ready at:\nhttp://localhost/doc/1', ) - .stdout() - .stderr() - .command(['deploy', 'examples/valid/', '--hub', 'my-hub']) - .it('sends new version to Bump', ({ stdout }) => { - expect(stdout).to.contain( - 'Your new documentation version will soon be ready at http://localhost/doc/1', - ); - }); - }); + }) + }) describe('Successful runs with a URL', () => { - test - .nock('https://bump.sh', (api) => - api - .post( - '/api/v1/versions', - (body) => body.documentation === 'coucou' && !body.branch_name, - ) - .reply(201, { doc_public_url: 'http://localhost/doc/1' }), + it('sends new version to Bump', async () => { + nock('https://bump.sh') + .post('/api/v1/versions', (body) => body.documentation === 'coucou' && !body.branch_name) + .reply(201, {doc_public_url: 'http://localhost/doc/1'}) + + nock('https://developers.bump.sh') + .get('/source.json') + .replyWithFile(200, 'examples/valid/asyncapi.no-refs.v2.yml', { + 'Content-Type': 'application/json', + }) + + const {stdout} = await runCommand( + ['deploy', 'https://developers.bump.sh/source.json', '--doc', 'coucou'].join(' '), ) - .nock('https://developers.bump.sh', (api) => - api - .get('/source.json') - .replyWithFile(200, 'examples/valid/asyncapi.no-refs.v2.yml', { - 'Content-Type': 'application/json', - }), + expect(stdout).to.contain( + 'Your coucou documentation...has received a new deployment which will soon be ready at:\nhttp://localhost/doc/1', ) - .stdout() - .stderr() - .command(['deploy', 'https://developers.bump.sh/source.json', '--doc', 'coucou']) - .it('sends new version to Bump', ({ stdout }) => { - expect(stdout).to.contain( - 'Your new documentation version will soon be ready at http://localhost/doc/1', - ); - }); - }); + }) + }) describe('Server errors', () => { describe('Authentication error', () => { - test - .nock('https://bump.sh', (api) => api.post('/api/v1/versions').reply(401)) - .stdout() - .stderr() - .command(['deploy', 'examples/valid/openapi.v3.json', '--doc', 'coucou']) - .catch((err) => { - expect(err.message).to.contain('not allowed to deploy'); - throw err; - }) - .exit(101) - .it("Doesn't create a deployed version", ({ stdout }) => { - expect(stdout).to.not.contain( - 'Your new documentation version will soon be ready', - ); - }); - }); + it("Doesn't create a deployed version", async () => { + nock('https://bump.sh').post('/api/v1/versions').reply(401) + + const {error, stdout} = await runCommand( + ['deploy', 'examples/valid/openapi.v3.json', '--doc', 'coucou'].join(' '), + ) + expect(error?.message).to.contain('not allowed to deploy') + expect(error?.oclif?.exit).to.equal(101) + expect(stdout).to.not.contain( + 'Your coucou documentation...has received a new deployment which will soon be ready', + ) + }) + }) describe('Not found error', () => { - test - .nock('https://bump.sh', (api) => api.post('/api/v1/versions').reply(404)) - .stdout() - .stderr() - .command(['deploy', 'examples/valid/openapi.v3.json', '--doc', 'coucou']) - .catch((err) => { - expect(err.message).to.contain( - "It seems the documentation provided doesn't exist", - ); - throw err; - }) - .exit(104) - .it("Doesn't create a deployed version", ({ stdout }) => { - expect(stdout).to.not.contain( - 'Your new documentation version will soon be ready', - ); - }); - }); + it("Doesn't create a deployed version", async () => { + nock('https://bump.sh').post('/api/v1/versions').reply(404) + + const {error, stdout} = await runCommand( + ['deploy', 'examples/valid/openapi.v3.json', '--doc', 'coucou'].join(' '), + ) + expect(error?.message).to.contain("It seems the documentation provided doesn't exist") + expect(error?.oclif?.exit).to.equal(104) + expect(stdout).to.not.contain( + 'Your coucou documentation...has received a new deployment which will soon be ready', + ) + }) + }) describe('Invalid dry-run deploy', () => { - test - .nock('https://bump.sh', (api) => api.post('/api/v1/validations').reply(422)) - .stdout() - .stderr() - .command([ - 'deploy', - 'examples/invalid/asyncapi.yml', - '--doc', - 'coucou', - '--dry-run', - ]) - .catch((err) => { - expect(err.message).to.contain('Invalid definition file'); - throw err; - }) - .exit(122) - .it('warns user about the invalid version with details'); - }); - }); + it('warns user about the invalid version with details', async () => { + nock('https://bump.sh').post('/api/v1/validations').reply(422) + + const {error} = await runCommand( + ['deploy', 'examples/invalid/asyncapi.yml', '--doc', 'coucou', '--dry-run'].join(' '), + ) + expect(error?.message).to.contain('Invalid definition file') + expect(error?.oclif?.exit).to.equal(122) + }) + }) + }) describe('User bad usages', () => { - test - .command(['deploy', 'FILE', '--doc', 'coucou']) - .catch((err) => expect(err.message).to.match(/no such file or directory/)) - .it('Fails deploying an inexistant file'); - - test - .command(['deploy']) - .exit(2) - .it('exits with status 2 when no file argument is provided'); - - test - .command(['deploy', 'examples/valid/openapi.v3.json']) - .catch((err) => expect(err.message).to.match(/missing required flag(.|\n)+--doc/im)) - .it('fails when no documentation id or slug is provided'); - - test - .env({ BUMP_TOKEN: '' }, { clear: true }) - .command(['deploy', 'examples/valid/openapi.v3.json', '--doc', 'coucou']) - .catch((err) => - expect(err.message).to.match(/missing required flag(.|\n)+--token/im), - ) - .it('fails when no access token is provided'); - }); -}); + it('Fails deploying an inexistant file', async () => { + const {error} = await runCommand(['deploy', 'FILE', '--doc', 'coucou'].join(' ')) + expect(error?.message).to.match(/no such file or directory/) + expect(error?.oclif?.exit).to.equal(2) + }) + + it('exits with status 2 when no file argument is provided', async () => { + const {error} = await runCommand(['deploy'].join(' ')) + expect(error?.oclif?.exit).to.equal(2) + }) + + it('fails when no documentation id or slug is provided', async () => { + const {error} = await runCommand(['deploy', 'examples/valid/openapi.v3.json'].join(' ')) + expect(error?.message).to.contain('Missing required flag --doc=') + expect(error?.oclif?.exit).to.equal(2) + }) + + it('fails when no access token is provided', async () => { + // Mock env variables BUMP_TOKEN + process.env.BUMP_TOKEN = process.env.BUMP_TOKEN || '' + const stubs = [] + stubs.push(stub(process.env, 'BUMP_TOKEN').value('')) + + const {error} = await runCommand(['deploy', 'examples/valid/openapi.v3.json', '--doc', 'coucou'].join(' ')) + expect(error?.message).to.contain('Missing required flag token') + expect(error?.oclif?.exit).to.equal(2) + + stubs.map((s) => s.restore()) + }) + }) +}) diff --git a/test/commands/diff.test.ts b/test/commands/diff.test.ts index dbac582e..8eb42135 100644 --- a/test/commands/diff.test.ts +++ b/test/commands/diff.test.ts @@ -1,387 +1,266 @@ -import base, { expect } from '@oclif/test'; -import nock from 'nock'; -import * as sinon from 'sinon'; -import { Diff } from '../../lib/core/diff'; +import {runCommand} from '@oclif/test' +import {expect} from 'chai' +import nock from 'nock' +import {stub} from 'sinon' -nock.disableNetConnect(); +nock.disableNetConnect() -const test = base.env({ BUMP_TOKEN: 'BAR' }); +process.env.BUMP_TOKEN = process.env.BUMP_TOKEN || 'BAR' +process.env.BUMP_POLLING_PERIOD = '0' describe('diff subcommand', () => { - let pollingStub: sinon.SinonStub; - beforeEach(() => { - pollingStub = sinon.stub(Diff.prototype, 'pollingPeriod'); - pollingStub.get(() => 0); - }); + describe('Successful runs', () => { + it('asks for a diff to Bump and returns the newly created version', async () => { + nock('https://bump.sh') + .post('/api/v1/versions', (body) => body.documentation === 'coucou' && !body.branch_name) + .once() + .reply(201, {doc_public_url: 'http://localhost/doc/1', id: '123'}) + .get('/api/v1/versions/123') + .once() + .reply(202) + .get('/api/v1/versions/123') + .once() + .reply(200, {diff_summary: 'Updated: POST /versions'}) - afterEach(() => { - pollingStub.restore(); - }); + const {stderr, stdout} = await runCommand(['diff', 'examples/valid/openapi.v3.json', '--doc', 'coucou'].join(' ')) + expect(stderr).to.match(/Comparing the given definition file/) + expect(stdout).to.contain('Updated: POST /versions') + }) - describe('Successful runs', () => { - test - .nock('https://bump.sh', (api) => { - api - .post( - '/api/v1/versions', - (body) => body.documentation === 'coucou' && !body.branch_name, - ) - .once() - .reply(201, { id: '123', doc_public_url: 'http://localhost/doc/1' }) - .get('/api/v1/versions/123') - .once() - .reply(202) - .get('/api/v1/versions/123') - .once() - .reply(200, { diff_summary: 'Updated: POST /versions' }); - }) - .stdout() - .stderr() - .command(['diff', 'examples/valid/openapi.v3.json', '--doc', 'coucou']) - .it( - 'asks for a diff to Bump and returns the newly created version', - async ({ stdout, stderr }) => { - expect(stderr).to.match(/Comparing the given definition file/); - expect(stdout).to.contain('Updated: POST /versions'); - }, - ); - - test - .nock('https://bump.sh', (api) => { - api - .post( - '/api/v1/versions', - (body) => body.documentation === 'coucou' && !body.branch_name, - ) - .once() - .reply(201, { id: '123', doc_public_url: 'http://localhost/doc/1' }) - .get('/api/v1/versions/123') - .once() - .reply(202) - .get('/api/v1/versions/123') - .once() - .reply(200, { diff_details: [] }); - }) - .stdout() - .stderr() - .command([ - 'diff', - 'examples/valid/openapi.v3.json', - '--doc', - 'coucou', - '--format', - 'json', - ]) - .it( - 'asks for a diff to Bump and returns the newly created version (no content change)', - async ({ stdout, stderr }) => { - expect(stderr).to.eq(''); - expect(stdout).to.contain('[]'); - }, - ); - - test - .nock('https://bump.sh', (api) => { - api - .post( - '/api/v1/versions', - (body) => body.documentation === 'coucou' && body.branch_name === 'next', - ) - .once() - .reply(201, { id: '123', doc_public_url: 'http://localhost/doc/1' }) - .get('/api/v1/versions/123') - .once() - .reply(202) - .get('/api/v1/versions/123') - .once() - .reply(200, { diff_summary: 'Updated: POST /versions' }); - }) - .stdout() - .stderr() - .command([ - 'diff', - 'examples/valid/openapi.v3.json', - '--doc', - 'coucou', - '--branch', - 'next', - ]) - .it( - 'asks for a diff to Bump on given branch and returns the newly created version', - async ({ stdout, stderr }) => { - expect(stderr).to.match(/Comparing the given definition file/); - expect(stdout).to.contain('Updated: POST /versions'); - }, - ); - - test - .nock('https://bump.sh', (api) => { - api - .post('/api/v1/versions') - .once() - .reply(201, { id: '123', doc_public_url: 'http://localhost/doc/1' }) - .post('/api/v1/versions', (body) => body.previous_version_id === '123') - .once() - .reply(201, { id: '321', doc_public_url: 'http://localhost/doc/1' }) - .get('/api/v1/versions/321') - .once() - .reply(202) - .get('/api/v1/versions/321') - .once() - .reply(200, { diff_summary: 'Updated: POST /versions' }); - }) - .stdout() - .stderr() - .command([ - 'diff', - 'examples/valid/openapi.v3.json', - 'examples/valid/openapi.v2.json', - '--doc', - 'coucou', - ]) - .it('asks for a diff between the two files to Bump', async ({ stdout, stderr }) => { - expect(stderr).to.match(/Comparing the two given definition files/); - expect(stdout).to.contain('Updated: POST /versions'); - }); - - test - .nock('https://bump.sh', (api) => { - api - .post('/api/v1/versions') - .once() - .reply(204) - .post('/api/v1/versions') - .once() - .reply(201, { id: '321', doc_public_url: 'http://localhost/doc/1' }) - .get('/api/v1/versions/321') - .once() - .reply(202) - .get('/api/v1/versions/321') - .once() - .reply(200, { diff_summary: 'Updated: POST /versions' }); - }) - .stdout() - .stderr() - .command([ - 'diff', - 'examples/valid/openapi.v3.json', - 'examples/valid/openapi.v2.json', - '--doc', - 'coucou', - ]) - .it( - 'asks for a diff between the two files to Bump even when first file has no changes compared to currently deployed version', - async ({ stdout, stderr }) => { - expect(stderr).to.match(/Comparing the two given definition files/); - expect(stdout).to.contain('Updated: POST /versions'); - }, - ); - - test - .nock('https://bump.sh', (api) => { - api - .post('/api/v1/versions') - .once() - .reply(201, { id: '321', doc_public_url: 'http://localhost/doc/1' }) - .post('/api/v1/versions') - .once() - .reply(204); - }) - .stdout() - .stderr() - .command([ - 'diff', - 'examples/valid/openapi.v3.json', - 'examples/valid/openapi.v2.json', - '--doc', - 'coucou', - ]) - .it( - "doesn't display any diff when second file has no changes", - async ({ stdout, stderr }) => { - expect(stderr).to.match(/Comparing the two given definition files/); - expect(stdout).to.not.contain('Updated: POST /versions'); - }, - ); - - test - .nock('https://bump.sh', (api) => { - api - .post('/api/v1/diffs') - .once() - .reply(201, { - id: '321abc', - public_url: 'http://localhost/preview/321abc', - }) - .get('/api/v1/diffs/321abc?formats[]=text') - .once() - .reply(202) - .get('/api/v1/diffs/321abc?formats[]=text') - .once() - .reply(200, { diff_summary: 'Updated: POST /versions' }); - }) - .stdout() - .stderr() - .command([ - 'diff', - 'examples/valid/openapi.v3.json', - 'examples/valid/openapi.v2.json', - ]) - .it( - 'asks for a public diff between the two files to Bump', - async ({ stdout, stderr }) => { - expect(stderr).to.match(/Comparing the two given definition files/); - expect(stdout).to.contain('Updated: POST /versions'); - }, - ); - - base - .env({ CI: '1' }) - .nock('https://bump.sh', (api) => { - api - .post('/api/v1/diffs') - .once() - .reply(201, { - id: '321abc', - public_url: 'http://localhost/preview/321abc', - }) - .get('/api/v1/diffs/321abc?formats[]=text') - .once() - .reply(202) - .get('/api/v1/diffs/321abc?formats[]=text') - .once() - .reply(200, { diff_summary: 'Updated: POST /versions', diff_breaking: true }); - }) - .stdout() - .stderr() - .command([ - 'diff', - 'examples/valid/openapi.v3.json', - 'examples/valid/openapi.v2.json', - ]) - .exit(1) - .it( - 'asks for a public diff between the two files to Bump and exit 1 due to breaking change', - async ({ stdout, stderr }) => { - expect(stderr).to.match(/Comparing the two given definition files/); - expect(stdout).to.contain('Updated: POST /versions'); - }, - ); - - test - .nock('https://bump.sh', (api) => { - api - .post('/api/v1/versions') - .once() - .reply(201, { id: '123', doc_public_url: 'http://localhost/doc/1' }) - .get('/api/v1/versions/123') - .once() - .reply(200, {}); - }) - .stdout() - .stderr() - .command(['diff', 'examples/valid/openapi.v3.json', '--doc', 'coucou']) - .it('asks for a diff with content change only', async ({ stdout, stderr }) => { - expect(stderr).to.match(/Comparing the given definition file/); - expect(stdout).to.contain('No structural changes detected.'); - }); - - test - .nock('https://bump.sh', (api) => { - api.post('/api/v1/versions').once().reply(204, {}); - }) - .stdout() - .stderr() - .command(['diff', 'examples/valid/openapi.v3.json', '--doc', 'coucou']) - .it('notifies an unchanged definition', async ({ stdout, stderr }) => { - expect(stderr).to.match(/Comparing the given definition file/); - expect(stdout).to.contain('No changes detected.'); - }); - }); + it('asks for a diff to Bump and returns the newly created version (no content change)', async () => { + nock('https://bump.sh') + .post('/api/v1/versions', (body) => body.documentation === 'coucou' && !body.branch_name) + .once() + .reply(201, {doc_public_url: 'http://localhost/doc/1', id: '123'}) + .get('/api/v1/versions/123') + .once() + .reply(202) + .get('/api/v1/versions/123') + .once() + .reply(200, {diff_details: []}) + + const {stderr, stdout} = await runCommand( + ['diff', 'examples/valid/openapi.v3.json', '--doc', 'coucou', '--format', 'json'].join(' '), + ) + expect(stderr).to.eq('') + expect(stdout).to.contain('[]') + }) + + it('asks for a diff to Bump on given branch and returns the newly created version', async () => { + nock('https://bump.sh') + .post('/api/v1/versions', (body) => body.documentation === 'coucou' && body.branch_name === 'next') + .once() + .reply(201, {doc_public_url: 'http://localhost/doc/1', id: '123'}) + .get('/api/v1/versions/123') + .once() + .reply(202) + .get('/api/v1/versions/123') + .once() + .reply(200, {diff_summary: 'Updated: POST /versions'}) + + const {stderr, stdout} = await runCommand( + ['diff', 'examples/valid/openapi.v3.json', '--doc', 'coucou', '--branch', 'next'].join(' '), + ) + expect(stderr).to.match(/Comparing the given definition file/) + expect(stdout).to.contain('Updated: POST /versions') + }) + + it('asks for a diff between the two files to Bump', async () => { + nock('https://bump.sh') + .post('/api/v1/versions') + .once() + .reply(201, {doc_public_url: 'http://localhost/doc/1', id: '123'}) + .post('/api/v1/versions', (body) => body.previous_version_id === '123') + .once() + .reply(201, {doc_public_url: 'http://localhost/doc/1', id: '321'}) + .get('/api/v1/versions/321') + .once() + .reply(202) + .get('/api/v1/versions/321') + .once() + .reply(200, {diff_summary: 'Updated: POST /versions'}) + + const {stderr, stdout} = await runCommand( + ['diff', 'examples/valid/openapi.v3.json', 'examples/valid/openapi.v2.json', '--doc', 'coucou'].join(' '), + ) + expect(stderr).to.match(/Comparing the two given definition files/) + expect(stdout).to.contain('Updated: POST /versions') + }) + + it('asks for a diff between the two files to Bump even when first file has no changes compared to currently deployed version', async () => { + nock('https://bump.sh') + .post('/api/v1/versions') + .once() + .reply(204) + .post('/api/v1/versions') + .once() + .reply(201, {doc_public_url: 'http://localhost/doc/1', id: '321'}) + .get('/api/v1/versions/321') + .once() + .reply(202) + .get('/api/v1/versions/321') + .once() + .reply(200, {diff_summary: 'Updated: POST /versions'}) + + const {stderr, stdout} = await runCommand( + ['diff', 'examples/valid/openapi.v3.json', 'examples/valid/openapi.v2.json', '--doc', 'coucou'].join(' '), + ) + expect(stderr).to.match(/Comparing the two given definition files/) + expect(stdout).to.contain('Updated: POST /versions') + }) + + it("doesn't display any diff when second file has no changes", async () => { + nock('https://bump.sh') + .post('/api/v1/versions') + .once() + .reply(201, {doc_public_url: 'http://localhost/doc/1', id: '321'}) + .post('/api/v1/versions') + .once() + .reply(204) + + const {stderr, stdout} = await runCommand( + ['diff', 'examples/valid/openapi.v3.json', 'examples/valid/openapi.v2.json', '--doc', 'coucou'].join(' '), + ) + expect(stderr).to.match(/Comparing the two given definition files/) + expect(stdout).to.not.contain('Updated: POST /versions') + }) + + it('asks for a public diff between the two files to Bump', async () => { + nock('https://bump.sh') + .post('/api/v1/diffs') + .once() + .reply(201, { + id: '321abc', + public_url: 'http://localhost/preview/321abc', + }) + .get('/api/v1/diffs/321abc?formats[]=text') + .once() + .reply(202) + .get('/api/v1/diffs/321abc?formats[]=text') + .once() + .reply(200, {diff_summary: 'Updated: POST /versions'}) + + const {stderr, stdout} = await runCommand( + ['diff', 'examples/valid/openapi.v3.json', 'examples/valid/openapi.v2.json'].join(' '), + ) + expect(stderr).to.match(/Comparing the two given definition files/) + expect(stdout).to.contain('Updated: POST /versions') + }) + + it('asks for a public diff between the two files to Bump and exit 1 due to breaking change', async () => { + // Mock env variables CI + process.env.CI = process.env.CI || '' + const stubs = [] + stubs.push(stub(process.env, 'CI').value('1')) + nock('https://bump.sh') + .post('/api/v1/diffs') + .once() + .reply(201, { + id: '321abc', + public_url: 'http://localhost/preview/321abc', + }) + .get('/api/v1/diffs/321abc?formats[]=text') + .once() + .reply(202) + .get('/api/v1/diffs/321abc?formats[]=text') + .once() + .reply(200, {diff_breaking: true, diff_summary: 'Updated: POST /versions'}) + + const {error, stderr, stdout} = await runCommand( + ['diff', 'examples/valid/openapi.v3.json', 'examples/valid/openapi.v2.json'].join(' '), + ) + expect(stderr).to.match(/Comparing the two given definition files/) + expect(stdout).to.contain('Updated: POST /versions') + expect(error?.oclif?.exit).to.equal(1) + }) + + it('asks for a diff with content change only', async () => { + nock('https://bump.sh') + .post('/api/v1/versions') + .once() + .reply(201, {doc_public_url: 'http://localhost/doc/1', id: '123'}) + .get('/api/v1/versions/123') + .once() + .reply(200, {}) + + const {stderr, stdout} = await runCommand(['diff', 'examples/valid/openapi.v3.json', '--doc', 'coucou'].join(' ')) + expect(stderr).to.match(/Comparing the given definition file/) + expect(stdout).to.contain('No structural changes detected.') + }) + + it('notifies an unchanged definition', async () => { + nock('https://bump.sh').post('/api/v1/versions').once().reply(204, {}) + + const {stderr, stdout} = await runCommand(['diff', 'examples/valid/openapi.v3.json', '--doc', 'coucou'].join(' ')) + expect(stderr).to.match(/Comparing the given definition file/) + expect(stdout).to.contain('No changes detected.') + }) + }) describe('Server errors', () => { describe('Authentication error', () => { - test - .nock('https://bump.sh', (api) => api.post('/api/v1/versions').reply(401)) - .stdout() - .stderr() - .command(['diff', 'examples/valid/openapi.v3.json', '--doc', 'coucou']) - .catch((err) => { - expect(err.message).to.contain('not allowed to deploy'); - throw err; - }) - .exit(101) - .it("Doesn't create a diff version"); - }); + it("Doesn't create a diff version", async () => { + nock('https://bump.sh').post('/api/v1/versions').reply(401) + const {error} = await runCommand(['diff', 'examples/valid/openapi.v3.json', '--doc', 'coucou'].join(' ')) + expect(error?.message).to.contain('not allowed to deploy') + expect(error?.oclif?.exit).to.equal(101) + }) + }) describe('Not found error', () => { - test - .nock('https://bump.sh', (api) => api.post('/api/v1/versions').reply(404)) - .stdout() - .stderr() - .command(['deploy', 'examples/valid/openapi.v3.json', '--doc', 'coucou']) - .catch((err) => { - expect(err.message).to.contain( - "It seems the documentation provided doesn't exist", - ); - throw err; - }) - .exit(104) - .it("Doesn't create a diff version"); - }); + it("Doesn't create a diff version", async () => { + nock('https://bump.sh').post('/api/v1/versions').reply(404) + + const {error} = await runCommand(['deploy', 'examples/valid/openapi.v3.json', '--doc', 'coucou'].join(' ')) + expect(error?.message).to.contain("It seems the documentation provided doesn't exist") + expect(error?.oclif?.exit).to.equal(104) + }) + }) describe('Timeout reached while polling for results', () => { - test - .nock('https://bump.sh', (api) => { - api - .post('/api/v1/versions') - .once() - .reply(201, { id: '123', doc_public_url: 'http://localhost/doc/1' }) - .get('/api/v1/versions/123') - .times(121) - .reply(202); - }) - .stdout() - .stderr() - .command(['diff', 'examples/valid/openapi.v3.json', '--doc', 'coucou']) - .catch((err) => { - expect(err.message).to.contain( - 'We were unable to compute your documentation diff', - ); - throw err; - }) - .exit(2) - .it('asks for a diff to Bump', async ({ stdout, stderr }) => { - expect(stderr).to.match(/Comparing the given definition file/); - expect(stdout).to.eq(''); - }); - }); - }); + it('asks for a diff to Bump', async () => { + nock('https://bump.sh') + .post('/api/v1/versions') + .once() + .reply(201, {doc_public_url: 'http://localhost/doc/1', id: '123'}) + + nock('https://bump.sh').get('/api/v1/versions/123').times(121).reply(202) + + const {error, stderr, stdout} = await runCommand( + ['diff', 'examples/valid/openapi.v3.json', '--doc', 'coucou'].join(' '), + ) + expect(error?.message).to.contain('We were unable to compute your documentation diff') + expect(error?.oclif?.exit).to.equal(2) + expect(stderr).to.match(/Comparing the given definition file/) + expect(stdout).to.eq('') + }) + }) + }) describe('User bad usages', () => { - test - .command(['diff', 'FILE', '--doc', 'coucou']) - .catch((err) => expect(err.message).to.match(/no such file or directory/)) - .it('Fails diff of an inexistant file'); - - test - .command(['diff']) - .exit(2) - .it('exits with status 2 when no file argument is provided'); - - test - .command(['diff', 'examples/valid/openapi.v3.json']) - .catch((err) => - expect(err.message).to.match(/Please provide a second file argument or login/im), - ) - .it('fails when no documentation id or slug is provided'); + it('Fails diff of an inexistant file', async () => { + const {error} = await runCommand(['diff', 'FILE', '--doc', 'coucou'].join(' ')) + expect(error?.message).to.match(/no such file or directory/) + }) - test - .env({ BUMP_TOKEN: '' }, { clear: true }) - .command(['diff', 'examples/valid/openapi.v3.json', '--doc', 'coucou']) - .catch((err) => - expect(err.message).to.match(/Please provide a second file argument or login/im), - ) - .it('fails when no access token is provided'); - }); -}); + it('exits with status 2 when no file argument is provided', async () => { + const {error} = await runCommand(['diff'].join(' ')) + expect(error?.oclif?.exit).to.equal(2) + }) + + it('fails when no documentation id or slug is provided', async () => { + const {error} = await runCommand(['diff', 'examples/valid/openapi.v3.json'].join(' ')) + expect(error?.message).to.match(/please provide a second file argument or login/im) + }) + + it('fails when no access token is provided', async () => { + // Mock env variables BUMP_TOKEN + process.env.BUMP_TOKEN = process.env.BUMP_TOKEN || '' + const stubs = [] + stubs.push(stub(process.env, 'BUMP_TOKEN').value('')) + const {error} = await runCommand(['diff', 'examples/valid/openapi.v3.json', '--doc', 'coucou'].join(' ')) + expect(error?.message).to.match(/please provide a second file argument or login/im) + stubs.map((s) => s.restore()) + }) + }) +}) diff --git a/test/commands/overlay.test.ts b/test/commands/overlay.test.ts index 941f759a..7a7ac07c 100644 --- a/test/commands/overlay.test.ts +++ b/test/commands/overlay.test.ts @@ -1,58 +1,45 @@ -import base, { expect } from '@oclif/test'; -import { rm } from 'node:fs/promises'; - -const test = base; +import {runCommand} from '@oclif/test' +import {expect} from 'chai' +import {rm} from 'node:fs/promises' describe('overlay subcommand', () => { describe('Successfully compute the merged API document with the given overlay', () => { - test - .stdout() - .stderr() - .command([ - 'overlay', - 'examples/valid/openapi.v3.json', - 'examples/valid/overlay.yaml', - ]) - .it('Spits the result to stdout', ({ stdout, stderr }) => { - expect(stderr).to.contain("Let's apply the overlay to the main definition"); + it('Spits the result to stdout', async () => { + const {stderr, stdout} = await runCommand( + ['overlay', 'examples/valid/openapi.v3.json', 'examples/valid/overlay.yaml'].join(' '), + ) + expect(stderr).to.contain("Let's apply the overlay to the main definition") - const overlayedDefinition = JSON.parse(stdout); + const overlayedDefinition = JSON.parse(stdout) - // Target on info description - expect(overlayedDefinition.info.description).to.match( - /Protect Earth's Tree Tracker API/, - ); + // Target on info description + expect(overlayedDefinition.info.description).to.match(/Protect Earth's Tree Tracker API/) - // Target on info contact information - expect(overlayedDefinition.info.contact.email).to.equal('help@protect.earth'); - // Target on all servers - expect(overlayedDefinition.servers.length).to.equal(1); - expect(overlayedDefinition.servers[0].description).to.equal('Production'); - // Target on nodes which have "x-beta":true field - expect(overlayedDefinition.components.schemas.Pong.properties).to.have.all.keys( - 'pong', - ); - }); + // Target on info contact information + expect(overlayedDefinition.info.contact.email).to.equal('help@protect.earth') + // Target on all servers + expect(overlayedDefinition.servers.length).to.equal(1) + expect(overlayedDefinition.servers[0].description).to.equal('Production') + // Target on nodes which have "x-beta":true field + expect(overlayedDefinition.components.schemas.Pong.properties).to.have.all.keys('pong') + }) - test - .do(async () => await rm('tmp/openapi.overlayed.json', { force: true })) - .stdout() - .stderr() - .command([ - 'overlay', - 'examples/valid/openapi.v3.json', - 'examples/valid/overlay.yaml', - '--out', - 'tmp/openapi.overlayed.json', - ]) - .it( - 'Stores the result to the target output file argument', - async ({ stdout, stderr }) => { - expect(stderr).to.contain("Let's apply the overlay to the main definition"); - expect(stdout).to.be.empty; - // Cleanup created file - await rm('tmp/openapi.overlayed.json'); - }, - ); - }); -}); + it('Stores the result to the target output file argument', async () => { + await rm('tmp/openapi.overlayed.json', {force: true}) + const {stderr, stdout} = await runCommand( + [ + 'overlay', + 'examples/valid/openapi.v3.json', + 'examples/valid/overlay.yaml', + '--out', + 'tmp/openapi.overlayed.json', + ].join(' '), + ) + expect(stderr).to.contain("Let's apply the overlay to the main definition") + /* eslint-disable-next-line @typescript-eslint/no-unused-expressions */ + expect(stdout).to.be.empty + // Cleanup created file + await rm('tmp/openapi.overlayed.json') + }) + }) +}) diff --git a/test/commands/preview.test.ts b/test/commands/preview.test.ts index d08fa77a..e35fc81c 100644 --- a/test/commands/preview.test.ts +++ b/test/commands/preview.test.ts @@ -1,124 +1,102 @@ -import nock from 'nock'; -import { expect, test } from '@oclif/test'; +import {runCommand} from '@oclif/test' +import {expect} from 'chai' +import nock from 'nock' -nock.disableNetConnect(); +nock.disableNetConnect() describe('preview subcommand', () => { describe('Successful preview', () => { - test - .nock('https://bump.sh', (api) => - api.post('/api/v1/previews').reply(200, { - id: '123abc-cba321', - expires_at: new Date(), - public_url: 'https://bump.sh/preview/123abc-cba321', - }), - ) - .stdout() - .stderr() - .command(['preview', 'examples/valid/openapi.v3.json']) - .it('Creates a preview from an openapi file', ({ stdout, stderr }) => { - expect(stderr).to.match(/Let's render a preview on Bump.sh... done/); + it('Creates a preview from an openapi file', async () => { + nock('https://bump.sh').post('/api/v1/previews').reply(200, { + expires_at: new Date(), + id: '123abc-cba321', + public_url: 'https://bump.sh/preview/123abc-cba321', + }) + const {stderr, stdout} = await runCommand(['preview', 'examples/valid/openapi.v3.json'].join(' ')) + expect(stderr).to.match(/Let's render a preview on Bump.sh... done/) - expect(stdout).to.match(/preview is visible at/); - expect(stdout).to.match(/https:\/\/bump.sh\/preview\/123abc-cba321/); - }); + expect(stdout).to.match(/preview is visible at/) + expect(stdout).to.match(/https:\/\/bump.sh\/preview\/123abc-cba321/) + }) - test - .nock('https://bump.sh', (api) => - api.post('/api/v1/previews').reply(201, { - id: '123abc-cba321', - expires_at: new Date(), - public_url: 'https://bump.sh/preview/123abc-cba321', - }), - ) - .stdout() - .stderr() - .command(['preview', '--live', 'examples/valid/openapi.v3.json']) - .it('Creates a live preview and waits for file update', ({ stdout, stderr }) => { - expect(stderr).to.match(/Let's render a preview on Bump.sh... done/); + /* Since oclif v4 I couldn't find a way to end the waiting command + * It seems with oclif/test v1 the command was signaled to stop, + * but right now if you try to run the following test the mocha + * process runs forever... + */ + // it('Creates a live preview and waits for file update', async () => { + // nock('https://bump.sh').post('/api/v1/previews').reply(201, { + // expires_at: new Date(), + // id: '123abc-cba321', + // public_url: 'https://bump.sh/preview/123abc-cba321', + // }) + // const {stderr, stdout} = await runCommand(['preview', '--live', 'examples/valid/openapi.v3.json'].join(' ')) + // expect(stderr).to.match(/Let's render a preview on Bump.sh... done/) - expect(stdout).to.match(/preview is visible at/); - expect(stdout).to.match(/https:\/\/bump.sh\/preview\/123abc-cba321/); - expect(stderr).to.match(/Waiting for changes on file/); - }); + // expect(stdout).to.match(/preview is visible at/) + // expect(stdout).to.match(/https:\/\/bump.sh\/preview\/123abc-cba321/) + // expect(stderr).to.match(/Waiting for changes on file/) + // }) - test - .nock('http://example.org', (api) => { - api - .get('/param-lights.json') - .replyWithFile(200, 'examples/valid/params/lights.json', { - 'Content-Type': 'application/json', - }); + it('Creates a preview from an asyncapi file with $refs', async () => { + nock('http://example.org').get('/param-lights.json').replyWithFile(200, 'examples/valid/params/lights.json', { + 'Content-Type': 'application/json', }) - .nock('https://bump.sh', (api) => { - api.post('/api/v1/previews').reply(200, { - id: '123abc-cba321', - expires_at: new Date(), - public_url: 'https://bump.sh/preview/123abc-cba321', - }); + + nock('https://bump.sh').post('/api/v1/previews').reply(200, { + expires_at: new Date(), + id: '123abc-cba321', + public_url: 'https://bump.sh/preview/123abc-cba321', }) - .stdout() - .stderr() - .command(['preview', 'examples/valid/asyncapi.v2.yml']) - .it('Creates a preview from an asyncapi file with $refs', ({ stdout, stderr }) => { - expect(stderr).to.match(/Let's render a preview on Bump.sh... done/); - expect(stdout).to.match(/preview is visible at/); - expect(stdout).to.match(/https:\/\/bump.sh\/preview\/123abc-cba321/); - }); - }); + const {stderr, stdout} = await runCommand(['preview', 'examples/valid/asyncapi.v2.yml'].join(' ')) + expect(stderr).to.match(/Let's render a preview on Bump.sh... done/) + + expect(stdout).to.match(/preview is visible at/) + expect(stdout).to.match(/https:\/\/bump.sh\/preview\/123abc-cba321/) + }) + }) describe('Server errors', () => { describe('Validation error', () => { - test - .nock('https://bump.sh', (api) => - api.post('/api/v1/previews').reply(422, { - message: 'Invalid definition file', + it('Fails with an error message from the API response', async () => { + nock('https://bump.sh') + .post('/api/v1/previews') + .reply(422, { errors: { raw_definition: 'failed schema #: "openapi" wasn\'t supplied.', }, - }), - ) - .stderr() - .stdout() - .command(['preview', 'examples/valid/openapi.v3.json']) - .catch((err) => { - expect(err.message).to.contain('"openapi" wasn\'t supplied.'); - throw err; - }) - .exit(122) - .it('Fails with an error message from the API response', ({ stdout }) => { - expect(stdout).to.not.match(/preview is visible at/); - }); - }); + message: 'Invalid definition file', + }) + const {error, stdout} = await runCommand(['preview', 'examples/valid/openapi.v3.json'].join(' ')) + expect(error?.oclif?.exit).to.equal(122) + expect(error?.message).to.contain('"openapi" wasn\'t supplied.') + expect(stdout).to.not.match(/preview is visible at/) + }) + }) describe('Server internal error', () => { - test - .nock('https://bump.sh', (api) => api.post('/api/v1/previews').reply(500)) - .stderr() - .stdout() - .command(['preview', 'examples/valid/openapi.v3.json']) - .catch((err) => { - expect(err.message).to.contain('Unhandled API error (status: 500)'); - throw err; - }) - .exit(100) - .it('Fails rendering and displays a generic error', ({ stdout }) => { - expect(stdout).to.not.match(/preview is visible at/); - }); - }); - }); + it('Fails rendering and displays a generic error', async () => { + nock('https://bump.sh').post('/api/v1/previews').reply(500) + + const {error, stdout} = await runCommand(['preview', 'examples/valid/openapi.v3.json'].join(' ')) + expect(error?.oclif?.exit).to.equal(100) + expect(error?.message).to.contain('Unhandled API error (status: 500)') + expect(stdout).to.not.match(/preview is visible at/) + }) + }) + }) describe('User bad usages', () => { - test - .command(['preview', 'FILE']) - .catch((err) => expect(err.message).to.match(/no such file or directory/)) - .it('Fails previewing an inexistant file'); + it('Fails previewing an inexistant file', async () => { + const {error} = await runCommand(['preview', 'FILE'].join(' ')) + expect(error?.message).to.match(/no such file or directory/) + }) - test - .command(['preview']) + it('exits with status 2 when no file argument is provided', async () => { + const {error} = await runCommand(['preview'].join(' ')) // checks to ensure the command exits with status 2 - .exit(2) - .it('exits with status 2 when no file argument is provided'); - }); -}); + expect(error?.oclif?.exit).to.equal(2) + }) + }) +}) diff --git a/test/helpers/init.js b/test/helpers/init.js deleted file mode 100644 index 86f06bf3..00000000 --- a/test/helpers/init.js +++ /dev/null @@ -1,4 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -const path = require('path'); - -process.env.TS_NODE_PROJECT = path.resolve('test/tsconfig.json'); diff --git a/test/helpers/init.ts b/test/helpers/init.ts new file mode 100644 index 00000000..edfb267b --- /dev/null +++ b/test/helpers/init.ts @@ -0,0 +1,4 @@ +import path from 'node:path' + +process.env.TS_NODE_PROJECT = path.resolve('test/tsconfig.json') +process.env.BUMP_HOST = 'bump.sh' diff --git a/test/integration.js b/test/integration.cjs similarity index 92% rename from test/integration.js rename to test/integration.cjs index e23db36f..d73b8c23 100755 --- a/test/integration.js +++ b/test/integration.cjs @@ -1,7 +1,7 @@ 'use strict'; const spawn = require('cross-spawn'); -const cmd = './bin/run'; +const cmd = './bin/run.js'; const tests = [['--version'], ['--help']]; for (let i = 0; i < tests.length; i++) { diff --git a/test/tsconfig.json b/test/tsconfig.json index a4948884..95898fce 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -1,11 +1,9 @@ { - "declaration": false, "extends": "../tsconfig", "compilerOptions": { - "module": "commonjs", - "sourceMap": true + "noEmit": true }, - "ts-node": { - "files": true - } + "references": [ + {"path": ".."} + ] } diff --git a/test/unit/api.test.ts b/test/unit/api.test.ts index 14965b00..988e3b0a 100644 --- a/test/unit/api.test.ts +++ b/test/unit/api.test.ts @@ -1,114 +1,101 @@ -import * as os from 'os'; -import * as path from 'path'; -import * as sinon from 'sinon'; -import base, { expect } from '@oclif/test'; -import * as Config from '@oclif/config'; -import nock from 'nock'; -import chalk from 'chalk'; +import {Config} from '@oclif/core' +import {expect} from 'chai' +import chalk from 'chalk' +import nock from 'nock' +import * as os from 'node:os' +import {spy, stub} from 'sinon' -import { BumpApi } from '../../src/api'; -import { PreviewRequest } from '../../src/api/models'; +import {BumpApi} from '../../src/api' +import {PreviewRequest} from '../../src/api/models' -nock.disableNetConnect(); -const root = path.join(__dirname, '../../'); -const test = base.add('config', () => Config.load(root)); +nock.disableNetConnect() // Force no colors in output messages -chalk.level = 0; +chalk.level = 0 +// Default oclif config from root of repo +const config = await Config.load('../../') describe('BumpApi HTTP client class', () => { describe('nominal authenticated API call', () => { - const matchAuthorizationHeader = sinon.spy(sinon.stub().returns(true)); + it('sends valid Authorization headers', async () => { + const matchAuthorizationHeader = spy(stub().returns(true)) - test - .nock( - 'https://bump.sh', - { - reqheaders: { - Authorization: matchAuthorizationHeader, - }, + nock('https://bump.sh', { + reqheaders: { + Authorization: matchAuthorizationHeader, }, - (api) => api.post('/api/v1/versions').reply(201, {}), - ) - .do( - async (ctx) => - await new BumpApi(ctx.config).postVersion( - { documentation: 'hello', definition: '' }, - 'my-secret-token', - ), - ) - .it('sends valid Authorization headers', async () => { - expect(matchAuthorizationHeader.firstCall.args[0]).to.equal( - 'Basic bXktc2VjcmV0LXRva2Vu', - ); - }); - }); + }) + .post('/api/v1/versions') + .reply(201, {}) + + await new BumpApi(config).postVersion({definition: '', documentation: 'hello'}, 'my-secret-token') + + expect(matchAuthorizationHeader.firstCall.args[0]).to.equal('Basic bXktc2VjcmV0LXRva2Vu') + }) + }) describe('Customizing the API client with env variables', () => { - const matchUserAgentHeader = sinon.spy(sinon.stub().returns(true)); + it('sends User-Agent with custom content', async () => { + // Create a stub for user agent header + const matchUserAgentHeader = spy(stub().returns(true)) - test - .env( - { BUMP_HOST: 'http://localhost', BUMP_USER_AGENT: 'ua-extra-content' }, - { clear: true }, - ) - .nock( - 'http://localhost', + // Mock env variables BUMP_HOST & BUMP_USER_AGENT + process.env.BUMP_HOST = process.env.BUMP_HOST || '' + process.env.BUMP_USER_AGENT = process.env.BUMP_USER_AGENT || '' + const stubs = [ + stub(process.env, 'BUMP_HOST').value('http://localhost'), + stub(process.env, 'BUMP_USER_AGENT').value('ua-extra-content'), + ] + + // Mock HTTP request + nock('http://localhost', { + reqheaders: { + 'User-Agent': matchUserAgentHeader, + }, + }) + .post('/api/v1/versions') + .reply(201, {}) + + // System under test + await new BumpApi(config).postVersion( { - reqheaders: { - 'User-Agent': matchUserAgentHeader, - }, + definition: '', + documentation: 'hello', }, - (api) => api.post('/api/v1/versions').reply(201, {}), + 'token', ) - .do( - async (ctx) => - await new BumpApi(ctx.config).postVersion( - { - documentation: 'hello', - definition: '', - }, - 'token', - ), + + expect(matchUserAgentHeader.firstCall.args[0]).to.match( + new RegExp(`^bump-cli/([0-9.]+)(-[a-z0-9.]+)? ${os.platform()}-${os.arch()} node-v[0-9.]+ ua-extra-content$`), ) - .it('sends User-Agent with custom content', async () => { - expect(matchUserAgentHeader.firstCall.args[0]).to.match( - new RegExp( - `^bump-cli/([0-9\.]+)(-[a-z0-9\.]+)? ${os.platform()}-${os.arch()} node-v[0-9\.]+ ua-extra-content$`, - ), - ); - }); - }); + stubs.map((s) => s.restore()) + }) + }) describe('Handling HTTP errors', () => { - test - .nock('https://bump.sh', (api) => - api.post('/api/v1/previews').reply(422, { + it('displays error information to the user', async () => { + nock('https://bump.sh') + .post('/api/v1/previews') + .reply(422, { errors: { documentation: { slug: 'is invalid', }, - references: [{ location: 'not a filepath' }, { content: 'is invalid' }], param: ['invalid', 'too small', 'wrong'], + references: [{location: 'not a filepath'}, {content: 'is invalid'}], }, - }), - ) - .do( - async (ctx) => - await new BumpApi(ctx.config).postPreview({ - definition: '{}', - } as PreviewRequest), - ) - .catch( - (err) => { - expect(err.message).to.match(/documentation.slug is invalid/); - expect(err.message).to.match( - /references 0.location not a filepath, 1.content is invalid/, - ); + }) - expect(err.message).to.match(/param invalid, too small, wrong/); - }, - { raiseIfNotThrown: false }, - ) - .it('displays error information to the user'); - }); -}); + try { + await new BumpApi(config).postPreview({ + definition: '{}', + } as PreviewRequest) + } catch (error) { + const {message} = error as Error + expect(message).to.match(/documentation.slug is invalid/) + expect(message).to.match(/references 0.location not a filepath, 1.content is invalid/) + + expect(message).to.match(/param invalid, too small, wrong/) + } + }) + }) +}) diff --git a/test/unit/definition.test.ts b/test/unit/definition.test.ts index 30c50ee3..996c1071 100644 --- a/test/unit/definition.test.ts +++ b/test/unit/definition.test.ts @@ -1,205 +1,185 @@ -import { expect, test } from '@oclif/test'; -import { API, APIDefinition } from '../../src/definition'; -import nock from 'nock'; -import path from 'path'; -import * as YAML from '@stoplight/yaml'; +import * as YAML from '@stoplight/yaml' +import {expect} from 'chai' +import nock from 'nock' +import path from 'node:path' -nock.disableNetConnect(); +import {API, APIDefinition} from '../../src/definition' + +nock.disableNetConnect() describe('API class', () => { describe('API.load(..)', () => { describe('with inexistent file', () => { - test - .do(async () => await API.load('FILE')) - .catch( - (err) => { - expect(err.message).to.match(/Error opening file/); - }, - { raiseIfNotThrown: false }, - ) - .it('throws an error'); - }); + it('throws an error', async () => { + try { + await API.load('FILE') + } catch (error) { + const {message} = error as Error + expect(message).to.match(/Error opening file/) + } + }) + }) describe('with no references', () => { - test.it('parses successfully an OpenAPI contract', async () => { - const api = await API.load('examples/valid/openapi.v2.json'); - expect(api.version).to.equal('2.0'); - expect(api.references).to.be.an('array').that.is.empty; - }); - - test.it('parses successfully an AsyncAPI contract', async () => { - const api = await API.load('examples/valid/asyncapi.v2.3.yml'); - expect(api.version).to.equal('2.3.0'); - }); - - test.it('parses successfully an AsyncAPI 2.4 contract', async () => { - nock.enableNetConnect('raw.githubusercontent.com'); + it('parses successfully an OpenAPI contract', async () => { + const api = await API.load('examples/valid/openapi.v2.json') + expect(api.version).to.equal('2.0') + /* eslint-disable-next-line @typescript-eslint/no-unused-expressions */ + expect(api.references).to.be.an('array').that.is.empty + }) + + it('parses successfully an AsyncAPI contract', async () => { + const api = await API.load('examples/valid/asyncapi.v2.3.yml') + expect(api.version).to.equal('2.3.0') + }) + + it('parses successfully an AsyncAPI 2.4 contract', async () => { + nock.enableNetConnect('raw.githubusercontent.com') const api = await API.load( 'https://raw.githubusercontent.com/asyncapi/spec/v2.4.0/examples/streetlights-kafka.yml', - ); - nock.disableNetConnect(); - expect(api.version).to.equal('2.4.0'); - }); + ) + nock.disableNetConnect() + expect(api.version).to.equal('2.4.0') + }) - test.it('parses successfully an AsyncAPI 2.5 contract', async () => { - const api = await API.load('examples/valid/asyncapi.v2.5.yml'); - expect(api.version).to.equal('2.5.0'); - }); - }); + it('parses successfully an AsyncAPI 2.5 contract', async () => { + const api = await API.load('examples/valid/asyncapi.v2.5.yml') + expect(api.version).to.equal('2.5.0') + }) + }) describe('with file & http references', () => { - test - .nock('http://example.org', (api) => api.get('/param-lights.json').reply(200, {})) - .it('parses successfully', async () => { - const api = await API.load('examples/valid/asyncapi.v2.yml'); - expect(api.version).to.equal('2.2.0'); - expect(api.references.length).to.equal(5); - const locations = api.references.map((ref) => ref.location); - expect(locations).to.include('http://example.org/param-lights.json'); + it('parses successfully', async () => { + nock('http://example.org').get('/param-lights.json').reply(200, {}) + + // System under test + const api = await API.load('examples/valid/asyncapi.v2.yml') - expect(locations).to.include(['params', 'streetlightId.json'].join(path.sep)); + expect(api.version).to.equal('2.2.0') + expect(api.references.length).to.equal(5) + const locations = api.references.map((ref) => ref.location) + expect(locations).to.include('http://example.org/param-lights.json') - expect(locations).to.not.include( - ['.', 'params', 'streetlightId.json'].join(path.sep), - ); + expect(locations).to.include(['params', 'streetlightId.json'].join(path.sep)) - expect(locations).to.include(['doc', 'introduction.md'].join(path.sep)); - }); - }); + expect(locations).to.not.include(['.', 'params', 'streetlightId.json'].join(path.sep)) + + expect(locations).to.include(['doc', 'introduction.md'].join(path.sep)) + }) + }) describe('with a relative descendant file path', () => { - test.it('parses successfully', async () => { - const api = await API.load('./examples/valid/openapi.v2.json'); - expect(api.version).to.equal('2.0'); - }); - }); + it('parses successfully', async () => { + const api = await API.load('./examples/valid/openapi.v2.json') + expect(api.version).to.equal('2.0') + }) + }) describe('with a file path containing special characters', () => { - test.it('parses successfully', async () => { - const api = await API.load('./examples/valid/__gitlab-é__.yml'); - expect(api.version).to.equal('3.0.0'); - }); - }); + it('parses successfully', async () => { + const api = await API.load('./examples/valid/__gitlab-é__.yml') + expect(api.version).to.equal('3.0.0') + }) + }) describe('with an http file containing relative URL refs', () => { - test - .nock('http://example.org', (api) => - api - .get('/openapi') - .replyWithFile(200, 'examples/valid/openapi.v3.json', { - 'Content-Type': 'application/json', - }) - .get('/schemas/all.yml') - .replyWithFile(200, 'examples/valid/schemas/all.yml', { - 'Content-Type': 'application/yaml', - }), - ) - .it('parses external file successfully', async () => { - const api = await API.load('http://example.org/openapi'); - expect(api.version).to.equal('3.0.2'); - expect(api.references.map((ref) => ref.location)).to.contain( - ['schemas', 'all.yml'].join(path.sep), - ); - }); - }); + it('parses external file successfully', async () => { + nock('http://example.org') + .get('/openapi') + .replyWithFile(200, 'examples/valid/openapi.v3.json', { + 'Content-Type': 'application/json', + }) + .get('/schemas/all.yml') + .replyWithFile(200, 'examples/valid/schemas/all.yml', { + 'Content-Type': 'application/yaml', + }) + const api = await API.load('http://example.org/openapi') + expect(api.version).to.equal('3.0.2') + expect(api.references.map((ref) => ref.location)).to.contain(['schemas', 'all.yml'].join(path.sep)) + }) + }) describe('with an invalid definition file', () => { - for (const [example, error] of Object.entries({ - './examples/invalid/openapi.yml': 'Unsupported API specification', - './examples/invalid/array.yml': 'Unsupported API specification', - './examples/invalid/string.yml': 'Unsupported API specification', - './examples/valid/asyncapi.v3.yml': 'Unsupported API specification', - })) { - test - .do(async () => await API.load(example)) - .catch( - (err) => { - expect(err.message).to.match(new RegExp(error)); - }, - { raiseIfNotThrown: false }, - ) - .it(`throws an error with details about ${example}`); - } - }); - }); + it(`throws an error with details`, async () => { + for (const [example, error] of Object.entries({ + './examples/invalid/array.yml': 'Unsupported API specification', + './examples/invalid/openapi.yml': 'Unsupported API specification', + './examples/invalid/string.yml': 'Unsupported API specification', + './examples/valid/asyncapi.v3.yml': 'Unsupported API specification', + })) { + try { + /* eslint-disable-next-line no-await-in-loop */ + await API.load(example) + } catch (error_) { + const {message} = error_ as Error + expect(message).to.match(new RegExp(error)) + } + } + }) + }) + }) describe('serializeDefinition()', () => { describe('with no overlay applied', () => { - test.it('returns the rawDefinition, no matter the argument', async () => { - const api = await API.load('examples/valid/openapi.v2.json'); + it('returns the rawDefinition, no matter the argument', async () => { + const api = await API.load('examples/valid/openapi.v2.json') - expect(api.serializeDefinition()).to.equal(api.rawDefinition); - expect(api.serializeDefinition('destination/file.json')).to.equal( - api.rawDefinition, - ); - }); - }); + expect(api.serializeDefinition()).to.equal(api.rawDefinition) + expect(api.serializeDefinition('destination/file.json')).to.equal(api.rawDefinition) + }) + }) describe('with an overlay applied', () => { - test.it('returns the overlayed definition', async () => { - const api = await API.load('examples/valid/openapi.v2.json'); - await api.applyOverlay('examples/valid/overlay.yaml'); + it('returns the overlayed definition', async () => { + const api = await API.load('examples/valid/openapi.v2.json') + await api.applyOverlay('examples/valid/overlay.yaml') - expect(api.serializeDefinition()).to.equal( - JSON.stringify(api.overlayedDefinition), - ); + expect(api.serializeDefinition()).to.equal(JSON.stringify(api.overlayedDefinition)) - expect(api.serializeDefinition('destination/file.yaml')).to.equal( - YAML.safeStringify(api.overlayedDefinition), - ); - }); - }); - }); + expect(api.serializeDefinition('destination/file.yaml')).to.equal(YAML.safeStringify(api.overlayedDefinition)) + }) + }) + }) describe('applyOverlay()', () => { describe('when overlay is valid', () => { - test.it( - 'sets the overlayedDefinition with the given overlay file path', - async () => { - const api = await API.load('examples/valid/openapi.v2.json'); - - expect(api.overlayedDefinition).to.be.undefined; - await api.applyOverlay('examples/valid/overlay.yaml'); - expect(api.overlayedDefinition).to.exist; - expect((api.overlayedDefinition as APIDefinition).info.description).to.match( - /Protect Earth's Tree Tracker API/, - ); - }, - ); - - test - .nock('http://example.org', (api) => - api.get('/source.yaml').replyWithFile(200, 'examples/valid/overlay.yaml', { - 'Content-Type': 'application/yaml', - }), - ) - .it('sets the overlayedDefinition with the given overlay URL', async () => { - const api = await API.load('examples/valid/openapi.v2.json'); - - expect(api.overlayedDefinition).to.be.undefined; - await api.applyOverlay('http://example.org/source.yaml'); - expect(api.overlayedDefinition).to.exist; - expect((api.overlayedDefinition as APIDefinition).info.description).to.match( - /Protect Earth's Tree Tracker API/, - ); - }); - }); + it('sets the overlayedDefinition with the given overlay file path', async () => { + const api = await API.load('examples/valid/openapi.v2.json') + + /* eslint-disable-next-line @typescript-eslint/no-unused-expressions */ + expect(api.overlayedDefinition).to.be.undefined + await api.applyOverlay('examples/valid/overlay.yaml') + /* eslint-disable-next-line @typescript-eslint/no-unused-expressions */ + expect(api.overlayedDefinition).to.exist + expect((api.overlayedDefinition as APIDefinition).info.description).to.match(/Protect Earth's Tree Tracker API/) + }) + + it('sets the overlayedDefinition with the given overlay URL', async () => { + nock('http://example.org').get('/source.yaml').replyWithFile(200, 'examples/valid/overlay.yaml', { + 'Content-Type': 'application/yaml', + }) + + const api = await API.load('examples/valid/openapi.v2.json') + + /* eslint-disable-next-line @typescript-eslint/no-unused-expressions */ + expect(api.overlayedDefinition).to.be.undefined + await api.applyOverlay('http://example.org/source.yaml') + /* eslint-disable-next-line @typescript-eslint/no-unused-expressions */ + expect(api.overlayedDefinition).to.exist + expect((api.overlayedDefinition as APIDefinition).info.description).to.match(/Protect Earth's Tree Tracker API/) + }) + }) describe('when overlay is invalid', () => { - test - .do(async () => { - const api = await API.load('examples/valid/openapi.v2.json'); - await api.applyOverlay('examples/valid/openapi.v2.json'); - }) - .catch( - (err) => { - expect(err.message).to.match( - /examples\/valid\/openapi.v2.json does not look like an OpenAPI overlay/, - ); - }, - { raiseIfNotThrown: false }, - ) - .it('throws an error'); - }); - }); -}); + it('throws an error', async () => { + const api = await API.load('examples/valid/openapi.v2.json') + try { + await api.applyOverlay('examples/valid/openapi.v2.json') + } catch (error) { + const {message} = error as Error + expect(message).to.match(/examples\/valid\/openapi.v2.json does not look like an OpenAPI overlay/) + } + }) + }) + }) +}) diff --git a/tsconfig.json b/tsconfig.json index 045aede5..901420d2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,16 +1,19 @@ { "compilerOptions": { "declaration": true, - "importHelpers": true, - "module": "commonjs", - "outDir": "lib", + "module": "nodenext", + "outDir": "dist", "rootDir": "src", "strict": true, - "target": "es2017", - "esModuleInterop": true + "target": "es2022", + "moduleResolution": "node16", + "resolveJsonModule": true }, "include": [ - "src/**/*", + "./src/**/*", "typings.d.ts" - ] + ], + "ts-node": { + "esm": true + } } diff --git a/typings.d.ts b/typings.d.ts index 5d69ffab..950bba18 100644 --- a/typings.d.ts +++ b/typings.d.ts @@ -1,11 +1,11 @@ // oas-schemas doesn't define TS types -declare module 'oas-schemas'; +declare module 'oas-schemas' // mergician doesn't define TS types -declare module 'mergician'; - -// Internals of json-schema-ref-parser doesn't expose types -declare module '@apidevtools/json-schema-ref-parser/lib/options'; +declare module 'mergician' // Load repo root level package.json file -declare module '*.json'; +declare module '*.json' + +// object-treeify doesn't define TS types +declare module 'object-treeify'