Feature: hopp-cli in TypeScript (#2074)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com> Co-authored-by: liyasthomas <liyascthomas@gmail.com> Co-authored-by: Gita Alekhya Paul <gitaalekhyapaul@gmail.com>
This commit is contained in:
146
packages/hoppscotch-cli/.gitignore
vendored
Normal file
146
packages/hoppscotch-cli/.gitignore
vendored
Normal file
@@ -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
|
||||
8
packages/hoppscotch-cli/.prettierrc
Normal file
8
packages/hoppscotch-cli/.prettierrc
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"semi": true,
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": false,
|
||||
"printWidth": 80,
|
||||
"useTabs": false,
|
||||
"tabWidth": 2
|
||||
}
|
||||
128
packages/hoppscotch-cli/CODE_OF_CONDUCT.md
Normal file
128
packages/hoppscotch-cli/CODE_OF_CONDUCT.md
Normal file
@@ -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.
|
||||
57
packages/hoppscotch-cli/CONTRIBUTING.md
Normal file
57
packages/hoppscotch-cli/CONTRIBUTING.md
Normal file
@@ -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
|
||||
```
|
||||
21
packages/hoppscotch-cli/LICENSE
Normal file
21
packages/hoppscotch-cli/LICENSE
Normal file
@@ -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.
|
||||
52
packages/hoppscotch-cli/README.md
Normal file
52
packages/hoppscotch-cli/README.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Hoppscotch CLI <sup>ALPHA</sup>
|
||||
|
||||
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 <file_path>`**
|
||||
- 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)**
|
||||
3
packages/hoppscotch-cli/bin/hopp
Executable file
3
packages/hoppscotch-cli/bin/hopp
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env node
|
||||
// * The entry point of the CLI
|
||||
require("../dist").cli(process.argv);
|
||||
45
packages/hoppscotch-cli/package.json
Normal file
45
packages/hoppscotch-cli/package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
24
packages/hoppscotch-cli/src/commands/test.ts
Normal file
24
packages/hoppscotch-cli/src/commands/test.ts
Normal file
@@ -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);
|
||||
})
|
||||
)();
|
||||
};
|
||||
85
packages/hoppscotch-cli/src/handlers/error.ts
Normal file
85
packages/hoppscotch-cli/src/handlers/error.ts
Normal file
@@ -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 = <T extends HoppErrorCode>(error: HoppError<T>) => {
|
||||
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);
|
||||
}
|
||||
};
|
||||
60
packages/hoppscotch-cli/src/index.ts
Normal file
60
packages/hoppscotch-cli/src/index.ts
Normal file
@@ -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) {}
|
||||
};
|
||||
36
packages/hoppscotch-cli/src/interfaces/request.ts
Normal file
36
packages/hoppscotch-cli/src/interfaces/request.ts
Normal file
@@ -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<any>;
|
||||
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;
|
||||
}
|
||||
68
packages/hoppscotch-cli/src/interfaces/response.ts
Normal file
68
packages/hoppscotch-cli/src/interfaces/response.ts
Normal file
@@ -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;
|
||||
}
|
||||
19
packages/hoppscotch-cli/src/tsconfig.json
Normal file
19
packages/hoppscotch-cli/src/tsconfig.json
Normal file
@@ -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": "../"
|
||||
}
|
||||
]
|
||||
}
|
||||
36
packages/hoppscotch-cli/src/types/errors.ts
Normal file
36
packages/hoppscotch-cli/src/types/errors.ts
Normal file
@@ -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 HoppErrorCode> = T extends null
|
||||
? { code: T }
|
||||
: { code: T } & HoppErrors[T];
|
||||
|
||||
export const error = <T extends HoppErrorCode>(error: HoppError<T>) => error;
|
||||
export type HoppCLIError = HoppError<HoppErrorCode>;
|
||||
export type HoppErrnoException = NodeJS.ErrnoException;
|
||||
31
packages/hoppscotch-cli/src/types/request.ts
Normal file
31
packages/hoppscotch-cli/src/types/request.ts
Normal file
@@ -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<HoppRESTRequest>;
|
||||
};
|
||||
|
||||
export type RequestReport = {
|
||||
path: string;
|
||||
tests: TestReport[];
|
||||
errors: HoppCLIError[];
|
||||
result: boolean;
|
||||
};
|
||||
28
packages/hoppscotch-cli/src/types/response.ts
Normal file
28
packages/hoppscotch-cli/src/types/response.ts
Normal file
@@ -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[];
|
||||
};
|
||||
155
packages/hoppscotch-cli/src/utils/checks.ts
Normal file
155
packages/hoppscotch-cli/src/utils/checks.ts
Normal file
@@ -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 = <P extends PropertyKey>(
|
||||
target: object,
|
||||
prop: P
|
||||
): target is Record<P, unknown> => 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<HoppRESTRequest> => {
|
||||
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<HoppCLIError, string> =>
|
||||
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;
|
||||
};
|
||||
129
packages/hoppscotch-cli/src/utils/collections.ts
Normal file
129
packages/hoppscotch-cli/src/utils/collections.ts
Normal file
@@ -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<HoppRESTRequest>[]): T.Task<RequestReport[]> =>
|
||||
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>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<HoppRESTRequest>[]
|
||||
): CollectionStack[] =>
|
||||
pipe(
|
||||
collections,
|
||||
A.map(
|
||||
(collection) => <CollectionStack>{ 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);
|
||||
};
|
||||
7
packages/hoppscotch-cli/src/utils/constants.ts
Normal file
7
packages/hoppscotch-cli/src/utils/constants.ts
Normal file
@@ -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;
|
||||
145
packages/hoppscotch-cli/src/utils/display.ts
Normal file
145
packages/hoppscotch-cli/src/utils/display.ts
Normal file
@@ -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.")),
|
||||
};
|
||||
37
packages/hoppscotch-cli/src/utils/functions/array.ts
Normal file
37
packages/hoppscotch-cli/src/utils/functions/array.ts
Normal file
@@ -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 =
|
||||
<T>(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 =
|
||||
<T, U>(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<KeyType, ValueType> =>
|
||||
tuples.length > 0
|
||||
? (Object.assign as any)(...tuples.map(([key, val]) => ({ [key]: val })))
|
||||
: {};
|
||||
113
packages/hoppscotch-cli/src/utils/getters.ts
Normal file
113
packages/hoppscotch-cli/src/utils/getters.ts
Normal file
@@ -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(<Record<string, string>>{}, (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,
|
||||
};
|
||||
80
packages/hoppscotch-cli/src/utils/mutators.ts
Normal file
80
packages/hoppscotch-cli/src/utils/mutators.ts
Normal file
@@ -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<HoppRESTRequest>,
|
||||
*/
|
||||
export const parseCollectionData = (
|
||||
path: string
|
||||
): TE.TaskEither<HoppCLIError, HoppCollection<HoppRESTRequest>[]> =>
|
||||
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.",
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
268
packages/hoppscotch-cli/src/utils/pre-request.ts
Normal file
268
packages/hoppscotch-cli/src/utils/pre-request.ts
Normal file
@@ -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<HoppCLIError, EffectiveHoppRESTRequest> =>
|
||||
pipe(
|
||||
TE.of(request),
|
||||
TE.chain(({ preRequestScript }) =>
|
||||
runPreRequestScript(preRequestScript, envs)
|
||||
),
|
||||
TE.map(
|
||||
({ selected, global }) =>
|
||||
<Environment>{ 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<HoppCLIError, EffectiveHoppRESTRequest> {
|
||||
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<HoppCLIError, string | null | FormData> {
|
||||
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})`,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
321
packages/hoppscotch-cli/src/utils/request.ts
Normal file
321
packages/hoppscotch-cli/src/utils/request.ts
Normal file
@@ -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<HoppCLIError, RequestRunnerResponse> =>
|
||||
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: <HoppEnvs>envs,
|
||||
report: <RequestReport>{},
|
||||
};
|
||||
|
||||
// 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 = <EffectiveHoppRESTRequest>{
|
||||
...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;
|
||||
};
|
||||
197
packages/hoppscotch-cli/src/utils/test.ts
Normal file
197
packages/hoppscotch-cli/src/utils/test.ts
Normal file
@@ -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<HoppCLIError, TestRunnerRes> =>
|
||||
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) => <TestRunnerRes>{ 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<TestReport[]> =>
|
||||
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 }) =>
|
||||
<TestReport>{
|
||||
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),
|
||||
},
|
||||
}),
|
||||
<TestMetrics>{
|
||||
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)
|
||||
);
|
||||
16
packages/hoppscotch-cli/tsconfig.json
Normal file
16
packages/hoppscotch-cli/tsconfig.json
Normal file
@@ -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"]
|
||||
}
|
||||
19
packages/hoppscotch-cli/tsup.config.ts
Normal file
19
packages/hoppscotch-cli/tsup.config.ts
Normal file
@@ -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,
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
export default {
|
||||
module.exports = {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
collectCoverage: true,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
})()
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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__"]
|
||||
}
|
||||
|
||||
11
packages/hoppscotch-js-sandbox/tsup.config.ts
Normal file
11
packages/hoppscotch-js-sandbox/tsup.config.ts
Normal file
@@ -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,
|
||||
})
|
||||
345
pnpm-lock.yaml
generated
345
pnpm-lock.yaml
generated
@@ -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==}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user