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:
Deepanshu Dhruw
2022-03-28 13:56:15 +05:30
committed by GitHub
parent cdf61079ae
commit 909d524de5
36 changed files with 2654 additions and 119 deletions

146
packages/hoppscotch-cli/.gitignore vendored Normal file
View 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

View File

@@ -0,0 +1,8 @@
{
"semi": true,
"trailingComma": "es5",
"singleQuote": false,
"printWidth": 80,
"useTabs": false,
"tabWidth": 2
}

View 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.

View 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
```

View 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.

View 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)**

View File

@@ -0,0 +1,3 @@
#!/usr/bin/env node
// * The entry point of the CLI
require("../dist").cli(process.argv);

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

View 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);
})
)();
};

View 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);
}
};

View 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) {}
};

View 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;
}

View 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;
}

View 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": "../"
}
]
}

View 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;

View 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;
};

View 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[];
};

View 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;
};

View 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);
};

View 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;

View 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.")),
};

View 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 })))
: {};

View 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,
};

View 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.",
})
)
)
);

View 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})`,
})
)
);
}

View 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;
};

View 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)
);

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

View 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,
});

View File

@@ -1,4 +1,4 @@
export default {
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
collectCoverage: true,

View File

@@ -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",

View File

@@ -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,
}
)
})()

View File

@@ -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 }

View File

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

View 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
View File

@@ -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==}