From 909d524de56131b737f7d865daaaf21707336d9e Mon Sep 17 00:00:00 2001 From: Deepanshu Dhruw Date: Mon, 28 Mar 2022 13:56:15 +0530 Subject: [PATCH] Feature: hopp-cli in TypeScript (#2074) Co-authored-by: Andrew Bastin Co-authored-by: liyasthomas Co-authored-by: Gita Alekhya Paul --- packages/hoppscotch-cli/.gitignore | 146 ++++++++ packages/hoppscotch-cli/.prettierrc | 8 + packages/hoppscotch-cli/CODE_OF_CONDUCT.md | 128 +++++++ packages/hoppscotch-cli/CONTRIBUTING.md | 57 +++ packages/hoppscotch-cli/LICENSE | 21 ++ packages/hoppscotch-cli/README.md | 52 +++ packages/hoppscotch-cli/bin/hopp | 3 + packages/hoppscotch-cli/package.json | 45 +++ packages/hoppscotch-cli/src/commands/test.ts | 24 ++ packages/hoppscotch-cli/src/handlers/error.ts | 85 +++++ packages/hoppscotch-cli/src/index.ts | 60 +++ .../hoppscotch-cli/src/interfaces/request.ts | 36 ++ .../hoppscotch-cli/src/interfaces/response.ts | 68 ++++ packages/hoppscotch-cli/src/tsconfig.json | 19 + packages/hoppscotch-cli/src/types/errors.ts | 36 ++ packages/hoppscotch-cli/src/types/request.ts | 31 ++ packages/hoppscotch-cli/src/types/response.ts | 28 ++ packages/hoppscotch-cli/src/utils/checks.ts | 155 ++++++++ .../hoppscotch-cli/src/utils/collections.ts | 129 +++++++ .../hoppscotch-cli/src/utils/constants.ts | 7 + packages/hoppscotch-cli/src/utils/display.ts | 145 ++++++++ .../src/utils/functions/array.ts | 37 ++ packages/hoppscotch-cli/src/utils/getters.ts | 113 ++++++ packages/hoppscotch-cli/src/utils/mutators.ts | 80 ++++ .../hoppscotch-cli/src/utils/pre-request.ts | 268 ++++++++++++++ packages/hoppscotch-cli/src/utils/request.ts | 321 ++++++++++++++++ packages/hoppscotch-cli/src/utils/test.ts | 197 ++++++++++ packages/hoppscotch-cli/tsconfig.json | 16 + packages/hoppscotch-cli/tsup.config.ts | 19 + packages/hoppscotch-js-sandbox/jest.config.js | 2 +- packages/hoppscotch-js-sandbox/package.json | 19 +- packages/hoppscotch-js-sandbox/src/demo.ts | 53 --- packages/hoppscotch-js-sandbox/src/index.ts | 5 +- packages/hoppscotch-js-sandbox/tsconfig.json | 4 +- packages/hoppscotch-js-sandbox/tsup.config.ts | 11 + pnpm-lock.yaml | 345 +++++++++++++++--- 36 files changed, 2654 insertions(+), 119 deletions(-) create mode 100644 packages/hoppscotch-cli/.gitignore create mode 100644 packages/hoppscotch-cli/.prettierrc create mode 100644 packages/hoppscotch-cli/CODE_OF_CONDUCT.md create mode 100644 packages/hoppscotch-cli/CONTRIBUTING.md create mode 100644 packages/hoppscotch-cli/LICENSE create mode 100644 packages/hoppscotch-cli/README.md create mode 100755 packages/hoppscotch-cli/bin/hopp create mode 100644 packages/hoppscotch-cli/package.json create mode 100644 packages/hoppscotch-cli/src/commands/test.ts create mode 100644 packages/hoppscotch-cli/src/handlers/error.ts create mode 100644 packages/hoppscotch-cli/src/index.ts create mode 100644 packages/hoppscotch-cli/src/interfaces/request.ts create mode 100644 packages/hoppscotch-cli/src/interfaces/response.ts create mode 100644 packages/hoppscotch-cli/src/tsconfig.json create mode 100644 packages/hoppscotch-cli/src/types/errors.ts create mode 100644 packages/hoppscotch-cli/src/types/request.ts create mode 100644 packages/hoppscotch-cli/src/types/response.ts create mode 100644 packages/hoppscotch-cli/src/utils/checks.ts create mode 100644 packages/hoppscotch-cli/src/utils/collections.ts create mode 100644 packages/hoppscotch-cli/src/utils/constants.ts create mode 100644 packages/hoppscotch-cli/src/utils/display.ts create mode 100644 packages/hoppscotch-cli/src/utils/functions/array.ts create mode 100644 packages/hoppscotch-cli/src/utils/getters.ts create mode 100644 packages/hoppscotch-cli/src/utils/mutators.ts create mode 100644 packages/hoppscotch-cli/src/utils/pre-request.ts create mode 100644 packages/hoppscotch-cli/src/utils/request.ts create mode 100644 packages/hoppscotch-cli/src/utils/test.ts create mode 100644 packages/hoppscotch-cli/tsconfig.json create mode 100644 packages/hoppscotch-cli/tsup.config.ts delete mode 100644 packages/hoppscotch-js-sandbox/src/demo.ts create mode 100644 packages/hoppscotch-js-sandbox/tsup.config.ts diff --git a/packages/hoppscotch-cli/.gitignore b/packages/hoppscotch-cli/.gitignore new file mode 100644 index 000000000..a0bc82af9 --- /dev/null +++ b/packages/hoppscotch-cli/.gitignore @@ -0,0 +1,146 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/node +# Edit at https://www.toptal.com/developers/gitignore?templates=node + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test +.env.production + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# End of https://www.toptal.com/developers/gitignore/api/node + +# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode +# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode diff --git a/packages/hoppscotch-cli/.prettierrc b/packages/hoppscotch-cli/.prettierrc new file mode 100644 index 000000000..5c6bc9472 --- /dev/null +++ b/packages/hoppscotch-cli/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": false, + "printWidth": 80, + "useTabs": false, + "tabWidth": 2 +} diff --git a/packages/hoppscotch-cli/CODE_OF_CONDUCT.md b/packages/hoppscotch-cli/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..0d07723c8 --- /dev/null +++ b/packages/hoppscotch-cli/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or + advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email + address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +support@hoppscotch.io. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/packages/hoppscotch-cli/CONTRIBUTING.md b/packages/hoppscotch-cli/CONTRIBUTING.md new file mode 100644 index 000000000..d244e051d --- /dev/null +++ b/packages/hoppscotch-cli/CONTRIBUTING.md @@ -0,0 +1,57 @@ +# Contributing + +When contributing to this repository, please first discuss the change you wish to make via issue, +email, or any other method with the owners of this repository before making a change. + +Please note we have a code of conduct, please follow it in all your interactions with the project. + +## Pull Request Process + +1. Ensure any install or build dependencies are removed before the end of the layer when doing a + build. +2. Update the README.md with details of changes to the interface, this includes new environment + variables, exposed ports, useful file locations and container parameters. +3. Increase the version numbers in any examples files and the README.md to the new version that this + Pull Request would represent. The versioning scheme we use is [SemVer](https://semver.org). +4. You may merge the Pull Request once you have the sign-off of two other developers, or if you + do not have permission to do that, you may request the second reviewer merge it for you. + +## Set Up The Development Environment + +1. After cloning the repository, execute the following commands: + + ```bash + pnpm install + pnpm run build + ``` + +2. In order to test locally, you can use two types of package linking: + + 1. The 'pnpx' way (preferred since it does not hamper your original installation of the CLI): + + ```bash + pnpm link @hoppscotch/cli + + // Then to use or test the CLI: + pnpx hopp + + // After testing, to remove the package linking: + pnpm rm @hoppscotch/cli + ``` + + 2. The 'global' way (warning: this might override the globally installed CLI, if exists): + + ```bash + sudo pnpm link --global + + // Then to use or test the CLI: + hopp + + // After testing, to remove the package linking: + sudo pnpm rm --global @hoppscotch/cli + ``` + +3. To use the Typescript watch scripts: + ```bash + pnpm run dev + ``` diff --git a/packages/hoppscotch-cli/LICENSE b/packages/hoppscotch-cli/LICENSE new file mode 100644 index 000000000..4d209962a --- /dev/null +++ b/packages/hoppscotch-cli/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/hoppscotch-cli/README.md b/packages/hoppscotch-cli/README.md new file mode 100644 index 000000000..f1a70d75c --- /dev/null +++ b/packages/hoppscotch-cli/README.md @@ -0,0 +1,52 @@ +# Hoppscotch CLI ALPHA + +A CLI to run Hoppscotch test scripts in CI environments. + +### **Commands:** +- `hopp test [options] [file]`: testing hoppscotch collection.json file + +### **Usage:** +``` +hopp [options or commands] arguments +``` + +### **Options:** +- `-v`, `--ver`: see the current version of the CLI +- `-h`, `--help`: display help for command + +## **Command Descriptions:** + +1. #### **`hopp -v` / `hopp --ver`** + + - Prints out the current version of the Hoppscotch CLI + +2. #### **`hopp -h` / `hopp --help`** + + - Displays the help text + +3. #### **`hopp test `** + - Interactive CLI to accept Hoppscotch collection JSON path + - Parses the collection JSON and executes each requests + - Executes pre-request script. + - Outputs the response of each request. + - Executes and outputs test-script response. + +## Install + +Install [@hoppscotch/cli](https://www.npmjs.com/package/@hoppscotch/cli) from npm by running: +``` +npm i -g @hoppscotch/cli +``` + +## **Developing:** + +1. Clone the repository, make sure you've installed latest [pnpm](https://pnpm.io). +2. `pnpm install` +3. `cd packages/hoppscotch-cli` +4. `pnpm run build` +5. `sudo pnpm link --global` +6. Test the installation by executing `hopp` + +## **Contributing:** + +To get started contributing to the repository, please read **[CONTRIBUTING.md](./CONTRIBUTING.md)** diff --git a/packages/hoppscotch-cli/bin/hopp b/packages/hoppscotch-cli/bin/hopp new file mode 100755 index 000000000..96c440b9f --- /dev/null +++ b/packages/hoppscotch-cli/bin/hopp @@ -0,0 +1,3 @@ +#!/usr/bin/env node +// * The entry point of the CLI +require("../dist").cli(process.argv); diff --git a/packages/hoppscotch-cli/package.json b/packages/hoppscotch-cli/package.json new file mode 100644 index 000000000..4f19e01c3 --- /dev/null +++ b/packages/hoppscotch-cli/package.json @@ -0,0 +1,45 @@ +{ + "name": "@hoppscotch/cli", + "version": "0.1.1", + "description": "A CLI to run Hoppscotch test scripts in CI environments.", + "main": "dist/index.js", + "bin": { + "hopp": "bin/hopp" + }, + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "pnpx tsup", + "dev": "pnpx tsup --watch", + "debugger": "node debugger.js 9999", + "prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write", + "do-typecheck": "pnpx tsc --noEmit" + }, + "keywords": [ + "cli", + "hoppscotch", + "hopp-cli" + ], + "author": "Hoppscotch (support@hoppscotch.io)", + "license": "MIT", + "private": false, + "devDependencies": { + "@hoppscotch/data": "workspace:^0.4.0", + "@hoppscotch/js-sandbox": "workspace:^2.0.0", + "@swc/core": "^1.2.160", + "@types/axios": "^0.14.0", + "@types/chalk": "^2.2.0", + "@types/commander": "^2.12.2", + "esm": "^3.2.25", + "prettier": "^2.5.1", + "tsup": "^5.11.13", + "typescript": "^4.3.5", + "axios": "^0.21.4", + "chalk": "^4.1.1", + "commander": "^8.0.0", + "fp-ts": "^2.11.3", + "lodash": "^4.17.21", + "qs": "^6.10.3" + } +} diff --git a/packages/hoppscotch-cli/src/commands/test.ts b/packages/hoppscotch-cli/src/commands/test.ts new file mode 100644 index 000000000..349b4da96 --- /dev/null +++ b/packages/hoppscotch-cli/src/commands/test.ts @@ -0,0 +1,24 @@ +import * as TE from "fp-ts/TaskEither"; +import { pipe, flow } from "fp-ts/function"; +import { + collectionsRunner, + collectionsRunnerExit, + collectionsRunnerResult, +} from "../utils/collections"; +import { handleError } from "../handlers/error"; +import { checkFilePath } from "../utils/checks"; +import { parseCollectionData } from "../utils/mutators"; + +export const test = (path: string) => async () => { + await pipe( + path, + checkFilePath, + TE.chain(parseCollectionData), + TE.chainTaskK(collectionsRunner), + TE.chainW(flow(collectionsRunnerResult, collectionsRunnerExit, TE.of)), + TE.mapLeft((e) => { + handleError(e); + process.exit(1); + }) + )(); +}; diff --git a/packages/hoppscotch-cli/src/handlers/error.ts b/packages/hoppscotch-cli/src/handlers/error.ts new file mode 100644 index 000000000..e0b340701 --- /dev/null +++ b/packages/hoppscotch-cli/src/handlers/error.ts @@ -0,0 +1,85 @@ +import { log } from "console"; +import * as S from "fp-ts/string"; +import { HoppError, HoppErrorCode } from "../types/errors"; +import { hasProperty, isSafeCommanderError } from "../utils/checks"; +import { parseErrorMessage } from "../utils/mutators"; +import { exceptionColors } from "../utils/getters"; +const { BG_FAIL } = exceptionColors; + +/** + * Parses unknown error data and narrows it to get information realted to + * error in string format. + * @param e Error data to parse. + * @returns Information in string format appropriately parsed, based on error type. + */ +const parseErrorData = (e: unknown) => { + let parsedMsg: string; + + if (!!e && typeof e === "object") { + if (hasProperty(e, "message") && S.isString(e.message)) { + parsedMsg = e.message; + } else if (hasProperty(e, "data") && S.isString(e.data)) { + parsedMsg = e.data; + } else { + parsedMsg = JSON.stringify(e); + } + } else if (S.isString(e)) { + parsedMsg = e; + } else { + parsedMsg = JSON.stringify(e); + } + + return parsedMsg; +}; + +/** + * Handles HoppError to generate error messages based on data related + * to error code and exits program with exit code 1. + * @param error Error object with code of type HoppErrorCode. + */ +export const handleError = (error: HoppError) => { + const ERROR_CODE = BG_FAIL(error.code); + let ERROR_MSG; + + switch (error.code) { + case "FILE_NOT_FOUND": + ERROR_MSG = `File doesn't exists: ${error.path}`; + break; + case "UNKNOWN_COMMAND": + ERROR_MSG = `Unavailable command: ${error.command}`; + break; + case "FILE_NOT_JSON": + ERROR_MSG = `Please check file type: ${error.path}`; + break; + case "MALFORMED_COLLECTION": + ERROR_MSG = `${error.path}\n${parseErrorData(error.data)}`; + break; + case "NO_FILE_PATH": + ERROR_MSG = `Please provide a hoppscotch-collection file path.`; + break; + case "PARSING_ERROR": + ERROR_MSG = `Unable to parse -\n${error.data}`; + break; + case "REQUEST_ERROR": + case "TEST_SCRIPT_ERROR": + case "PRE_REQUEST_SCRIPT_ERROR": + ERROR_MSG = parseErrorData(error.data); + break; + case "INVALID_ARGUMENT": + case "UNKNOWN_ERROR": + case "SYNTAX_ERROR": + if (isSafeCommanderError(error.data)) { + ERROR_MSG = S.empty; + } else { + ERROR_MSG = parseErrorMessage(error.data); + } + break; + case "TESTS_FAILING": + ERROR_MSG = error.data; + break; + } + + if (!S.isEmpty(ERROR_MSG)) { + log(ERROR_CODE, ERROR_MSG); + } +}; diff --git a/packages/hoppscotch-cli/src/index.ts b/packages/hoppscotch-cli/src/index.ts new file mode 100644 index 000000000..2c0a34a1c --- /dev/null +++ b/packages/hoppscotch-cli/src/index.ts @@ -0,0 +1,60 @@ +import chalk from "chalk"; +import { program } from "commander"; +import * as E from "fp-ts/Either"; +import { version } from "../package.json"; +import { test } from "./commands/test"; +import { handleError } from "./handlers/error"; + +const accent = chalk.greenBright + +/** + * * Program Default Configuration + */ +const CLI_BEFORE_ALL_TXT = `hopp: The ${accent( + "Hoppscotch" +)} CLI - Version ${version} (${accent("https://hoppscotch.io")}) ${chalk.black.bold.bgYellowBright(" ALPHA ")} \n`; + +const CLI_AFTER_ALL_TXT = `\nFor more help, head on to ${accent( + "https://docs.hoppscotch.io/cli" +)}`; + +program + .name("hopp") + .version(version, "-v, --ver", "see the current version of hopp-cli") + .usage("[options or commands] arguments") + .addHelpText("beforeAll", CLI_BEFORE_ALL_TXT) + .addHelpText("after", CLI_AFTER_ALL_TXT) + .configureHelp({ + optionTerm: (option) => accent(option.flags), + subcommandTerm: (cmd) => accent(cmd.name(), cmd.usage()), + argumentTerm: (arg) => accent(arg.name()), + }) + .addHelpCommand(false) + .showHelpAfterError(true); + +program.exitOverride().configureOutput({ + writeErr: (str) => program.help(), + outputError: (str, write) => + handleError({ code: "INVALID_ARGUMENT", data: E.toError(str) }), +}); + +/** + * * CLI Commands + */ +program + .command("test") + .argument( + "[file]", + "path to a hoppscotch collection.json file for CI testing" + ) + .allowExcessArguments(false) + .allowUnknownOption(false) + .description("running hoppscotch collection.json file") + .addHelpText("after", `\nFor help, head on to ${accent("https://docs.hoppscotch.io/cli#test")}`) + .action(async (path) => await test(path)()); + +export const cli = async (args: string[]) => { + try { + await program.parseAsync(args); + } catch (e) {} +}; diff --git a/packages/hoppscotch-cli/src/interfaces/request.ts b/packages/hoppscotch-cli/src/interfaces/request.ts new file mode 100644 index 000000000..6dd22576a --- /dev/null +++ b/packages/hoppscotch-cli/src/interfaces/request.ts @@ -0,0 +1,36 @@ +import { AxiosPromise, AxiosRequestConfig } from "axios"; +import { HoppRESTRequest } from "@hoppscotch/data"; + +/** + * Provides definition to object returned by createRequest. + * @property {function} request Axios request promise, executed to get axios + * response promise. + * @property {string} path Path of request within collection file. + * @property {string} name Name of request within collection + * @property {string} testScript Stringified hoppscotch testScript, used while + * running testRunner. + */ +export interface RequestStack { + request: () => AxiosPromise; + path: string; +} + +/** + * Provides definition to axios request promise's request parameter. + * @property {boolean} supported - Boolean check for supported or unsupported requests. + */ +export interface RequestConfig extends AxiosRequestConfig { + supported: boolean; +} + +export interface EffectiveHoppRESTRequest extends HoppRESTRequest { + /** + * The effective final URL. + * + * This contains path, params and environment variables all applied to it + */ + effectiveFinalURL: string; + effectiveFinalHeaders: { key: string; value: string; active: boolean }[]; + effectiveFinalParams: { key: string; value: string; active: boolean }[]; + effectiveFinalBody: FormData | string | null; +} diff --git a/packages/hoppscotch-cli/src/interfaces/response.ts b/packages/hoppscotch-cli/src/interfaces/response.ts new file mode 100644 index 000000000..b04388e54 --- /dev/null +++ b/packages/hoppscotch-cli/src/interfaces/response.ts @@ -0,0 +1,68 @@ +import { TestResponse } from "@hoppscotch/js-sandbox"; +import { Method } from "axios"; +import { ExpectResult } from "../types/response"; +import { HoppEnvs } from "../types/request"; + +/** + * Defines column headers for table stream used to write table + * data on stdout. + * @property {string} path Path of request within collection file. + * @property {string} endpoint Endpoint from response config.url. + * @property {Method} method Method from response headers. + * @property {string} statusCode Template string concating status & statusText. + */ +export interface TableResponse { + endpoint: string; + method: Method; + statusCode: string; +} + +/** + * Describes additional details of HTTP response returned from + * requestRunner. + * @property {string} path Path of request within collection file. + * @property {string} endpoint Endpoint from response config.url. + * @property {Method} method Method from HTTP response headers. + * @property {string} statusText HTTP response status text. + */ +export interface RequestRunnerResponse extends TestResponse { + endpoint: string; + method: Method; + statusText: string; +} + +/** + * Describes test script details. + * @property {string} name Request name within collection. + * @property {string} testScript Stringified hoppscotch testScript, used while + * running testRunner. + * @property {TestResponse} response Response structure for test script runner. + */ +export interface TestScriptParams { + testScript: string; + response: TestResponse; + envs: HoppEnvs; +} + +/** + * Describe properties of test-report generated from test-runner. + * @property {string} descriptor Test description. + * @property {ExpectResult[]} expectResults Expected results for each + * test-case. + * @property {number} failing Total failing test-cases. + * @property {number} passing Total passing test-cases; + */ +export interface TestReport { + descriptor: string; + expectResults: ExpectResult[]; + failing: number; + passing: number; +} + +/** + * Describes error pair for failed HTTP requests. + * @example { 501: "Request Not Supported" } + */ +export interface ResponseErrorPair { + [key: number]: string; +} diff --git a/packages/hoppscotch-cli/src/tsconfig.json b/packages/hoppscotch-cli/src/tsconfig.json new file mode 100644 index 000000000..78d41bedd --- /dev/null +++ b/packages/hoppscotch-cli/src/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES6", + "module": "commonjs", + "outDir": "../dist", + "rootDir": ".", + "strict": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "references": [ + { + "path": "../" + } + ] +} diff --git a/packages/hoppscotch-cli/src/types/errors.ts b/packages/hoppscotch-cli/src/types/errors.ts new file mode 100644 index 000000000..9b896fa6b --- /dev/null +++ b/packages/hoppscotch-cli/src/types/errors.ts @@ -0,0 +1,36 @@ +type HoppErrorPath = { + path: string; +}; + +type HoppErrorCmd = { + command: string; +}; + +type HoppErrorData = { + data: any; +}; + +type HoppErrors = { + UNKNOWN_ERROR: HoppErrorData; + FILE_NOT_FOUND: HoppErrorPath; + UNKNOWN_COMMAND: HoppErrorCmd; + MALFORMED_COLLECTION: HoppErrorPath & HoppErrorData; + FILE_NOT_JSON: HoppErrorPath; + NO_FILE_PATH: {}; + PRE_REQUEST_SCRIPT_ERROR: HoppErrorData; + PARSING_ERROR: HoppErrorData; + TEST_SCRIPT_ERROR: HoppErrorData; + TESTS_FAILING: HoppErrorData; + SYNTAX_ERROR: HoppErrorData; + REQUEST_ERROR: HoppErrorData; + INVALID_ARGUMENT: HoppErrorData; +}; + +export type HoppErrorCode = keyof HoppErrors; +export type HoppError = T extends null + ? { code: T } + : { code: T } & HoppErrors[T]; + +export const error = (error: HoppError) => error; +export type HoppCLIError = HoppError; +export type HoppErrnoException = NodeJS.ErrnoException; diff --git a/packages/hoppscotch-cli/src/types/request.ts b/packages/hoppscotch-cli/src/types/request.ts new file mode 100644 index 000000000..87be44ff3 --- /dev/null +++ b/packages/hoppscotch-cli/src/types/request.ts @@ -0,0 +1,31 @@ +import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"; +import { TestReport } from "../interfaces/response"; +import { HoppCLIError } from "./errors"; + +export type FormDataEntry = { + key: string; + value: string | Blob; +}; + +export type HoppEnvs = { + global: { + key: string; + value: string; + }[]; + selected: { + key: string; + value: string; + }[]; +}; + +export type CollectionStack = { + path: string; + collection: HoppCollection; +}; + +export type RequestReport = { + path: string; + tests: TestReport[]; + errors: HoppCLIError[]; + result: boolean; +}; diff --git a/packages/hoppscotch-cli/src/types/response.ts b/packages/hoppscotch-cli/src/types/response.ts new file mode 100644 index 000000000..6f54a228a --- /dev/null +++ b/packages/hoppscotch-cli/src/types/response.ts @@ -0,0 +1,28 @@ +import { TestReport } from "../interfaces/response"; +import { HoppEnvs } from "./request"; + +/** + * The expectation failed (fail) or errored (error) + */ +export type ExpectResult = { + status: "pass" | "fail" | "error"; + message: string; +}; + +export type TestMetrics = { + /** + * Total passed and failed test-cases. + */ + tests: { failing: number; passing: number }; + + /** + * Total test-blocks/test-suites passed & failed, calculated + * based on test-cases failed/passed with in each test-block. + */ + testSuites: { failing: number; passing: number }; +}; + +export type TestRunnerRes = { + envs: HoppEnvs; + testsReport: TestReport[]; +}; diff --git a/packages/hoppscotch-cli/src/utils/checks.ts b/packages/hoppscotch-cli/src/utils/checks.ts new file mode 100644 index 000000000..658195f47 --- /dev/null +++ b/packages/hoppscotch-cli/src/utils/checks.ts @@ -0,0 +1,155 @@ +import fs from "fs/promises"; +import { join } from "path"; +import { pipe } from "fp-ts/function"; +import { + HoppCollection, + HoppRESTRequest, + isHoppRESTRequest, +} from "@hoppscotch/data"; +import * as A from "fp-ts/Array"; +import * as S from "fp-ts/string"; +import * as TE from "fp-ts/TaskEither"; +import { error, HoppCLIError, HoppErrnoException } from "../types/errors"; +import { CommanderError } from "commander"; + +/** + * Determines whether an object has a property with given name. + * @param target Object to be checked for given property. + * @param prop Property to be checked in target object. + * @returns True, if property exists in target object; False, otherwise. + */ +export const hasProperty =

( + target: object, + prop: P +): target is Record => prop in target; + +/** + * Typeguard to check valid Hoppscotch REST Collection. + * @param param The object to be checked. + * @returns True, if unknown parameter is valid Hoppscotch REST Collection; + * False, otherwise. + */ +export const isRESTCollection = ( + param: unknown +): param is HoppCollection => { + if (!!param && typeof param === "object") { + if (!hasProperty(param, "v") || typeof param.v !== "number") { + return false; + } + if (!hasProperty(param, "name") || typeof param.name !== "string") { + return false; + } + if (hasProperty(param, "id") && typeof param.id !== "string") { + return false; + } + if (!hasProperty(param, "requests") || !Array.isArray(param.requests)) { + return false; + } else { + // Checks each requests array to be valid HoppRESTRequest. + const checkRequests = A.every(isHoppRESTRequest)(param.requests); + if (!checkRequests) { + return false; + } + } + if (!hasProperty(param, "folders") || !Array.isArray(param.folders)) { + return false; + } else { + // Checks each folder to be valid REST collection. + const checkFolders = A.every(isRESTCollection)(param.folders); + if (!checkFolders) { + return false; + } + } + + return true; + } + + return false; +}; + +/** + * Checks if the given file path exists and is of JSON type. + * @param path The input file path to check. + * @returns Absolute path for valid file path OR HoppCLIError in case of error. + */ +export const checkFilePath = ( + path: string +): TE.TaskEither => + pipe( + path, + + /** + * Check the path type and returns string if passes else HoppCLIError. + */ + TE.fromPredicate(S.isString, () => error({ code: "NO_FILE_PATH" })), + + /** + * Trying to access given file path. + * If successfully accessed, we return the path from predicate step. + * Else return HoppCLIError with code FILE_NOT_FOUND. + */ + TE.chainFirstW( + TE.tryCatchK( + () => pipe(path, join, fs.access), + () => error({ code: "FILE_NOT_FOUND", path: path }) + ) + ), + + /** + * On successfully accessing given file path, we map file path to + * absolute path and return abs file path if file is JSON type. + */ + TE.map(join), + TE.chainW( + TE.fromPredicate(S.endsWith(".json"), (absPath) => + error({ code: "FILE_NOT_JSON", path: absPath }) + ) + ) + ); + +/** + * Checks if given error data is of type HoppCLIError, based on existence + * of code property. + * @param error Error data to check. + * @returns True, if unknown error validates to be HoppCLIError; + * False, otherwise. + */ +export const isHoppCLIError = (error: unknown): error is HoppCLIError => { + return ( + !!error && + typeof error === "object" && + hasProperty(error, "code") && + typeof error.code === "string" + ); +}; + +/** + * Checks if given error data is of type HoppErrnoException, based on existence + * of name property. + * @param error Error data to check. + * @returns True, if unknown error validates to be HoppErrnoException; + * False, otherwise. + */ +export const isHoppErrnoException = ( + error: unknown +): error is HoppErrnoException => { + return ( + !!error && + typeof error === "object" && + hasProperty(error, "name") && + typeof error.name === "string" + ); +}; + +/** + * Check whether given unknown error is instance of commander-error and + * has zero exit code (which we consider as safe error). + * @param error Error data to check. + * @returns True, if error data validates to be safe-commander-error; + * False, otherwise. + */ +export const isSafeCommanderError = ( + error: unknown +): error is CommanderError => { + return error instanceof CommanderError && error.exitCode === 0; +}; diff --git a/packages/hoppscotch-cli/src/utils/collections.ts b/packages/hoppscotch-cli/src/utils/collections.ts new file mode 100644 index 000000000..69b6e92a1 --- /dev/null +++ b/packages/hoppscotch-cli/src/utils/collections.ts @@ -0,0 +1,129 @@ +import * as T from "fp-ts/Task"; +import * as A from "fp-ts/Array"; +import { pipe } from "fp-ts/function"; +import { bold } from "chalk"; +import { log } from "console"; +import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"; +import { HoppEnvs, CollectionStack, RequestReport } from "../types/request"; +import { preProcessRequest, processRequest } from "./request"; +import { exceptionColors } from "./getters"; +import { TestReport } from "../interfaces/response"; +import { + printErrorsReport, + printFailedTestsReport, + printTestsMetrics, +} from "./display"; +const { WARN, FAIL } = exceptionColors; + +/** + * Processes each requests within collections to prints details of subsequent requests, + * tests and to display complete errors-report, failed-tests-report and test-metrics. + * @param collections Array of hopp-collection with hopp-requests to be processed. + * @returns List of report for each processed request. + */ +export const collectionsRunner = + (collections: HoppCollection[]): T.Task => + async () => { + const envs: HoppEnvs = { global: [], selected: [] }; + const requestsReport: RequestReport[] = []; + const collectionStack: CollectionStack[] = getCollectionStack(collections); + + while (collectionStack.length) { + // Pop out top-most collection from stack to be processed. + const { collection, path } = collectionStack.pop(); + + // Processing each request in collection + for (const request of collection.requests) { + const _request = preProcessRequest(request); + const requestPath = `${path}/${_request.name}`; + + // Request processing initiated message. + log(WARN(`\nRunning: ${bold(requestPath)}`)); + + // Processing current request. + const result = await processRequest(_request, envs, requestPath)(); + + // Updating global & selected envs with new envs from processed-request output. + const { global, selected } = result.envs; + envs.global = global; + envs.selected = selected; + + // Storing current request's report. + const requestReport = result.report; + requestsReport.push(requestReport); + } + + // Pushing remaining folders realted collection to stack. + for (const folder of collection.folders) { + collectionStack.push({ + path: `${path}/${folder.name}`, + collection: folder, + }); + } + } + + return requestsReport; + }; + +/** + * Transforms collections to generate collection-stack which describes each collection's + * path within collection & the collection itself. + * @param collections Hopp-collection objects to be mapped to collection-stack type. + * @returns Mapped collections to collection-stack. + */ +const getCollectionStack = ( + collections: HoppCollection[] +): CollectionStack[] => + pipe( + collections, + A.map( + (collection) => { collection, path: collection.name } + ) + ); + +/** + * Prints collection-runner-report using test-metrics data in table format. + * @param requestsReport Provides data for each request-report which includes + * failed-tests-report, errors + * @returns True, if collection runner executed without any errors or failed test-cases. + * False, if errors occured or test-cases failed. + */ +export const collectionsRunnerResult = ( + requestsReport: RequestReport[] +): boolean => { + const testsReport: TestReport[] = []; + let finalResult = true; + + // Printing requests-report details of failed-tests and errors + for (const requestReport of requestsReport) { + const { path, tests, errors, result } = requestReport; + + finalResult = finalResult && result; + + printFailedTestsReport(path, tests); + + printErrorsReport(path, errors); + + testsReport.push.apply(testsReport, tests); + } + + printTestsMetrics(testsReport); + + return finalResult; +}; + +/** + * Exiting hopp cli process with appropriate exit code depending on + * collections-runner result. + * If result is true, we exit the cli process with code 0. + * Else, exit with code 1. + * @param result Boolean defining the collections-runner result. + */ +export const collectionsRunnerExit = (result: boolean) => { + if (!result) { + const EXIT_MSG = FAIL(`\nExited with code 1`); + process.stdout.write(EXIT_MSG); + process.exit(1); + } + process.exit(0); +}; diff --git a/packages/hoppscotch-cli/src/utils/constants.ts b/packages/hoppscotch-cli/src/utils/constants.ts new file mode 100644 index 000000000..ce717a52d --- /dev/null +++ b/packages/hoppscotch-cli/src/utils/constants.ts @@ -0,0 +1,7 @@ +import { ResponseErrorPair } from "../interfaces/response"; + +export const responseErrors: ResponseErrorPair = { + 501: "REQUEST NOT SUPPORTED", + 408: "NETWORK TIMEOUT", + 400: "BAD REQUEST", +} as const; diff --git a/packages/hoppscotch-cli/src/utils/display.ts b/packages/hoppscotch-cli/src/utils/display.ts new file mode 100644 index 000000000..2c22ff0a1 --- /dev/null +++ b/packages/hoppscotch-cli/src/utils/display.ts @@ -0,0 +1,145 @@ +import { bold } from "chalk"; +import { groupEnd, group, log } from "console"; +import { handleError } from "../handlers/error"; +import { RequestConfig } from "../interfaces/request"; +import { RequestRunnerResponse, TestReport } from "../interfaces/response"; +import { HoppCLIError } from "../types/errors"; +import { exceptionColors, getColorStatusCode } from "./getters"; +import { + getFailedExpectedResults, + getFailedTestsReport, + getTestMetrics, +} from "./test"; +const { FAIL, SUCCESS, BG_INFO } = exceptionColors; + +/** + * Prints test-suites in pretty-way describing each test-suites failed/passed + * status. + * @param testsReport Providing details of each test-suites with tests-report. + */ +export const printTestSuitesReport = (testsReport: TestReport[]) => { + group(); + for (const testReport of testsReport) { + const { failing, descriptor } = testReport; + + if (failing > 0) { + log(`${FAIL("✖")} ${descriptor}`); + } else { + log(`${SUCCESS("✔")} ${descriptor}`); + } + } + groupEnd(); +}; + +/** + * Prints total number of test-cases and test-suites passed/failed. + * @param testsReport Provides testSuites and testCases metrics. + */ +export const printTestsMetrics = (testsReport: TestReport[]) => { + const { testSuites, tests } = getTestMetrics(testsReport); + + const failedTestCasesOut = FAIL(`${tests.failing} failing`); + const passedTestCasesOut = SUCCESS(`${tests.passing} passing`); + const testCasesOut = `Test Cases: ${failedTestCasesOut} ${passedTestCasesOut}\n`; + + const failedTestSuitesOut = FAIL(`${testSuites.failing} failing`); + const passedTestSuitesOut = SUCCESS(`${testSuites.passing} passing`); + const testSuitesOut = `Test Suites: ${failedTestSuitesOut} ${passedTestSuitesOut}\n`; + + const message = `\n${testCasesOut}${testSuitesOut}`; + process.stdout.write(message); +}; + +/** + * Prints details of each reported error for a request with error code. + * @param path Request's path in collection for which errors occured. + * @param errorsReport List of errors reported. + */ +export const printErrorsReport = ( + path: string, + errorsReport: HoppCLIError[] +) => { + if (errorsReport.length > 0) { + const REPORTED_ERRORS_TITLE = FAIL(`\n${bold(path)} reported errors:`); + + group(REPORTED_ERRORS_TITLE); + for (const errorReport of errorsReport) { + handleError(errorReport); + } + groupEnd(); + } +}; + +/** + * Prints details of each failed tests for given request's path. + * @param path Request's path in collection for which tests-failed. + * @param testsReport Overall tests-report including failed-tests-report. + */ +export const printFailedTestsReport = ( + path: string, + testsReport: TestReport[] +) => { + const failedTestsReport = getFailedTestsReport(testsReport); + + // Only printing test-reports with failing test-cases. + if (failedTestsReport.length > 0) { + const FAILED_TESTS_PATH = FAIL(`\n${bold(path)} failed tests:`); + group(FAILED_TESTS_PATH); + + for (const failedTestReport of failedTestsReport) { + const { descriptor, expectResults } = failedTestReport; + const failedExpectResults = getFailedExpectedResults(expectResults); + + // Only printing failed expected-results. + if (failedExpectResults.length > 0) { + group("⦁", descriptor); + + for (const failedExpectResult of failedExpectResults) { + log(FAIL("-"), failedExpectResult.message); + } + + groupEnd(); + } + } + + groupEnd(); + } +}; + +/** + * Provides methods for printing request-runner's state messages. + */ +export const printRequestRunner = { + // Request-runner starting message. + start: (requestConfig: RequestConfig) => { + const METHOD = BG_INFO(` ${requestConfig.method} `); + const ENDPOINT = requestConfig.url; + + process.stdout.write(`${METHOD} ${ENDPOINT}`); + }, + + // Prints response's status, when request-runner executes successfully. + success: (requestResponse: RequestRunnerResponse) => { + const { status, statusText } = requestResponse; + const statusMsg = getColorStatusCode(status, statusText); + + process.stdout.write(` ${statusMsg}\n`); + }, + + // Prints error message, when request-runner fails to execute. + fail: () => log(FAIL(" ERROR\n⚠ Error running request.")), +}; + +/** + * Provides methods for printing test-runner's state messages. + */ +export const printTestRunner = { + fail: () => log(FAIL("⚠ Error running test-script.")), +}; + +/** + * Provides methods for printing pre-request-runner's state messages. + */ +export const printPreRequestRunner = { + fail: () => log(FAIL("⚠ Error running pre-request-script.")), +}; diff --git a/packages/hoppscotch-cli/src/utils/functions/array.ts b/packages/hoppscotch-cli/src/utils/functions/array.ts new file mode 100644 index 000000000..4a5a07280 --- /dev/null +++ b/packages/hoppscotch-cli/src/utils/functions/array.ts @@ -0,0 +1,37 @@ +import { clone } from "lodash"; + +/** + * Sorts the array based on the sort func. + * NOTE: Creates a new array, if you don't need ref + * to original array, use `arrayUnsafeSort` for better perf + * @param sortFunc Sort function to sort against + */ +export const arraySort = + (sortFunc: (a: T, b: T) => number) => + (arr: T[]) => { + const newArr = clone(arr); + + newArr.sort(sortFunc); + + return newArr; + }; + +/** + * Equivalent to `Array.prototype.flatMap`. + * @param mapFunc The map function. + * @returns Array formed by applying given mapFunc. + */ +export const arrayFlatMap = + (mapFunc: (value: T, index: number, arr: T[]) => U[]) => + (arr: T[]) => + arr.flatMap(mapFunc); + +export const tupleToRecord = < + KeyType extends string | number | symbol, + ValueType +>( + tuples: [KeyType, ValueType][] +): Record => + tuples.length > 0 + ? (Object.assign as any)(...tuples.map(([key, val]) => ({ [key]: val }))) + : {}; diff --git a/packages/hoppscotch-cli/src/utils/getters.ts b/packages/hoppscotch-cli/src/utils/getters.ts new file mode 100644 index 000000000..514d2a314 --- /dev/null +++ b/packages/hoppscotch-cli/src/utils/getters.ts @@ -0,0 +1,113 @@ +import { + HoppRESTHeader, + Environment, + parseTemplateStringE, + HoppRESTParam, +} from "@hoppscotch/data"; +import chalk from "chalk"; +import { pipe } from "fp-ts/function"; +import * as A from "fp-ts/Array"; +import * as E from "fp-ts/Either"; +import * as S from "fp-ts/string"; +import * as O from "fp-ts/Option"; +import { error } from "../types/errors"; + +/** + * Generates template string (status + statusText) with specific color unicodes + * based on type of status. + * @param status Status code of a HTTP response. + * @param statusText Status text of a HTTP response. + * @returns Template string with related color unicodes. + */ +export const getColorStatusCode = ( + status: number | string, + statusText: string +): string => { + const statusCode = `${status == 0 ? "Error" : status} : ${statusText}`; + + if (status.toString().startsWith("2")) { + return chalk.greenBright(statusCode); + } else if (status.toString().startsWith("3")) { + return chalk.yellowBright(statusCode); + } + + return chalk.redBright(statusCode); +}; + +/** + * Replaces all template-string with their effective ENV values to generate effective + * request headers/parameters meta-data. + * @param metaData Headers/parameters on which ENVs will be applied. + * @param environment Provides ENV variables for parsing template-string. + * @returns Active, non-empty-key, parsed headers/parameters pairs. + */ +export const getEffectiveFinalMetaData = ( + metaData: HoppRESTHeader[] | HoppRESTParam[], + environment: Environment +) => + pipe( + metaData, + + /** + * Selecting only non-empty and active pairs. + */ + A.filter(({ key, active }) => !S.isEmpty(key) && active), + A.map(({ key, value }) => ({ + active: true, + key: parseTemplateStringE(key, environment.variables), + value: parseTemplateStringE(value, environment.variables), + })), + E.fromPredicate( + /** + * Check if every key-value is right either. Else return HoppCLIError with + * appropriate reason. + */ + A.every(({ key, value }) => E.isRight(key) && E.isRight(value)), + (reason) => error({ code: "PARSING_ERROR", data: reason }) + ), + E.map( + /** + * Filtering and mapping only right-eithers for each key-value as [string, string]. + */ + A.filterMap(({ key, value }) => + E.isRight(key) && E.isRight(value) + ? O.some({ active: true, key: key.right, value: value.right }) + : O.none + ) + ) + ); + +/** + * Reduces array of HoppRESTParam or HoppRESTHeader to unique key-value + * pair. + * @param metaData Array of meta-data to reduce. + * @returns Object with unique key-value pair. + */ +export const getMetaDataPairs = ( + metaData: HoppRESTParam[] | HoppRESTHeader[] +) => + pipe( + metaData, + + // Excluding non-active & empty key request meta-data. + A.filter(({ active, key }) => active && !S.isEmpty(key)), + + // Reducing array of request-meta-data to key-value pair object. + A.reduce(>{}, (target, { key, value }) => + Object.assign(target, { [`${key}`]: value }) + ) + ); + +/** + * Object providing aliases for chalk color properties based on exceptions. + */ +export const exceptionColors = { + WARN: chalk.yellow, + INFO: chalk.blue, + FAIL: chalk.red, + SUCCESS: chalk.green, + BG_WARN: chalk.bgYellow, + BG_FAIL: chalk.bgRed, + BG_INFO: chalk.bgBlue, + BG_SUCCESS: chalk.bgGreen, +}; diff --git a/packages/hoppscotch-cli/src/utils/mutators.ts b/packages/hoppscotch-cli/src/utils/mutators.ts new file mode 100644 index 000000000..eb145d881 --- /dev/null +++ b/packages/hoppscotch-cli/src/utils/mutators.ts @@ -0,0 +1,80 @@ +import fs from "fs/promises"; +import * as E from "fp-ts/Either"; +import * as TE from "fp-ts/TaskEither"; +import * as A from "fp-ts/Array"; +import * as J from "fp-ts/Json"; +import { pipe } from "fp-ts/function"; +import { FormDataEntry } from "../types/request"; +import { error, HoppCLIError } from "../types/errors"; +import { isRESTCollection, isHoppErrnoException } from "./checks"; +import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"; + +/** + * Parses array of FormDataEntry to FormData. + * @param values Array of FormDataEntry. + * @returns FormData with key-value pair from FormDataEntry. + */ +export const toFormData = (values: FormDataEntry[]) => { + const formData = new FormData(); + + values.forEach(({ key, value }) => formData.append(key, value)); + + return formData; +}; + +/** + * Parses provided error message to maintain hopp-error messages. + * @param e Custom error data. + * @returns Parsed error message without extra spaces. + */ +export const parseErrorMessage = (e: unknown) => { + let msg: string; + if (isHoppErrnoException(e)) { + msg = e.message.replace(e.code! + ":", "").replace("error:", ""); + } else if (typeof e === "string") { + msg = e; + } else { + msg = JSON.stringify(e); + } + return msg.replace(/\n+$|\s{2,}/g, "").trim(); +}; + +/** + * Parses collection json file for given path:context.path, and validates + * the parsed collectiona array. + * @param path Collection json file path. + * @returns For successful parsing we get array of HoppCollection, + */ +export const parseCollectionData = ( + path: string +): TE.TaskEither[]> => + pipe( + // Trying to read give collection json path. + TE.tryCatch( + () => pipe(path, fs.readFile), + (reason) => error({ code: "UNKNOWN_ERROR", data: E.toError(reason) }) + ), + + // Checking if parsed file data is array. + TE.chainEitherKW((data) => + pipe( + data.toString(), + J.parse, + E.map((jsonData) => (Array.isArray(jsonData) ? jsonData : [jsonData])), + E.mapLeft((e) => + error({ code: "MALFORMED_COLLECTION", path, data: E.toError(e) }) + ) + ) + ), + + // Validating collections to be HoppRESTCollection. + TE.chainW( + TE.fromPredicate(A.every(isRESTCollection), () => + error({ + code: "MALFORMED_COLLECTION", + path, + data: "Please check the collection data.", + }) + ) + ) + ); diff --git a/packages/hoppscotch-cli/src/utils/pre-request.ts b/packages/hoppscotch-cli/src/utils/pre-request.ts new file mode 100644 index 000000000..fa2bc76bb --- /dev/null +++ b/packages/hoppscotch-cli/src/utils/pre-request.ts @@ -0,0 +1,268 @@ +import { + Environment, + HoppRESTRequest, + parseBodyEnvVariablesE, + parseRawKeyValueEntriesE, + parseTemplateString, + parseTemplateStringE, +} from "@hoppscotch/data"; +import { runPreRequestScript } from "@hoppscotch/js-sandbox"; +import { flow, pipe } from "fp-ts/function"; +import * as TE from "fp-ts/TaskEither"; +import * as E from "fp-ts/Either"; +import * as RA from "fp-ts/ReadonlyArray"; +import * as A from "fp-ts/Array"; +import * as O from "fp-ts/Option"; +import * as S from "fp-ts/string"; +import qs from "qs"; +import { EffectiveHoppRESTRequest } from "../interfaces/request"; +import { error, HoppCLIError } from "../types/errors"; +import { HoppEnvs } from "../types/request"; +import { isHoppCLIError } from "./checks"; +import { tupleToRecord, arraySort, arrayFlatMap } from "./functions/array"; +import { toFormData } from "./mutators"; +import { getEffectiveFinalMetaData } from "./getters"; + +/** + * Runs pre-request-script runner over given request which extracts set ENVs and + * applies them on current request to generate updated request. + * @param request HoppRESTRequest to be converted to EffectiveHoppRESTRequest. + * @param envs Environment variables related to request. + * @returns EffectiveHoppRESTRequest that includes parsed ENV variables with in + * request OR HoppCLIError with error code and related information. + */ +export const preRequestScriptRunner = ( + request: HoppRESTRequest, + envs: HoppEnvs +): TE.TaskEither => + pipe( + TE.of(request), + TE.chain(({ preRequestScript }) => + runPreRequestScript(preRequestScript, envs) + ), + TE.map( + ({ selected, global }) => + { name: "Env", variables: [...selected, ...global] } + ), + TE.chainEitherKW((env) => getEffectiveRESTRequest(request, env)), + TE.mapLeft((reason) => + isHoppCLIError(reason) + ? reason + : error({ + code: "PRE_REQUEST_SCRIPT_ERROR", + data: reason, + }) + ) + ); + +/** + * Outputs an executable request format with environment variables applied + * + * @param request The request to source from + * @param environment The environment to apply + * + * @returns An object with extra fields defining a complete request + */ +export function getEffectiveRESTRequest( + request: HoppRESTRequest, + environment: Environment +): E.Either { + const envVariables = environment.variables; + + // Parsing final headers with applied ENVs. + const _effectiveFinalHeaders = getEffectiveFinalMetaData( + request.headers, + environment + ); + if (E.isLeft(_effectiveFinalHeaders)) { + return _effectiveFinalHeaders; + } + const effectiveFinalHeaders = _effectiveFinalHeaders.right; + + // Parsing final parameters with applied ENVs. + const _effectiveFinalParams = getEffectiveFinalMetaData( + request.params, + environment + ); + if (E.isLeft(_effectiveFinalParams)) { + return _effectiveFinalParams; + } + const effectiveFinalParams = _effectiveFinalParams.right; + + // Authentication + if (request.auth.authActive) { + // TODO: Support a better b64 implementation than btoa ? + if (request.auth.authType === "basic") { + const username = parseTemplateString(request.auth.username, envVariables); + const password = parseTemplateString(request.auth.password, envVariables); + + effectiveFinalHeaders.push({ + active: true, + key: "Authorization", + value: `Basic ${btoa(`${username}:${password}`)}`, + }); + } else if ( + request.auth.authType === "bearer" || + request.auth.authType === "oauth-2" + ) { + effectiveFinalHeaders.push({ + active: true, + key: "Authorization", + value: `Bearer ${parseTemplateString( + request.auth.token, + envVariables + )}`, + }); + } else if (request.auth.authType === "api-key") { + const { key, value, addTo } = request.auth; + if (addTo === "Headers") { + effectiveFinalHeaders.push({ + active: true, + key: parseTemplateString(key, envVariables), + value: parseTemplateString(value, envVariables), + }); + } else if (addTo === "Query params") { + effectiveFinalParams.push({ + active: true, + key: parseTemplateString(key, envVariables), + value: parseTemplateString(value, envVariables), + }); + } + } + } + + // Parsing final-body with applied ENVs. + const _effectiveFinalBody = getFinalBodyFromRequest(request, envVariables); + if (E.isLeft(_effectiveFinalBody)) { + return _effectiveFinalBody; + } + const effectiveFinalBody = _effectiveFinalBody.right; + + if (request.body.contentType) + effectiveFinalHeaders.push({ + active: true, + key: "content-type", + value: request.body.contentType, + }); + + // Parsing final-endpoint with applied ENVs. + const _effectiveFinalURL = parseTemplateStringE( + request.endpoint, + envVariables + ); + if (E.isLeft(_effectiveFinalURL)) { + return E.left( + error({ + code: "PARSING_ERROR", + data: `${request.endpoint} (${_effectiveFinalURL.left})`, + }) + ); + } + const effectiveFinalURL = _effectiveFinalURL.right; + + return E.right({ + ...request, + effectiveFinalURL, + effectiveFinalHeaders, + effectiveFinalParams, + effectiveFinalBody, + }); +} + +/** + * Replaces template variables in request's body from the given set of ENVs, + * to generate final request body without any template variables. + * @param request Provides request's body, on which ENVs has to be applied. + * @param envVariables Provides set of key-value pairs (environment variables), + * used to parse-out template variables. + * @returns Final request body without any template variables as value. + * Or, HoppCLIError in case of error while parsing. + */ +function getFinalBodyFromRequest( + request: HoppRESTRequest, + envVariables: Environment["variables"] +): E.Either { + if (request.body.contentType === null) { + return E.right(null); + } + + if (request.body.contentType === "application/x-www-form-urlencoded") { + return pipe( + request.body.body, + parseRawKeyValueEntriesE, + E.map( + flow( + RA.toArray, + + /** + * Filtering out empty keys and non-active pairs. + */ + A.filter(({ active, key }) => active && !S.isEmpty(key)), + + /** + * Mapping each key-value to template-string-parser with either on array, + * which will be resolved in further steps. + */ + A.map(({ key, value }) => [ + parseTemplateStringE(key, envVariables), + parseTemplateStringE(value, envVariables), + ]), + + /** + * Filtering and mapping only right-eithers for each key-value as [string, string]. + */ + A.filterMap(([key, value]) => + E.isRight(key) && E.isRight(value) + ? O.some([key.right, value.right] as [string, string]) + : O.none + ), + tupleToRecord, + qs.stringify + ) + ), + E.mapLeft((e) => error({ code: "PARSING_ERROR", data: e.message })) + ); + } + + if (request.body.contentType === "multipart/form-data") { + return pipe( + request.body.body, + A.filter((x) => x.key !== "" && x.active), // Remove empty keys + + // Sort files down + arraySort((a, b) => { + if (a.isFile) return 1; + if (b.isFile) return -1; + return 0; + }), + + // FormData allows only a single blob in an entry, + // we split array blobs into separate entries (FormData will then join them together during exec) + arrayFlatMap((x) => + x.isFile + ? x.value.map((v) => ({ + key: parseTemplateString(x.key, envVariables), + value: v as string | Blob, + })) + : [ + { + key: parseTemplateString(x.key, envVariables), + value: parseTemplateString(x.value, envVariables), + }, + ] + ), + toFormData, + E.right + ); + } + + return pipe( + parseBodyEnvVariablesE(request.body.body, envVariables), + E.mapLeft((e) => + error({ + code: "PARSING_ERROR", + data: `${request.body.body} (${e})`, + }) + ) + ); +} diff --git a/packages/hoppscotch-cli/src/utils/request.ts b/packages/hoppscotch-cli/src/utils/request.ts new file mode 100644 index 000000000..ece74eb70 --- /dev/null +++ b/packages/hoppscotch-cli/src/utils/request.ts @@ -0,0 +1,321 @@ +import axios, { Method } from "axios"; +import { URL } from "url"; +import * as S from "fp-ts/string"; +import * as A from "fp-ts/Array"; +import * as T from "fp-ts/Task"; +import * as E from "fp-ts/Either"; +import * as TE from "fp-ts/TaskEither"; +import { HoppRESTRequest } from "@hoppscotch/data"; +import { responseErrors } from "./constants"; +import { getMetaDataPairs } from "./getters"; +import { testRunner, getTestScriptParams, hasFailedTestCases } from "./test"; +import { RequestConfig, EffectiveHoppRESTRequest } from "../interfaces/request"; +import { RequestRunnerResponse } from "../interfaces/response"; +import { preRequestScriptRunner } from "./pre-request"; +import { HoppEnvs, RequestReport } from "../types/request"; +import { + printPreRequestRunner, + printRequestRunner, + printTestRunner, + printTestSuitesReport, +} from "./display"; +import { error, HoppCLIError } from "../types/errors"; + +// !NOTE: The `config.supported` checks are temporary until OAuth2 and Multipart Forms are supported + +/** + * Transforms given request data to request-config used by request-runner to + * perform HTTP request. + * @param req Effective request data with parsed ENVs. + * @returns Request config with data realted to HTTP request. + */ +export const createRequest = (req: EffectiveHoppRESTRequest): RequestConfig => { + const config: RequestConfig = { + supported: true, + }; + const { finalBody, finalEndpoint, finalHeaders, finalParams } = getRequest; + const reqParams = finalParams(req); + const reqHeaders = finalHeaders(req); + config.url = finalEndpoint(req); + config.method = req.method as Method; + config.params = getMetaDataPairs(reqParams); + config.headers = getMetaDataPairs(reqHeaders); + if (req.auth.authActive) { + switch (req.auth.authType) { + case "oauth-2": { + // TODO: OAuth2 Request Parsing + // !NOTE: Temporary `config.supported` check + config.supported = false; + } + default: { + break; + } + } + } + if (req.body.contentType) { + config.headers["Content-Type"] = req.body.contentType; + switch (req.body.contentType) { + case "multipart/form-data": { + // TODO: Parse Multipart Form Data + // !NOTE: Temporary `config.supported` check + config.supported = false; + break; + } + default: { + config.data = finalBody(req); + break; + } + } + } + + return config; +}; + +/** + * Performs http request using axios with given requestConfig axios + * parameters. + * @param requestConfig The axios request config. + * @returns If successfully ran, we get runner-response including HTTP response data. + * Else, HoppCLIError with appropriate error code & data. + */ +export const requestRunner = + ( + requestConfig: RequestConfig + ): TE.TaskEither => + async () => { + try { + // NOTE: Temporary parsing check for request endpoint. + requestConfig.url = new URL(requestConfig.url ?? "").toString(); + + let status: number; + const baseResponse = await axios(requestConfig); + const { config } = baseResponse; + const runnerResponse: RequestRunnerResponse = { + ...baseResponse, + endpoint: getRequest.endpoint(config.url), + method: getRequest.method(config.method), + body: baseResponse.data, + }; + + // !NOTE: Temporary `config.supported` check + if ((config as RequestConfig).supported === false) { + status = 501; + runnerResponse.status = status; + runnerResponse.statusText = responseErrors[status]; + } + + return E.right(runnerResponse); + } catch (e) { + let status: number; + const runnerResponse: RequestRunnerResponse = { + endpoint: "", + method: "GET", + body: {}, + statusText: responseErrors[400], + status: 400, + headers: [], + }; + + if (axios.isAxiosError(e)) { + runnerResponse.endpoint = e.config.url ?? ""; + + if (e.response) { + const { data, status, statusText, headers } = e.response; + runnerResponse.body = data; + runnerResponse.statusText = statusText; + runnerResponse.status = status; + runnerResponse.headers = headers; + } else if ((e.config as RequestConfig).supported === false) { + status = 501; + runnerResponse.status = status; + runnerResponse.statusText = responseErrors[status]; + } else if (e.request) { + return E.left(error({ code: "REQUEST_ERROR", data: E.toError(e) })); + } + + return E.right(runnerResponse); + } + + return E.left(error({ code: "REQUEST_ERROR", data: E.toError(e) })); + } + }; + +/** + * Getter object methods for request-runner. + */ +const getRequest = { + method: (value: string | undefined) => + value ? (value.toUpperCase() as Method) : "GET", + + endpoint: (value: string | undefined): string => (value ? value : ""), + + finalEndpoint: (req: EffectiveHoppRESTRequest): string => + S.isEmpty(req.effectiveFinalURL) ? req.endpoint : req.effectiveFinalURL, + + finalHeaders: (req: EffectiveHoppRESTRequest) => + A.isNonEmpty(req.effectiveFinalHeaders) + ? req.effectiveFinalHeaders + : req.headers, + + finalParams: (req: EffectiveHoppRESTRequest) => + A.isNonEmpty(req.effectiveFinalParams) + ? req.effectiveFinalParams + : req.params, + + finalBody: (req: EffectiveHoppRESTRequest) => + req.effectiveFinalBody ? req.effectiveFinalBody : req.body.body, +}; + +/** + * Processes given request, which includes executing pre-request-script, + * running request & executing test-script. + * @param request Request to be processed. + * @param envs Global + selected envs used by requests with in collection. + * @returns Updated envs and current request's report. + */ +export const processRequest = + ( + request: HoppRESTRequest, + envs: HoppEnvs, + path: string + ): T.Task<{ envs: HoppEnvs; report: RequestReport }> => + async () => { + // Initialising updatedEnvs with given parameter envs, will eventually get updated. + const result = { + envs: envs, + report: {}, + }; + + // Initial value for current request's report with default values for properties. + const report: RequestReport = { + path: path, + tests: [], + errors: [], + result: true, + }; + + // Initial value for effective-request with default values for properties. + let effectiveRequest = { + ...request, + effectiveFinalBody: null, + effectiveFinalHeaders: [], + effectiveFinalParams: [], + effectiveFinalURL: "", + }; + + // Executing pre-request-script + const preRequestRes = await preRequestScriptRunner(request, envs)(); + if (E.isLeft(preRequestRes)) { + printPreRequestRunner.fail(); + + // Updating report for errors & current result + report.errors.push(preRequestRes.left); + report.result = report.result && false; + } else { + // Updating effective-request + effectiveRequest = preRequestRes.right; + } + + // Creating request-config for request-runner. + const requestConfig = createRequest(effectiveRequest); + + printRequestRunner.start(requestConfig); + + // Default value for request-runner's response. + let _requestRunnerRes: RequestRunnerResponse = { + endpoint: "", + method: "GET", + headers: [], + status: 400, + statusText: "", + body: Object(null), + }; + // Executing request-runner. + const requestRunnerRes = await requestRunner(requestConfig)(); + if (E.isLeft(requestRunnerRes)) { + // Updating report for errors & current result + report.errors.push(requestRunnerRes.left); + report.result = report.result && false; + + printRequestRunner.fail(); + } else { + _requestRunnerRes = requestRunnerRes.right; + printRequestRunner.success(_requestRunnerRes); + } + + // Extracting test-script-runner parameters. + const testScriptParams = getTestScriptParams( + _requestRunnerRes, + request, + envs + ); + + // Executing test-runner. + const testRunnerRes = await testRunner(testScriptParams)(); + if (E.isLeft(testRunnerRes)) { + printTestRunner.fail(); + + // Updating report with current errors & result. + report.errors.push(testRunnerRes.left); + report.result = report.result && false; + } else { + const { envs, testsReport } = testRunnerRes.right; + const _hasFailedTestCases = hasFailedTestCases(testsReport); + + // Updating report with current tests & result. + report.tests = testsReport; + report.result = report.result && _hasFailedTestCases; + + // Updating resulting envs from test-runner. + result.envs = envs; + + printTestSuitesReport(testsReport); + } + + result.report = report; + + return result; + }; + +/** + * Generates new request without any missing/invalid data using + * current request object. + * @param request Hopp rest request to be processed. + * @returns Updated request object free of invalid/missing data. + */ +export const preProcessRequest = ( + request: HoppRESTRequest +): HoppRESTRequest => { + const tempRequest = Object.assign({}, request); + if (!tempRequest.v) { + tempRequest.v = "1"; + } + if (!tempRequest.name) { + tempRequest.name = "Untitled Request"; + } + if (!tempRequest.method) { + tempRequest.method = "GET"; + } + if (!tempRequest.endpoint) { + tempRequest.endpoint = ""; + } + if (!tempRequest.params) { + tempRequest.params = []; + } + if (!tempRequest.headers) { + tempRequest.headers = []; + } + if (!tempRequest.preRequestScript) { + tempRequest.preRequestScript = ""; + } + if (!tempRequest.testScript) { + tempRequest.testScript = ""; + } + if (!tempRequest.auth) { + tempRequest.auth = { authActive: false, authType: "none" }; + } + if (!tempRequest.body) { + tempRequest.body = { contentType: null, body: null }; + } + return tempRequest; +}; diff --git a/packages/hoppscotch-cli/src/utils/test.ts b/packages/hoppscotch-cli/src/utils/test.ts new file mode 100644 index 000000000..22a034bcd --- /dev/null +++ b/packages/hoppscotch-cli/src/utils/test.ts @@ -0,0 +1,197 @@ +import { HoppRESTRequest } from "@hoppscotch/data"; +import { execTestScript, TestDescriptor } from "@hoppscotch/js-sandbox"; +import { flow, pipe } from "fp-ts/function"; +import * as RA from "fp-ts/ReadonlyArray"; +import * as A from "fp-ts/Array"; +import * as TE from "fp-ts/TaskEither"; +import * as T from "fp-ts/Task"; +import { + RequestRunnerResponse, + TestReport, + TestScriptParams, +} from "../interfaces/response"; +import { error, HoppCLIError } from "../types/errors"; +import { HoppEnvs } from "../types/request"; +import { ExpectResult, TestMetrics, TestRunnerRes } from "../types/response"; + +/** + * Executes test script and runs testDescriptorParser to generate test-report using + * expected-results, test-status & test-descriptor. + * @param testScriptData Parameters related to test-script function. + * @returns If executes successfully, we get TestRunnerRes(updated ENVs + test-reports). + * Else, HoppCLIError with appropriate code & data. + */ +export const testRunner = ( + testScriptData: TestScriptParams +): TE.TaskEither => + pipe( + /** + * Executing test-script. + */ + TE.of(testScriptData), + TE.chain(({ testScript, response, envs }) => + execTestScript(testScript, envs, response) + ), + + /** + * Recursively parsing test-results using test-descriptor-parser + * to generate test-reports. + */ + TE.chainTaskK(({ envs, tests }) => + pipe( + tests, + A.map(testDescriptorParser), + T.sequenceArray, + T.map( + flow( + RA.flatten, + RA.toArray, + (testsReport) => { envs, testsReport } + ) + ) + ) + ), + TE.mapLeft((e) => + error({ + code: "TEST_SCRIPT_ERROR", + data: e, + }) + ) + ); + +/** + * Recursive function to parse test-descriptor from nested-children and + * generate tests-report. + * @param testDescriptor Object with details of test-descriptor. + * @returns Flattened array of TestReport parsed from TestDescriptor. + */ +export const testDescriptorParser = ( + testDescriptor: TestDescriptor +): T.Task => + pipe( + /** + * Generate single TestReport from given testDescriptor. + */ + testDescriptor, + ({ expectResults, descriptor }) => + A.isNonEmpty(expectResults) + ? pipe( + expectResults, + A.reduce({ failing: 0, passing: 0 }, (prev, { status }) => + /** + * Incrementing number of passed test-cases if status is "pass", + * else, incrementing number of failed test-cases. + */ + status === "pass" + ? { failing: prev.failing, passing: prev.passing + 1 } + : { failing: prev.failing + 1, passing: prev.passing } + ), + ({ failing, passing }) => + { + failing, + passing, + descriptor, + expectResults, + }, + Array.of + ) + : [], + T.of, + + /** + * Recursive call to testDescriptorParser on testDescriptor's children. + * The result is concated with previous testReport. + */ + T.chain((testReport) => + pipe( + testDescriptor.children, + A.map(testDescriptorParser), + T.sequenceArray, + T.map(flow(RA.flatten, RA.toArray, A.concat(testReport))) + ) + ) + ); + +/** + * Extracts parameter object from request-runner's response, request and envs + * for test-runner. + * @param reqRunnerRes Provides response data. + * @param request Provides test-script data. + * @param envs Current ENVs state with-in collections-runner. + * @returns Object to be passed as parameter for test-runner + */ +export const getTestScriptParams = ( + reqRunnerRes: RequestRunnerResponse, + request: HoppRESTRequest, + envs: HoppEnvs +) => { + const testScriptParams: TestScriptParams = { + testScript: request.testScript, + response: { + body: reqRunnerRes.body, + status: reqRunnerRes.status, + headers: reqRunnerRes.headers, + }, + envs: envs, + }; + return testScriptParams; +}; + +/** + * Combines quantitative details (test-cases passed/failed) of each test-report + * to generate TestMetrics object with total test-cases & total test-suites. + * @param testsReport Contains details of each test-report (failed/passed test-cases). + * @returns Object containing details of total test-cases passed/failed and + * total test-suites passed/failed. + */ +export const getTestMetrics = (testsReport: TestReport[]): TestMetrics => + testsReport.reduce( + ({ testSuites, tests }, testReport) => ({ + tests: { + failing: tests.failing + testReport.failing, + passing: tests.passing + testReport.passing, + }, + testSuites: { + failing: testSuites.failing + (testReport.failing > 0 ? 1 : 0), + passing: testSuites.passing + (testReport.failing === 0 ? 1 : 0), + }, + }), + { + tests: { failing: 0, passing: 0 }, + testSuites: { failing: 0, passing: 0 }, + } + ); + +/** + * Filters tests-report containing atleast one or more failed test-cases. + * @param testsReport Provides "failing" test-cases data. + * @returns Tests report with one or more test-cases failing. + */ +export const getFailedTestsReport = (testsReport: TestReport[]) => + pipe( + testsReport, + A.filter(({ failing }) => failing > 0) + ); + +/** + * Filters expected-results containing which has status as "fail" or "error". + * @param expectResults Provides "status" data for each expected result. + * @returns Expected results with "fail" or "error" status. + */ +export const getFailedExpectedResults = (expectResults: ExpectResult[]) => + pipe( + expectResults, + A.filter(({ status }) => status !== "pass") + ); + +/** + * Checks if any of the tests-report have failed test-cases. + * @param testsReport Provides "failing" test-cases data. + * @returns True, if one or more failed test-cases found. + * False, if all test-cases passed. + */ +export const hasFailedTestCases = (testsReport: TestReport[]) => + pipe( + testsReport, + A.every(({ failing }) => failing === 0) + ); diff --git a/packages/hoppscotch-cli/tsconfig.json b/packages/hoppscotch-cli/tsconfig.json new file mode 100644 index 000000000..53d804a35 --- /dev/null +++ b/packages/hoppscotch-cli/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES6", + "module": "commonjs", + "outDir": ".", + "rootDir": ".", + "strict": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "composite": true + }, + "files": ["package.json"] +} diff --git a/packages/hoppscotch-cli/tsup.config.ts b/packages/hoppscotch-cli/tsup.config.ts new file mode 100644 index 000000000..f3ac47715 --- /dev/null +++ b/packages/hoppscotch-cli/tsup.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: [ "./src/index.ts" ], + outDir: "./dist/", + format: ["cjs"], + platform: "node", + sourcemap: true, + bundle: true, + target: "node12", + skipNodeModulesBundle: false, + esbuildOptions(options) { + options.bundle = true + }, + noExternal: [ + /\w+/ + ], + clean: true, +}); diff --git a/packages/hoppscotch-js-sandbox/jest.config.js b/packages/hoppscotch-js-sandbox/jest.config.js index d183c8a9d..6fe7cc21a 100644 --- a/packages/hoppscotch-js-sandbox/jest.config.js +++ b/packages/hoppscotch-js-sandbox/jest.config.js @@ -1,4 +1,4 @@ -export default { +module.exports = { preset: "ts-jest", testEnvironment: "node", collectCoverage: true, diff --git a/packages/hoppscotch-js-sandbox/package.json b/packages/hoppscotch-js-sandbox/package.json index 9ebf9de92..9aa81af68 100644 --- a/packages/hoppscotch-js-sandbox/package.json +++ b/packages/hoppscotch-js-sandbox/package.json @@ -3,19 +3,25 @@ "version": "2.0.0", "description": "JavaScript sandboxes for running external scripts used by Hoppscotch clients", "main": "./lib/index.js", + "module": "./lib/index.mjs", + "type": "commonjs", + "exports": { + ".": { + "require": "./lib/index.js", + "default": "./lib/index.mjs" + } + }, "types": "./lib/", - "type": "module", "engines": { "node": ">=14", "pnpm": ">=3" }, "scripts": { - "demo": "esrun src/demo.ts", "lint": "eslint --ext .ts,.js --ignore-path .gitignore .", "lintfix": "eslint --fix --ext .ts,.js --ignore-path .gitignore .", - "test": "npx jest", - "build": "npx tsc", - "clean": "npx tsc --build --clean", + "test": "pnpx jest", + "build": "pnpx tsup", + "clean": "pnpx tsc --build --clean", "postinstall": "pnpm run build", "prepublish": "pnpm run build", "do-lint": "pnpm run lint", @@ -37,7 +43,8 @@ "@hoppscotch/data": "workspace:^0.4.0", "fp-ts": "^2.11.9", "lodash": "^4.17.21", - "quickjs-emscripten": "^0.15.0" + "quickjs-emscripten": "^0.15.0", + "tsup": "^5.11.13" }, "devDependencies": { "@digitak/esrun": "^3.1.2", diff --git a/packages/hoppscotch-js-sandbox/src/demo.ts b/packages/hoppscotch-js-sandbox/src/demo.ts deleted file mode 100644 index 2652010f8..000000000 --- a/packages/hoppscotch-js-sandbox/src/demo.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { runTestScript } from "./index" -import { TestResponse } from "./test-runner" - -const dummyResponse: TestResponse = { - status: 200, - body: "hoi", - headers: [], -} -// eslint-disable-next-line prettier/prettier -;(async () => { - console.dir( - await runTestScript( - ` - pw.test("Arithmetic operations and toBe", () => { - const size = 500 + 500; - pw.expect(size).toBe(1000); - pw.expect(size - 500).toBe(500); - pw.expect(size * 4).toBe(4000); - pw.expect(size / 4).toBe(250); - }); - pw.test("toBeLevelxxx", () => { - pw.expect(200).toBeLevel2xx(); - pw.expect(204).toBeLevel2xx(); - pw.expect(300).not.toBeLevel2xx(); - pw.expect(300).toBeLevel3xx(); - pw.expect(304).toBeLevel3xx(); - pw.expect(204).not.toBeLevel3xx(); - pw.expect(401).toBeLevel4xx(); - pw.expect(404).toBeLevel4xx(); - pw.expect(204).not.toBeLevel4xx(); - pw.expect(501).toBeLevel5xx(); - pw.expect(504).toBeLevel5xx(); - pw.expect(204).not.toBeLevel5xx(); - }); - pw.test("toBeType", () => { - pw.expect("hello").toBeType("string"); - pw.expect(10).toBeType("number"); - pw.expect(true).toBeType("boolean"); - pw.expect("deffonotanumber").not.toBeType("number"); - }); - pw.test("toHaveLength", () => { - const arr = [1, 2, 3]; - pw.expect(arr).toHaveLength(3); - pw.expect(arr).not.toHaveLength(4); - }); - `, - dummyResponse - ), - { - depth: 100, - } - ) -})() diff --git a/packages/hoppscotch-js-sandbox/src/index.ts b/packages/hoppscotch-js-sandbox/src/index.ts index 766d31f1d..5d4449e34 100644 --- a/packages/hoppscotch-js-sandbox/src/index.ts +++ b/packages/hoppscotch-js-sandbox/src/index.ts @@ -3,11 +3,14 @@ import * as TE from "fp-ts/TaskEither" import { execPreRequestScript } from "./preRequest" import { execTestScript, - TestResponse, + TestResponse as _TestResponse, TestDescriptor as _TestDescriptor, TestResult, } from "./test-runner" +export * from "./test-runner" + +export type TestResponse = _TestResponse export type TestDescriptor = _TestDescriptor export type SandboxTestResult = TestResult & { tests: TestDescriptor } diff --git a/packages/hoppscotch-js-sandbox/tsconfig.json b/packages/hoppscotch-js-sandbox/tsconfig.json index 8eb23a6d1..ff918da2d 100644 --- a/packages/hoppscotch-js-sandbox/tsconfig.json +++ b/packages/hoppscotch-js-sandbox/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "target": "ES6", - "module": "ESNext", + "module": "CommonJS", "moduleResolution": "Node", "skipLibCheck": true, "lib": ["ESNext", "ESNext.AsyncIterable", "DOM"], @@ -19,5 +19,5 @@ "sourceMap": true }, "include": ["./src", "./src/global.d.ts"], - "exclude": ["node_modules", "./src/__tests__", "./src/demo.ts"] + "exclude": ["node_modules", "./src/__tests__"] } diff --git a/packages/hoppscotch-js-sandbox/tsup.config.ts b/packages/hoppscotch-js-sandbox/tsup.config.ts new file mode 100644 index 000000000..483eeabea --- /dev/null +++ b/packages/hoppscotch-js-sandbox/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "tsup" + +export default defineConfig({ + entry: ["src/index.ts"], + outDir: "./lib/", + format: ["esm", "cjs"], + dts: true, + splitting: true, + sourcemap: true, + clean: true, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 469c49320..f2aba61c2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -343,6 +343,42 @@ importers: vue-template-babel-compiler: 1.1.3 worker-loader: 3.0.8 + packages/hoppscotch-cli: + specifiers: + '@hoppscotch/data': workspace:^0.4.0 + '@hoppscotch/js-sandbox': workspace:^2.0.0 + '@swc/core': ^1.2.160 + '@types/axios': ^0.14.0 + '@types/chalk': ^2.2.0 + '@types/commander': ^2.12.2 + axios: ^0.21.4 + chalk: ^4.1.1 + commander: ^8.0.0 + esm: ^3.2.25 + fp-ts: ^2.11.3 + lodash: ^4.17.21 + prettier: ^2.5.1 + qs: ^6.10.3 + tsup: ^5.11.13 + typescript: ^4.3.5 + devDependencies: + '@hoppscotch/data': link:../hoppscotch-data + '@hoppscotch/js-sandbox': link:../hoppscotch-js-sandbox + '@swc/core': 1.2.160 + '@types/axios': 0.14.0 + '@types/chalk': 2.2.0 + '@types/commander': 2.12.2 + axios: 0.21.4 + chalk: 4.1.2 + commander: 8.3.0 + esm: 3.2.25 + fp-ts: 2.11.9 + lodash: 4.17.21 + prettier: 2.6.0 + qs: 6.10.3 + tsup: 5.12.1_typescript@4.6.2 + typescript: 4.6.2 + packages/hoppscotch-data: specifiers: '@types/lodash': ^4.14.180 @@ -380,12 +416,14 @@ importers: prettier: ^2.6.0 quickjs-emscripten: ^0.15.0 ts-jest: ^27.1.3 + tsup: ^5.11.13 typescript: ^4.6.2 dependencies: '@hoppscotch/data': link:../hoppscotch-data fp-ts: 2.11.9 lodash: 4.17.21 quickjs-emscripten: 0.15.0 + tsup: 5.12.1_typescript@4.6.2 devDependencies: '@digitak/esrun': 3.1.2 '@relmify/jest-fp-ts': 2.0.0_fp-ts@2.11.9+io-ts@2.2.16 @@ -4063,11 +4101,11 @@ packages: ufo: 0.7.11 dev: false - /@nuxt/kit-edge/3.0.0-27465767.70f067a: - resolution: {integrity: sha512-3WJfEKf7ymS+lDnDi7TEIqigWyQoCDLwdYGplpRnjFBALGIbMhnv7x4LGgYroB0RT6QS84k6HgTiYaZf5OERuw==} + /@nuxt/kit-edge/3.0.0-27470397.9ebea90: + resolution: {integrity: sha512-mBUqr6uAqXBurjf63m3/AtaLlKnXMy2OguW4nCismxE30/fz0LtF/MndKcX7SrfeOcpsnrOYvIUhgLheS0Lhpg==} engines: {node: ^14.16.0 || ^16.11.0 || ^17.0.0} dependencies: - '@nuxt/schema': /@nuxt/schema-edge/3.0.0-27465767.70f067a + '@nuxt/schema': /@nuxt/schema-edge/3.0.0-27470397.9ebea90 c12: 0.2.4 consola: 2.15.3 defu: 6.0.0 @@ -4077,13 +4115,13 @@ packages: jiti: 1.13.0 knitwork: 0.1.1 lodash.template: 4.5.0 - mlly: 0.4.3 + mlly: 0.5.1 pathe: 0.2.0 pkg-types: 0.3.2 scule: 0.2.1 semver: 7.3.5 - unctx: 1.0.2 - unimport: 0.1.1 + unctx: 1.1.3 + unimport: 0.1.3 untyped: 0.4.3 transitivePeerDependencies: - esbuild @@ -4115,8 +4153,8 @@ packages: - encoding dev: false - /@nuxt/schema-edge/3.0.0-27465767.70f067a: - resolution: {integrity: sha512-S9RzCddYD2fyjUJYUiiNRwOdwjQjoPARm/4zXYx7sXnIOKvY7P84RgtzTNKDqlTilTY6j0NGShiqn/F9sC3mlw==} + /@nuxt/schema-edge/3.0.0-27470397.9ebea90: + resolution: {integrity: sha512-NTVqtIozgUW83uRLoEufjCulfkcZKPjrUXIuVNMmJwkkpO1SxJt4jY2uVTU8ahi8uG+iETkERcMbGi8NA86dtw==} engines: {node: ^14.16.0 || ^16.11.0 || ^17.0.0} dependencies: c12: 0.2.4 @@ -4128,7 +4166,7 @@ packages: scule: 0.2.1 std-env: 3.0.1 ufo: 0.8.1 - unimport: 0.1.1 + unimport: 0.1.3 transitivePeerDependencies: - esbuild - rollup @@ -4848,6 +4886,143 @@ packages: resolution: {integrity: sha512-2pTGuibAXJswAPJjaKisthqS/NOK5ypG4LYT6tEAV0S/mxW0zOIvYvGK0V8w8+SHxAm6vRMSjqSalFXeBAqs+Q==} dev: false + /@swc/core-android-arm-eabi/1.2.160: + resolution: {integrity: sha512-VzFP7tYgvpkUhd8wgyNtERqvoPBBDretyMFxAxPe2SxClaBs9Ka95PdiPPZalRq+vFCb/dFxD8Vhz+XO16Kpjg==} + engines: {node: '>=10'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@swc/core-android-arm64/1.2.160: + resolution: {integrity: sha512-m+xqQaa7TqW3Vm9MUvITtdU8OlAc/9yT+TgOS4l8WlfFI87IDnLLfinKKEp+xfKwzYDdIsh+sC+jdGdIBTMB+Q==} + engines: {node: '>=10'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@swc/core-darwin-arm64/1.2.160: + resolution: {integrity: sha512-9bG70KYKvjNf7tZtjOu1h4kDZPtoidZptIXPGSHuUgJ1BbSJYpfRR5xAmq4k37+GqOjIPJp4+lSGQPa2HfejpA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@swc/core-darwin-x64/1.2.160: + resolution: {integrity: sha512-+b4HdKAVf/XPZ9DjgG2axGLbquPEuYwEP3zeWgbWn0s0FYQ7WTFxznf3YrTJE9MYadJeCOs3U80E2xVAtRRS9Q==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@swc/core-freebsd-x64/1.2.160: + resolution: {integrity: sha512-E5agJwv+RVMoZ8FQIPSO5wLPDQx6jqcMpV207EB3pPaxPWGe4n3DH3vcibHp80RACDNdiaqo5lBeBnGJI4ithw==} + engines: {node: '>=10'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-arm-gnueabihf/1.2.160: + resolution: {integrity: sha512-uCttZRNx+lWVhCYGC6/pGUej08g1SQc5am6R9NVFh111goytcdlPnC4jV8oWzq2QhDWkkKxLoP2CZOytzI4+0w==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-arm64-gnu/1.2.160: + resolution: {integrity: sha512-sB18roiv8m/zsY6tXLSrbUls0eKkSkxOEF0ennXVEtz97rMJ+WWnkOc8gI+rUpj3MHbVAIxyDNyyZU4cH5g1jQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-arm64-musl/1.2.160: + resolution: {integrity: sha512-PJ7Ukb+BRR3pGYcUag8qRWOB11eByc5YLx/xAMSc3bRmaYW/oj6s8k+1DYiR//BAuNQdf14MpMFzDuWiDEUh7A==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-x64-gnu/1.2.160: + resolution: {integrity: sha512-wVh8Q86xz3t0y5zoUryWQ64bFG/YxdcykBgaog8lU9xkFb1KSqVRE9ia7aKA12/ZtAfpJZLRBleZxBAcaCg9FQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-x64-musl/1.2.160: + resolution: {integrity: sha512-AnWdarl9WWuDdbc2AX1w76W1jaekSCokxRrWdSGUgQytaZRtybKZEgThvJCQDrSlYQD4XDOhhVRCurTvy4JsfQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-win32-arm64-msvc/1.2.160: + resolution: {integrity: sha512-ScL27mZRTwEIqBIv9RY34nQvyBvhosiM5Lus4dCFmS71flPcAEv7hJgy4GE3YUQV0ryGNK9NaO43H8sAyNwKVQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@swc/core-win32-ia32-msvc/1.2.160: + resolution: {integrity: sha512-e75zbWlhlyrd5HdrYzELa6OlZxgyaVpJj+c9xMD95HcdklVbmsyt1vuqRxMyqaZUDLyehwwCDRX/ZeDme//M/A==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@swc/core-win32-x64-msvc/1.2.160: + resolution: {integrity: sha512-GAYT+WzYQY4sr17S21yJh4flJp/sQ62mAs6RfN89p7jIWgm0Bl/SooRl6ocsftTlnZm7K7QC8zmQVeNCWDCLPw==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@swc/core/1.2.160: + resolution: {integrity: sha512-nXoC7HA+aY7AtBPsiqGXocoRLAzzA7MV+InWQtILN7Uru4hB9+rLnLCPc3zSdg7pgnxJLa1tHup1Rz7Vv6TcIQ==} + engines: {node: '>=10'} + hasBin: true + optionalDependencies: + '@swc/core-android-arm-eabi': 1.2.160 + '@swc/core-android-arm64': 1.2.160 + '@swc/core-darwin-arm64': 1.2.160 + '@swc/core-darwin-x64': 1.2.160 + '@swc/core-freebsd-x64': 1.2.160 + '@swc/core-linux-arm-gnueabihf': 1.2.160 + '@swc/core-linux-arm64-gnu': 1.2.160 + '@swc/core-linux-arm64-musl': 1.2.160 + '@swc/core-linux-x64-gnu': 1.2.160 + '@swc/core-linux-x64-musl': 1.2.160 + '@swc/core-win32-arm64-msvc': 1.2.160 + '@swc/core-win32-ia32-msvc': 1.2.160 + '@swc/core-win32-x64-msvc': 1.2.160 + dev: true + /@szmarczak/http-timer/1.1.2: resolution: {integrity: sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==} engines: {node: '>=6'} @@ -4917,6 +5092,15 @@ packages: postcss: 7.0.39 dev: true + /@types/axios/0.14.0: + resolution: {integrity: sha1-7CMA++fX3d1+udOr+HmZlkyvzkY=} + deprecated: This is a stub types definition for axios (https://github.com/mzabriskie/axios). axios provides its own type definitions, so you don't need @types/axios installed! + dependencies: + axios: 0.26.1 + transitivePeerDependencies: + - debug + dev: true + /@types/babel__core/7.1.14: resolution: {integrity: sha512-zGZJzzBUVDo/eV6KgbE0f0ZI7dInEYvo12Rb70uNQDshC3SkRMb67ja0GgRHZgAX3Za6rhaWlvbDO8rrGyAb1g==} dependencies: @@ -4985,6 +5169,13 @@ packages: '@types/responselike': 1.0.0 dev: true + /@types/chalk/2.2.0: + resolution: {integrity: sha512-1zzPV9FDe1I/WHhRkf9SNgqtRJWZqrBWgu7JGveuHmmyR9CnAPCie2N/x+iHrgnpYBIcCJWHBoMRv2TRWktsvw==} + deprecated: This is a stub types definition for chalk (https://github.com/chalk/chalk). chalk provides its own type definitions, so you don't need @types/chalk installed! + dependencies: + chalk: 4.1.2 + dev: true + /@types/clean-css/4.2.5: resolution: {integrity: sha512-NEzjkGGpbs9S9fgC4abuBvTpVwE3i+Acu9BBod3PUyjDVZcNsGx61b8r2PphR61QGPnn0JHVs5ey6/I4eTrkxw==} dependencies: @@ -4998,6 +5189,13 @@ packages: '@types/tern': 0.23.4 dev: true + /@types/commander/2.12.2: + resolution: {integrity: sha512-0QEFiR8ljcHp9bAbWxecjVRuAMr16ivPiGOw6KFQBVrVd0RQIcM3xKdRisH2EDWgVWujiYtHwhSkSUoAAGzH7Q==} + deprecated: This is a stub types definition for commander (https://github.com/tj/commander.js). commander provides its own type definitions, so you don't need @types/commander installed! + dependencies: + commander: 8.3.0 + dev: true + /@types/component-emitter/1.2.11: resolution: {integrity: sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ==} dev: false @@ -6338,7 +6536,6 @@ packages: /any-promise/1.3.0: resolution: {integrity: sha1-q8av7tzqUugJzcA3au0845Y10X8=} - dev: true /anymatch/2.0.0: resolution: {integrity: sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==} @@ -6545,7 +6742,6 @@ packages: follow-redirects: 1.14.9 transitivePeerDependencies: - debug - dev: false /axios/0.26.1: resolution: {integrity: sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==} @@ -6553,7 +6749,6 @@ packages: follow-redirects: 1.14.9 transitivePeerDependencies: - debug - dev: false /babel-code-frame/6.26.0: resolution: {integrity: sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=} @@ -7211,7 +7406,6 @@ packages: dependencies: esbuild: 0.14.26 load-tsconfig: 0.2.3 - dev: true /bytes/3.0.0: resolution: {integrity: sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=} @@ -7233,7 +7427,6 @@ packages: /cac/6.7.12: resolution: {integrity: sha512-rM7E2ygtMkJqD9c7WnFU6fruFcN3xe4FM5yUmgxhZzIKJk4uHl9U/fhwdajGFQbQuv43FAUo1Fe8gX/oIKDeSA==} engines: {node: '>=8'} - dev: true /cacache/12.0.4: resolution: {integrity: sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==} @@ -7825,7 +8018,6 @@ packages: /commander/8.3.0: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} - dev: false /common-tags/1.8.2: resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} @@ -9158,7 +9350,6 @@ packages: cpu: [x64] os: [android] requiresBuild: true - dev: true optional: true /esbuild-android-arm64/0.14.26: @@ -9167,7 +9358,6 @@ packages: cpu: [arm64] os: [android] requiresBuild: true - dev: true optional: true /esbuild-darwin-64/0.14.26: @@ -9176,7 +9366,6 @@ packages: cpu: [x64] os: [darwin] requiresBuild: true - dev: true optional: true /esbuild-darwin-arm64/0.14.26: @@ -9185,7 +9374,6 @@ packages: cpu: [arm64] os: [darwin] requiresBuild: true - dev: true optional: true /esbuild-freebsd-64/0.14.26: @@ -9194,7 +9382,6 @@ packages: cpu: [x64] os: [freebsd] requiresBuild: true - dev: true optional: true /esbuild-freebsd-arm64/0.14.26: @@ -9203,7 +9390,6 @@ packages: cpu: [arm64] os: [freebsd] requiresBuild: true - dev: true optional: true /esbuild-linux-32/0.14.26: @@ -9212,7 +9398,6 @@ packages: cpu: [ia32] os: [linux] requiresBuild: true - dev: true optional: true /esbuild-linux-64/0.14.26: @@ -9221,7 +9406,6 @@ packages: cpu: [x64] os: [linux] requiresBuild: true - dev: true optional: true /esbuild-linux-arm/0.14.26: @@ -9230,7 +9414,6 @@ packages: cpu: [arm] os: [linux] requiresBuild: true - dev: true optional: true /esbuild-linux-arm64/0.14.26: @@ -9239,7 +9422,6 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true - dev: true optional: true /esbuild-linux-mips64le/0.14.26: @@ -9248,7 +9430,6 @@ packages: cpu: [mips64el] os: [linux] requiresBuild: true - dev: true optional: true /esbuild-linux-ppc64le/0.14.26: @@ -9257,7 +9438,6 @@ packages: cpu: [ppc64] os: [linux] requiresBuild: true - dev: true optional: true /esbuild-linux-riscv64/0.14.26: @@ -9266,7 +9446,6 @@ packages: cpu: [riscv64] os: [linux] requiresBuild: true - dev: true optional: true /esbuild-linux-s390x/0.14.26: @@ -9275,7 +9454,6 @@ packages: cpu: [s390x] os: [linux] requiresBuild: true - dev: true optional: true /esbuild-netbsd-64/0.14.26: @@ -9284,7 +9462,6 @@ packages: cpu: [x64] os: [netbsd] requiresBuild: true - dev: true optional: true /esbuild-openbsd-64/0.14.26: @@ -9293,7 +9470,6 @@ packages: cpu: [x64] os: [openbsd] requiresBuild: true - dev: true optional: true /esbuild-sunos-64/0.14.26: @@ -9302,7 +9478,6 @@ packages: cpu: [x64] os: [sunos] requiresBuild: true - dev: true optional: true /esbuild-windows-32/0.14.26: @@ -9311,7 +9486,6 @@ packages: cpu: [ia32] os: [win32] requiresBuild: true - dev: true optional: true /esbuild-windows-64/0.14.26: @@ -9320,7 +9494,6 @@ packages: cpu: [x64] os: [win32] requiresBuild: true - dev: true optional: true /esbuild-windows-arm64/0.14.26: @@ -9329,7 +9502,6 @@ packages: cpu: [arm64] os: [win32] requiresBuild: true - dev: true optional: true /esbuild/0.12.29: @@ -9364,7 +9536,6 @@ packages: esbuild-windows-32: 0.14.26 esbuild-windows-64: 0.14.26 esbuild-windows-arm64: 0.14.26 - dev: true /escalade/3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} @@ -9721,6 +9892,11 @@ packages: - supports-color dev: true + /esm/3.2.25: + resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} + engines: {node: '>=6'} + dev: true + /espree/9.3.1: resolution: {integrity: sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -10218,7 +10394,6 @@ packages: peerDependenciesMeta: debug: optional: true - dev: false /for-in/1.0.2: resolution: {integrity: sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=} @@ -10293,7 +10468,6 @@ packages: /fp-ts/2.11.9: resolution: {integrity: sha512-GhYlNKkCOfdjp71ocdtyaQGoqCswEoWDJLRr+2jClnBBq2dnSOtd6QxmJdALq8UhfqCyZZ0f0lxadU4OhwY9nw==} - dev: false /fragment-cache/0.2.1: resolution: {integrity: sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=} @@ -10576,7 +10750,6 @@ packages: minimatch: 3.1.2 once: 1.4.0 path-is-absolute: 1.0.1 - dev: true /glob/7.2.0: resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==} @@ -12516,7 +12689,6 @@ packages: /joycon/3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} - dev: true /js-base64/2.6.4: resolution: {integrity: sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==} @@ -12985,7 +13157,6 @@ packages: /load-tsconfig/0.2.3: resolution: {integrity: sha512-iyT2MXws+dc2Wi6o3grCFtGXpeMvHmJqS27sMPGtV2eUu4PeFnG+33I8BlFK1t1NWMjOpcx9bridn5yxLDX2gQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true /loader-runner/2.4.0: resolution: {integrity: sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==} @@ -13707,6 +13878,13 @@ packages: resolution: {integrity: sha512-xezyv7hnfFPuiDS3AiJuWs0OxlvooS++3L2lURvmh/1n7UG4O2Ehz9UkwWgg3wyLEPKGVfJLlr2DjjTCl9UJTg==} dev: true + /mlly/0.5.1: + resolution: {integrity: sha512-0axKqxbYyQvaAfi6BNqDluCJqg6RkjdsdFxSDoiP6l5HplSTr3ie5Vkxvw9U9ogdj65x56amTnAn+xSoP727Rg==} + dependencies: + pathe: 0.2.0 + pkg-types: 0.3.2 + dev: true + /mocha/9.2.2: resolution: {integrity: sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==} engines: {node: '>= 12.0.0'} @@ -13786,7 +13964,6 @@ packages: any-promise: 1.3.0 object-assign: 4.1.1 thenify-all: 1.6.0 - dev: true /nan/2.15.0: resolution: {integrity: sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==} @@ -14067,7 +14244,7 @@ packages: /nuxt-windicss/2.2.8: resolution: {integrity: sha512-l0mONjhsxhkDa/XVLbQZIaA2+xxo+IBCWyieR7vq1Rl4BDRghNXYPMi8H04qwNND8R0cQQNQYUmECxjUg1LlAQ==} dependencies: - '@nuxt/kit': /@nuxt/kit-edge/3.0.0-27465767.70f067a + '@nuxt/kit': /@nuxt/kit-edge/3.0.0-27470397.9ebea90 '@windicss/plugin-utils': 1.8.3 consola: 2.15.3 defu: 5.0.1 @@ -14676,7 +14853,6 @@ packages: /pirates/4.0.5: resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==} engines: {node: '>= 6'} - dev: true /pkg-dir/3.0.0: resolution: {integrity: sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==} @@ -14966,7 +15142,6 @@ packages: dependencies: lilconfig: 2.0.4 yaml: 1.10.2 - dev: true /postcss-loader/3.0.0: resolution: {integrity: sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA==} @@ -15703,7 +15878,6 @@ packages: engines: {node: '>=0.6'} dependencies: side-channel: 1.0.4 - dev: false /query-string/4.3.4: resolution: {integrity: sha1-u7aTucqRXCMlFbIosaArYJBD2+s=} @@ -16250,7 +16424,6 @@ packages: hasBin: true optionalDependencies: fsevents: 2.3.2 - dev: true /run-async/2.4.1: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} @@ -17424,7 +17597,6 @@ packages: mz: 2.7.0 pirates: 4.0.5 ts-interface-checker: 0.1.13 - dev: true /supports-color/2.0.0: resolution: {integrity: sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=} @@ -17726,13 +17898,11 @@ packages: engines: {node: '>=0.8'} dependencies: thenify: 3.3.1 - dev: true /thenify/3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} dependencies: any-promise: 1.3.0 - dev: true /thread-loader/3.0.4_webpack@4.46.0: resolution: {integrity: sha512-ByaL2TPb+m6yArpqQUZvP+5S1mZtXsEP7nWKKlAUTm7fCml8kB5s1uI3+eHRP2bk5mVYfRSBI7FFf+tWEyLZwA==} @@ -17905,7 +18075,6 @@ packages: /tree-kill/1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true - dev: true /trim-newlines/3.0.1: resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} @@ -17924,7 +18093,6 @@ packages: /ts-interface-checker/0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - dev: true /ts-jest/27.1.3_60149d457e34ffba7d4e845dde6a1263: resolution: {integrity: sha512-6Nlura7s6uM9BVUAoqLH7JHyMXjz8gluryjpPXxr3IxZdAXnU6FhjvVLHFtfd1vsE1p8zD1OJfskkc0jhTSnkA==} @@ -18124,6 +18292,34 @@ packages: - ts-node dev: true + /tsup/5.12.1_typescript@4.6.2: + resolution: {integrity: sha512-vI7E4T6+6n5guQ9UKUOkQmzd1n4V9abGK71lbnzJMLJspbkNby5zlwWvgvHafLdYCb1WXpjFuqqmNLjBA0Wz3g==} + hasBin: true + peerDependencies: + typescript: ^4.1.0 + peerDependenciesMeta: + typescript: + optional: true + dependencies: + bundle-require: 3.0.4_esbuild@0.14.26 + cac: 6.7.12 + chokidar: 3.5.3 + debug: 4.3.3 + esbuild: 0.14.26 + execa: 5.1.1 + globby: 11.1.0 + joycon: 3.1.1 + postcss-load-config: 3.1.3 + resolve-from: 5.0.0 + rollup: 2.70.1 + source-map: 0.7.3 + sucrase: 3.20.3 + tree-kill: 1.2.2 + typescript: 4.6.2 + transitivePeerDependencies: + - supports-color + - ts-node + /tsutils/3.21.0_typescript@4.6.2: resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} @@ -18244,8 +18440,18 @@ packages: engines: {node: '>=0.10.0'} dev: true - /unctx/1.0.2: - resolution: {integrity: sha512-qxRfnQZWJqkg180JeOCJEvtjj5/7wnWVqkNHln8muY5/z8kMWBFqikFBPwIPCQrZJ+jtaSWkVHJkuHUAXls6zw==} + /unctx/1.1.3: + resolution: {integrity: sha512-x3sI4ueuHw05zQgbzfpzF9XO+zw0C7sPCPoTRIgVPAXr76HALqcV97cJDEa5Nj+WCAl7V2rgSZR/p4uM78gO2g==} + dependencies: + acorn: 8.7.0 + estree-walker: 2.0.2 + magic-string: 0.26.1 + unplugin: 0.5.2 + transitivePeerDependencies: + - esbuild + - rollup + - vite + - webpack dev: true /undici/4.12.1: @@ -18285,8 +18491,8 @@ packages: engines: {node: '>= 0.4.12'} dev: true - /unimport/0.1.1: - resolution: {integrity: sha512-M3DEUx4idjPlAHyncuhvwemOYalbrl4gkxnaokrYVTSiFiu+WqM6kEybwMahRhNhVP4NLBs8QN/64BT2ujrYsA==} + /unimport/0.1.3: + resolution: {integrity: sha512-P21Psvf8rqgbx3pLpxvmOlTnwUU3bkRiou8hAijGfbuq5ioUILsffT48aTyIYDsYowoWoYtamn9d1IDxXOV37Q==} dependencies: '@rollup/pluginutils': 4.2.0 escape-string-regexp: 5.0.0 @@ -18427,6 +18633,28 @@ packages: webpack-virtual-modules: 0.4.3 dev: true + /unplugin/0.5.2: + resolution: {integrity: sha512-3SPYtus/56cxyD4jfjrnqCvb6jPxvdqJNaRXnEaG2BhNEMaoygu/39AG+LwKmiIUzj4XHyitcfZ7scGlWfEigA==} + peerDependencies: + esbuild: '>=0.13' + rollup: ^2.50.0 + vite: ^2.3.0 + webpack: 4 || 5 + peerDependenciesMeta: + esbuild: + optional: true + rollup: + optional: true + vite: + optional: true + webpack: + optional: true + dependencies: + chokidar: 3.5.3 + webpack-sources: 3.2.3 + webpack-virtual-modules: 0.4.3 + dev: true + /unquote/1.1.1: resolution: {integrity: sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=} @@ -18997,6 +19225,11 @@ packages: source-map: 0.6.1 dev: false + /webpack-sources/3.2.3: + resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} + engines: {node: '>=10.13.0'} + dev: true + /webpack-virtual-modules/0.4.3: resolution: {integrity: sha512-5NUqC2JquIL2pBAAo/VfBP6KuGkHIZQXW/lNKupLPfhViwh8wNsu0BObtl09yuKZszeEUfbXz8xhrHvSG16Nqw==}