diff --git a/.gitignore b/.gitignore index fb7adeee3..b9841a6e3 100644 --- a/.gitignore +++ b/.gitignore @@ -106,4 +106,6 @@ tests/*/screenshots tests/*/videos # Local Netlify folder -.netlify \ No newline at end of file +.netlify +# Andrew's crazy Volar shim generator +shims-volar.d.ts diff --git a/package.json b/package.json index 3811a757d..4e8fadb52 100644 --- a/package.json +++ b/package.json @@ -6,12 +6,11 @@ "private": true, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", + "preinstall": "npx only-allow pnpm", "prepare": "husky install", - "pre-commit": "lint-staged" - }, - "lint-staged": { - "*.{ts,js,vue}": "eslint", - "*.{css,scss,vue}": "stylelint" + "dev": "pnpm -r dev", + "lintfix": "pnpm -r lintfix", + "pre-commit": "pnpm -r lint" }, "workspaces": [ "./packages/*" @@ -19,5 +18,9 @@ "dependencies": { "husky": "^7.0.2", "lint-staged": "^11.1.2" + }, + "devDependencies": { + "@commitlint/cli": "^13.1.0", + "@commitlint/config-conventional": "^13.1.0" } } diff --git a/.eslintrc.js b/packages/hoppscotch-app/.eslintrc.js similarity index 100% rename from .eslintrc.js rename to packages/hoppscotch-app/.eslintrc.js diff --git a/packages/hoppscotch-app/.gitignore b/packages/hoppscotch-app/.gitignore new file mode 100644 index 000000000..b9841a6e3 --- /dev/null +++ b/packages/hoppscotch-app/.gitignore @@ -0,0 +1,111 @@ +# Created by .ignore support plugin (hsz.mobi) + +# Firebase +.firebase + +### Node template +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# 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 + +# 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/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# Nuxt generate +dist + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless + +# IDE / Editor +.idea + +# Service worker +sw.* + +# Mac OSX +.DS_Store + +# Vim swap files +*.swp + +# Build data +.hoppscotch + +# File explorer +.directory + +# Tests screenshots +tests/*/screenshots + +# Tests videos +tests/*/videos + +# Local Netlify folder +.netlify +# Andrew's crazy Volar shim generator +shims-volar.d.ts diff --git a/packages/hoppscotch-app/assets/icons/corner-down-left.svg b/packages/hoppscotch-app/assets/icons/corner-down-left.svg new file mode 100644 index 000000000..7a8b7473a --- /dev/null +++ b/packages/hoppscotch-app/assets/icons/corner-down-left.svg @@ -0,0 +1 @@ + diff --git a/packages/hoppscotch-app/assets/scss/styles.scss b/packages/hoppscotch-app/assets/scss/styles.scss index f54c9535e..3b179a194 100644 --- a/packages/hoppscotch-app/assets/scss/styles.scss +++ b/packages/hoppscotch-app/assets/scss/styles.scss @@ -17,7 +17,7 @@ ::-webkit-scrollbar-thumb { @apply bg-divider bg-clip-content; @apply rounded-full; - @apply border-solid border-4 border-transparent; + @apply border-solid border-transparent border-4; @apply hover:(bg-dividerDark bg-clip-content); } @@ -36,8 +36,9 @@ } input::placeholder, -textarea::placeholder { - @apply text-secondaryDark; +textarea::placeholder, +.CodeMirror-empty { + @apply text-secondary; @apply opacity-25; } @@ -116,8 +117,8 @@ a { &.link { @apply items-center; - @apply px-1 py-0.5; - @apply -mx-1 -my-0.5; + @apply py-0.5 px-1; + @apply -my-0.5 -mx-1; @apply text-accent; @apply rounded; @apply hover:text-accentDark; @@ -198,7 +199,7 @@ hr { .textarea { @apply flex; @apply w-full; - @apply px-4 py-2; + @apply py-2 px-4; @apply bg-transparent; @apply rounded; @apply text-secondaryDark; @@ -293,7 +294,7 @@ input[type="checkbox"] { @apply cursor-pointer; &::before { - @apply border-2 border-divider; + @apply border-divider border-2; @apply rounded; @apply inline-flex; @apply items-center; @@ -347,6 +348,7 @@ input[type="checkbox"] { @apply justify-start; @apply shadow; @apply font-medium; + @apply transition; font-size: var(--body-font-size); line-height: var(--body-line-height); @@ -358,7 +360,6 @@ input[type="checkbox"] { @apply ml-auto; @apply last:ml-4; @apply sm:ml-8; - @apply transition; @apply rounded; @apply text-current; @apply normal-case; @@ -461,6 +462,32 @@ input[type="checkbox"] { @apply w-full; } +.CodeMirror { + @apply !h-auto; + + font-size: var(--body-font-size); + + &:not(.CodeMirror-focused) .CodeMirror-activeline-background { + background: transparent !important; + } + + .CodeMirror-dialog-top { + @apply bg-primaryLight; + @apply border-dividerLight; + @apply px-4; + @apply py-2; + @apply z-5; + } + + .CodeMirror-scroll { + @apply min-h-64; + } + + * { + font-family: "Roboto Mono", monospace; + } +} + @media (max-width: 767px) { main { margin-bottom: env(safe-area-inset-bottom); diff --git a/packages/hoppscotch-app/components/collections/SaveRequest.vue b/packages/hoppscotch-app/components/collections/SaveRequest.vue index f9f39c09c..9b304e73d 100644 --- a/packages/hoppscotch-app/components/collections/SaveRequest.vue +++ b/packages/hoppscotch-app/components/collections/SaveRequest.vue @@ -1,5 +1,9 @@ - diff --git a/packages/hoppscotch-app/components/graphql/RequestOptions.vue b/packages/hoppscotch-app/components/graphql/RequestOptions.vue index 7113b4e4a..cd17475be 100644 --- a/packages/hoppscotch-app/components/graphql/RequestOptions.vue +++ b/packages/hoppscotch-app/components/graphql/RequestOptions.vue @@ -42,9 +42,7 @@ /> @@ -57,20 +55,7 @@ /> - +
@@ -108,19 +93,7 @@ /> - +
@@ -173,27 +146,7 @@ /> -
- -
+
- diff --git a/packages/hoppscotch-app/components/graphql/Response.vue b/packages/hoppscotch-app/components/graphql/Response.vue index feca15fcd..f0027d92a 100644 --- a/packages/hoppscotch-app/components/graphql/Response.vue +++ b/packages/hoppscotch-app/components/graphql/Response.vue @@ -18,6 +18,13 @@ {{ $t("response.title") }}
+
- +
-
+
- {{ $t("shortcut.request.send_request") }} - - - {{ $t("shortcut.general.show_all") }} - - +
- {{ getSpecialKey() }} - G -
-
- {{ getSpecialKey() }} - K -
- +
- diff --git a/packages/hoppscotch-app/components/lenses/renderers/ImageLensRenderer.vue b/packages/hoppscotch-app/components/lenses/renderers/ImageLensRenderer.vue index a2ee88044..230406d0b 100644 --- a/packages/hoppscotch-app/components/lenses/renderers/ImageLensRenderer.vue +++ b/packages/hoppscotch-app/components/lenses/renderers/ImageLensRenderer.vue @@ -27,12 +27,10 @@ />
-
- -
+ diff --git a/packages/hoppscotch-app/components/lenses/renderers/JSONLensRenderer.vue b/packages/hoppscotch-app/components/lenses/renderers/JSONLensRenderer.vue index fc6bc2f12..79652d54f 100644 --- a/packages/hoppscotch-app/components/lenses/renderers/JSONLensRenderer.vue +++ b/packages/hoppscotch-app/components/lenses/renderers/JSONLensRenderer.vue @@ -13,10 +13,18 @@ justify-between " > - +
+
-
- +
+
+
+ + +
+
+ +
+
+ +
+
+
+ +
+
+ +
+
+ chevron_right +
- + + diff --git a/packages/hoppscotch-app/components/lenses/renderers/RawLensRenderer.vue b/packages/hoppscotch-app/components/lenses/renderers/RawLensRenderer.vue index 563f9de1f..9203bddc2 100644 --- a/packages/hoppscotch-app/components/lenses/renderers/RawLensRenderer.vue +++ b/packages/hoppscotch-app/components/lenses/renderers/RawLensRenderer.vue @@ -17,6 +17,14 @@ {{ $t("response.body") }}
+
-
- -
+
- diff --git a/packages/hoppscotch-app/components/lenses/renderers/XMLLensRenderer.vue b/packages/hoppscotch-app/components/lenses/renderers/XMLLensRenderer.vue index eb5340f41..25d173c91 100644 --- a/packages/hoppscotch-app/components/lenses/renderers/XMLLensRenderer.vue +++ b/packages/hoppscotch-app/components/lenses/renderers/XMLLensRenderer.vue @@ -17,6 +17,14 @@ {{ $t("response.body") }}
+
-
- -
+
- diff --git a/packages/hoppscotch-app/components/realtime/Log.vue b/packages/hoppscotch-app/components/realtime/Log.vue index a09fdf7ca..ef1bf74d0 100644 --- a/packages/hoppscotch-app/components/realtime/Log.vue +++ b/packages/hoppscotch-app/components/realtime/Log.vue @@ -17,14 +17,13 @@ {{ title }} -
+
{{ entry.ts }}{{ getSourcePrefix(entry.source) - }}{{ entry.payload }}{{ entry.ts }}{{ source(entry.source) }}{{ entry.payload }} {{ $t("response.waiting_for_connection") }} @@ -32,27 +31,14 @@
- diff --git a/packages/hoppscotch-app/components/realtime/Mqtt.vue b/packages/hoppscotch-app/components/realtime/Mqtt.vue index 9f1bb3b7b..5cc5f0b7f 100644 --- a/packages/hoppscotch-app/components/realtime/Mqtt.vue +++ b/packages/hoppscotch-app/components/realtime/Mqtt.vue @@ -145,7 +145,7 @@ import { defineComponent } from "@nuxtjs/composition-api" import { Splitpanes, Pane } from "splitpanes" import "splitpanes/dist/splitpanes.css" import Paho from "paho-mqtt" -import debounce from "~/helpers/utils/debounce" +import debounce from "lodash/debounce" import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics" import { useSetting } from "~/newstore/settings" import useWindowSize from "~/helpers/utils/useWindowSize" diff --git a/packages/hoppscotch-app/components/realtime/Socketio.vue b/packages/hoppscotch-app/components/realtime/Socketio.vue index e9e461aee..7d933e3bf 100644 --- a/packages/hoppscotch-app/components/realtime/Socketio.vue +++ b/packages/hoppscotch-app/components/realtime/Socketio.vue @@ -165,7 +165,7 @@ import { Splitpanes, Pane } from "splitpanes" import "splitpanes/dist/splitpanes.css" import { io as Client } from "socket.io-client" import wildcard from "socketio-wildcard" -import debounce from "~/helpers/utils/debounce" +import debounce from "lodash/debounce" import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics" import { useSetting } from "~/newstore/settings" import useWindowSize from "~/helpers/utils/useWindowSize" diff --git a/packages/hoppscotch-app/components/realtime/Sse.vue b/packages/hoppscotch-app/components/realtime/Sse.vue index 3d230dc38..ca78d4b1a 100644 --- a/packages/hoppscotch-app/components/realtime/Sse.vue +++ b/packages/hoppscotch-app/components/realtime/Sse.vue @@ -89,8 +89,8 @@ import { defineComponent } from "@nuxtjs/composition-api" import { Splitpanes, Pane } from "splitpanes" import "splitpanes/dist/splitpanes.css" +import debounce from "lodash/debounce" import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics" -import debounce from "~/helpers/utils/debounce" export default defineComponent({ components: { Splitpanes, Pane }, diff --git a/packages/hoppscotch-app/components/realtime/Websocket.vue b/packages/hoppscotch-app/components/realtime/Websocket.vue index 75220c973..2bacf97e5 100644 --- a/packages/hoppscotch-app/components/realtime/Websocket.vue +++ b/packages/hoppscotch-app/components/realtime/Websocket.vue @@ -205,8 +205,8 @@ import { defineComponent } from "@nuxtjs/composition-api" import { Splitpanes, Pane } from "splitpanes" import "splitpanes/dist/splitpanes.css" +import debounce from "lodash/debounce" import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics" -import debounce from "~/helpers/utils/debounce" import useWindowSize from "~/helpers/utils/useWindowSize" import { useSetting } from "~/newstore/settings" diff --git a/packages/hoppscotch-app/components/smart/AceEditor.vue b/packages/hoppscotch-app/components/smart/AceEditor.vue deleted file mode 100644 index 299c0f7f6..000000000 --- a/packages/hoppscotch-app/components/smart/AceEditor.vue +++ /dev/null @@ -1,282 +0,0 @@ - - - - - diff --git a/packages/hoppscotch-app/components/smart/AutoComplete.vue b/packages/hoppscotch-app/components/smart/AutoComplete.vue index 402128a2f..c495247ce 100644 --- a/packages/hoppscotch-app/components/smart/AutoComplete.vue +++ b/packages/hoppscotch-app/components/smart/AutoComplete.vue @@ -150,6 +150,16 @@ export default defineComponent({ handleKeystroke(event) { switch (event.code) { + case "Enter": + event.preventDefault() + if (this.currentSuggestionIndex > -1) + this.forceSuggestion( + this.suggestions.find( + (_item, index) => index === this.currentSuggestionIndex + ) + ) + break + case "ArrowUp": event.preventDefault() this.currentSuggestionIndex = diff --git a/packages/hoppscotch-app/components/smart/EnvInput.vue b/packages/hoppscotch-app/components/smart/EnvInput.vue index 75c8aa94e..0040ca44e 100644 --- a/packages/hoppscotch-app/components/smart/EnvInput.vue +++ b/packages/hoppscotch-app/components/smart/EnvInput.vue @@ -483,7 +483,7 @@ export default defineComponent({ line-height: 1.9; &::before { - @apply text-secondaryDark; + @apply text-secondary; @apply opacity-25; @apply pointer-events-none; @@ -501,7 +501,6 @@ export default defineComponent({ @apply overflow-y-hidden; @apply resize-none; @apply focus:outline-none; - @apply transition; } .env-input::-webkit-scrollbar { diff --git a/packages/hoppscotch-app/components/smart/JsEditor.vue b/packages/hoppscotch-app/components/smart/JsEditor.vue deleted file mode 100644 index f78102525..000000000 --- a/packages/hoppscotch-app/components/smart/JsEditor.vue +++ /dev/null @@ -1,292 +0,0 @@ - - - - - diff --git a/packages/hoppscotch-app/helpers/GQLConnection.ts b/packages/hoppscotch-app/helpers/GQLConnection.ts index 4b2d4614f..2982819fa 100644 --- a/packages/hoppscotch-app/helpers/GQLConnection.ts +++ b/packages/hoppscotch-app/helpers/GQLConnection.ts @@ -195,7 +195,7 @@ export class GQLConnection { method: "post", url, headers: { - ...headers, + ...finalHeaders, "content-type": "application/json", }, data: JSON.stringify({ diff --git a/packages/hoppscotch-app/helpers/__tests__/editorutils.spec.js b/packages/hoppscotch-app/helpers/__tests__/editorutils.spec.js index b0e38e429..61616cfa8 100644 --- a/packages/hoppscotch-app/helpers/__tests__/editorutils.spec.js +++ b/packages/hoppscotch-app/helpers/__tests__/editorutils.spec.js @@ -15,16 +15,16 @@ describe("getEditorLangForMimeType", () => { expect(getEditorLangForMimeType("text/html")).toMatch("html") }) - test("returns 'plain_text' for plain text mime", () => { - expect(getEditorLangForMimeType("text/plain")).toMatch("plain_text") + test("returns 'text/x-yaml' for plain text mime", () => { + expect(getEditorLangForMimeType("text/plain")).toMatch("text/x-yaml") }) - test("returns 'plain_text' for unimplemented mimes", () => { - expect(getEditorLangForMimeType("image/gif")).toMatch("plain_text") + test("returns 'text/x-yaml' for unimplemented mimes", () => { + expect(getEditorLangForMimeType("image/gif")).toMatch("text/x-yaml") }) - test("returns 'plain_text' for null/undefined mimes", () => { - expect(getEditorLangForMimeType(null)).toMatch("plain_text") - expect(getEditorLangForMimeType(undefined)).toMatch("plain_text") + test("returns 'text/x-yaml' for null/undefined mimes", () => { + expect(getEditorLangForMimeType(null)).toMatch("text/x-yaml") + expect(getEditorLangForMimeType(undefined)).toMatch("text/x-yaml") }) }) diff --git a/packages/hoppscotch-app/helpers/codegen/codegen.ts b/packages/hoppscotch-app/helpers/codegen/codegen.ts index 45baf2d6f..88c057e70 100644 --- a/packages/hoppscotch-app/helpers/codegen/codegen.ts +++ b/packages/hoppscotch-app/helpers/codegen/codegen.ts @@ -150,8 +150,6 @@ function getCodegenGeneralRESTInfo( .map((x) => ({ ...x, active: true })) : request.effectiveFinalHeaders.map((x) => ({ ...x, active: true })) - console.log(finalHeaders) - return { name: request.name, uri: request.effectiveFinalURL, diff --git a/packages/hoppscotch-app/helpers/codegen/generators/c-libcurl.js b/packages/hoppscotch-app/helpers/codegen/generators/c-libcurl.js index 1f0cb8d92..1d537760b 100644 --- a/packages/hoppscotch-app/helpers/codegen/generators/c-libcurl.js +++ b/packages/hoppscotch-app/helpers/codegen/generators/c-libcurl.js @@ -24,7 +24,7 @@ export const CLibcurlCodegen = { `curl_easy_setopt(hnd, CURLOPT_CUSTOMREQUEST, "${method}");` ) requestString.push( - `curl_easy_setopt(hnd, CURLOPT_URL, "${url}${pathName}${queryString}");` + `curl_easy_setopt(hnd, CURLOPT_URL, "${url}${pathName}?${queryString}");` ) requestString.push(`struct curl_slist *headers = NULL;`) diff --git a/packages/hoppscotch-app/helpers/codegen/generators/cs-restsharp.js b/packages/hoppscotch-app/helpers/codegen/generators/cs-restsharp.js index 50fe6bbb9..c3452fac1 100644 --- a/packages/hoppscotch-app/helpers/codegen/generators/cs-restsharp.js +++ b/packages/hoppscotch-app/helpers/codegen/generators/cs-restsharp.js @@ -48,7 +48,7 @@ export const CsRestsharpCodegen = { // create client and request requestString.push(`var client = new RestClient("${url}");\n\n`) requestString.push( - `var request = new RestRequest("${pathName}${queryString}", ${requestDataFormat});\n\n` + `var request = new RestRequest("${pathName}?${queryString}", ${requestDataFormat});\n\n` ) // authentification diff --git a/packages/hoppscotch-app/helpers/codegen/generators/curl.js b/packages/hoppscotch-app/helpers/codegen/generators/curl.js index 5f6979478..2b7e18d1d 100644 --- a/packages/hoppscotch-app/helpers/codegen/generators/curl.js +++ b/packages/hoppscotch-app/helpers/codegen/generators/curl.js @@ -19,7 +19,7 @@ export const CurlCodegen = { }) => { const requestString = [] requestString.push(`curl -X ${method}`) - requestString.push(` '${url}${pathName}${queryString}'`) + requestString.push(` '${url}${pathName}?${queryString}'`) if (auth === "Basic Auth") { const basic = `${httpUser}:${httpPassword}` requestString.push( diff --git a/packages/hoppscotch-app/helpers/codegen/generators/go-native.js b/packages/hoppscotch-app/helpers/codegen/generators/go-native.js index 539470ca1..3339b265c 100644 --- a/packages/hoppscotch-app/helpers/codegen/generators/go-native.js +++ b/packages/hoppscotch-app/helpers/codegen/generators/go-native.js @@ -25,7 +25,7 @@ export const GoNativeCodegen = { const requestBody = rawInput ? rawParams : rawRequestBody if (method === "GET") { requestString.push( - `req, err := http.NewRequest("${method}", "${url}${pathName}${queryString}")\n` + `req, err := http.NewRequest("${method}", "${url}${pathName}?${queryString}")\n` ) } if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) { @@ -33,11 +33,11 @@ export const GoNativeCodegen = { if (isJSONContentType(contentType)) { requestString.push(`var reqBody = []byte(\`${requestBody}\`)\n\n`) requestString.push( - `req, err := http.NewRequest("${method}", "${url}${pathName}${queryString}", bytes.NewBuffer(reqBody))\n` + `req, err := http.NewRequest("${method}", "${url}${pathName}?${queryString}", bytes.NewBuffer(reqBody))\n` ) } else if (contentType.includes("x-www-form-urlencoded")) { requestString.push( - `req, err := http.NewRequest("${method}", "${url}${pathName}${queryString}", strings.NewReader("${requestBody}"))\n` + `req, err := http.NewRequest("${method}", "${url}${pathName}?${queryString}", strings.NewReader("${requestBody}"))\n` ) } } diff --git a/packages/hoppscotch-app/helpers/codegen/generators/java-okhttp.js b/packages/hoppscotch-app/helpers/codegen/generators/java-okhttp.js index 6061358f2..2225c4060 100644 --- a/packages/hoppscotch-app/helpers/codegen/generators/java-okhttp.js +++ b/packages/hoppscotch-app/helpers/codegen/generators/java-okhttp.js @@ -39,7 +39,7 @@ export const JavaOkhttpCodegen = { } requestString.push("Request request = new Request.Builder()") - requestString.push(`.url("${url}${pathName}${queryString}")`) + requestString.push(`.url("${url}${pathName}?${queryString}")`) if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) { requestString.push(`.method("${method}", body)`) diff --git a/packages/hoppscotch-app/helpers/codegen/generators/java-unirest.js b/packages/hoppscotch-app/helpers/codegen/generators/java-unirest.js index d4191513a..c257c76e9 100644 --- a/packages/hoppscotch-app/helpers/codegen/generators/java-unirest.js +++ b/packages/hoppscotch-app/helpers/codegen/generators/java-unirest.js @@ -32,7 +32,7 @@ export const JavaUnirestCodegen = { // create client and request const verb = verbs.find((v) => v.verb === method) requestString.push( - `HttpResponse response = Unirest.${verb.unirestMethod}("${url}${pathName}${queryString}")\n` + `HttpResponse response = Unirest.${verb.unirestMethod}("${url}${pathName}?${queryString}")\n` ) if (auth === "Basic Auth") { const basic = `${httpUser}:${httpPassword}` diff --git a/packages/hoppscotch-app/helpers/codegen/generators/javascript-fetch.js b/packages/hoppscotch-app/helpers/codegen/generators/javascript-fetch.js index 5d3401b42..7804e8d28 100644 --- a/packages/hoppscotch-app/helpers/codegen/generators/javascript-fetch.js +++ b/packages/hoppscotch-app/helpers/codegen/generators/javascript-fetch.js @@ -21,7 +21,7 @@ export const JavascriptFetchCodegen = { }) => { const requestString = [] let genHeaders = [] - requestString.push(`fetch("${url}${pathName}${queryString}", {\n`) + requestString.push(`fetch("${url}${pathName}?${queryString}", {\n`) requestString.push(` method: "${method}",\n`) if (auth === "Basic Auth") { const basic = `${httpUser}:${httpPassword}` diff --git a/packages/hoppscotch-app/helpers/codegen/generators/javascript-jquery.js b/packages/hoppscotch-app/helpers/codegen/generators/javascript-jquery.js index 2a2fb8575..b2ca1c0b3 100644 --- a/packages/hoppscotch-app/helpers/codegen/generators/javascript-jquery.js +++ b/packages/hoppscotch-app/helpers/codegen/generators/javascript-jquery.js @@ -21,7 +21,7 @@ export const JavascriptJqueryCodegen = { const genHeaders = [] requestString.push( - `jQuery.ajax({\n url: "${url}${pathName}${queryString}"` + `jQuery.ajax({\n url: "${url}${pathName}?${queryString}"` ) requestString.push(`,\n method: "${method.toUpperCase()}"`) const requestBody = rawInput ? rawParams : rawRequestBody diff --git a/packages/hoppscotch-app/helpers/codegen/generators/javascript-xhr.js b/packages/hoppscotch-app/helpers/codegen/generators/javascript-xhr.js index 650e9b24c..3c752d05a 100644 --- a/packages/hoppscotch-app/helpers/codegen/generators/javascript-xhr.js +++ b/packages/hoppscotch-app/helpers/codegen/generators/javascript-xhr.js @@ -25,7 +25,7 @@ export const JavascriptXhrCodegen = { const user = auth === "Basic Auth" ? `'${httpUser}'` : null const password = auth === "Basic Auth" ? `'${httpPassword}'` : null requestString.push( - `xhr.open('${method}', '${url}${pathName}${queryString}', true, ${user}, ${password})` + `xhr.open('${method}', '${url}${pathName}?${queryString}', true, ${user}, ${password})` ) if (auth === "Bearer Token" || auth === "OAuth 2.0") { requestString.push( diff --git a/packages/hoppscotch-app/helpers/codegen/generators/nodejs-axios.js b/packages/hoppscotch-app/helpers/codegen/generators/nodejs-axios.js index 91c4de7da..349e2d259 100644 --- a/packages/hoppscotch-app/helpers/codegen/generators/nodejs-axios.js +++ b/packages/hoppscotch-app/helpers/codegen/generators/nodejs-axios.js @@ -22,7 +22,7 @@ export const NodejsAxiosCodegen = { const requestBody = rawInput ? rawParams : rawRequestBody requestString.push( - `axios.${method.toLowerCase()}('${url}${pathName}${queryString}'` + `axios.${method.toLowerCase()}('${url}${pathName}?${queryString}'` ) if (requestBody.length !== 0) { requestString.push(", ") diff --git a/packages/hoppscotch-app/helpers/codegen/generators/nodejs-native.js b/packages/hoppscotch-app/helpers/codegen/generators/nodejs-native.js index fba2debdc..cc551ccac 100644 --- a/packages/hoppscotch-app/helpers/codegen/generators/nodejs-native.js +++ b/packages/hoppscotch-app/helpers/codegen/generators/nodejs-native.js @@ -24,7 +24,7 @@ export const NodejsNativeCodegen = { requestString.push(`const http = require('http');\n\n`) - requestString.push(`const url = '${url}${pathName}${queryString}';\n`) + requestString.push(`const url = '${url}${pathName}?${queryString}';\n`) requestString.push(`const options = {\n`) requestString.push(` method: '${method}',\n`) diff --git a/packages/hoppscotch-app/helpers/codegen/generators/nodejs-request.js b/packages/hoppscotch-app/helpers/codegen/generators/nodejs-request.js index 1c3361921..cbdf218a0 100644 --- a/packages/hoppscotch-app/helpers/codegen/generators/nodejs-request.js +++ b/packages/hoppscotch-app/helpers/codegen/generators/nodejs-request.js @@ -25,7 +25,7 @@ export const NodejsRequestCodegen = { requestString.push(`const request = require('request');\n`) requestString.push(`const options = {\n`) requestString.push(` method: '${method.toLowerCase()}',\n`) - requestString.push(` url: '${url}${pathName}${queryString}'`) + requestString.push(` url: '${url}${pathName}?${queryString}'`) if (auth === "Basic Auth") { const basic = `${httpUser}:${httpPassword}` diff --git a/packages/hoppscotch-app/helpers/codegen/generators/nodejs-unirest.js b/packages/hoppscotch-app/helpers/codegen/generators/nodejs-unirest.js index 45b1e1eb5..dabab4982 100644 --- a/packages/hoppscotch-app/helpers/codegen/generators/nodejs-unirest.js +++ b/packages/hoppscotch-app/helpers/codegen/generators/nodejs-unirest.js @@ -25,7 +25,7 @@ export const NodejsUnirestCodegen = { requestString.push(`const unirest = require('unirest');\n`) requestString.push(`const req = unirest(\n`) requestString.push( - `'${method.toLowerCase()}', '${url}${pathName}${queryString}')\n` + `'${method.toLowerCase()}', '${url}${pathName}?${queryString}')\n` ) if (auth === "Basic Auth") { diff --git a/packages/hoppscotch-app/helpers/codegen/generators/php-curl.js b/packages/hoppscotch-app/helpers/codegen/generators/php-curl.js index d5d4c5661..3b58d6cfd 100644 --- a/packages/hoppscotch-app/helpers/codegen/generators/php-curl.js +++ b/packages/hoppscotch-app/helpers/codegen/generators/php-curl.js @@ -25,7 +25,7 @@ export const PhpCurlCodegen = { requestString.push(` "${url}${pathName}${queryString}",\n`) + requestString.push(` CURLOPT_URL => "${url}${pathName}?${queryString}",\n`) requestString.push(` CURLOPT_RETURNTRANSFER => true,\n`) requestString.push(` CURLOPT_ENCODING => "",\n`) requestString.push(` CURLOPT_MAXREDIRS => 10,\n`) diff --git a/packages/hoppscotch-app/helpers/codegen/generators/powershell-restmethod.js b/packages/hoppscotch-app/helpers/codegen/generators/powershell-restmethod.js index 8693cf450..40880627c 100644 --- a/packages/hoppscotch-app/helpers/codegen/generators/powershell-restmethod.js +++ b/packages/hoppscotch-app/helpers/codegen/generators/powershell-restmethod.js @@ -26,7 +26,7 @@ export const PowershellRestmethodCodegen = { let variables = "" requestString.push( - `Invoke-RestMethod -Method '${formattedMethod}' -Uri '${url}${pathName}${queryString}'` + `Invoke-RestMethod -Method '${formattedMethod}' -Uri '${url}${pathName}?${queryString}'` ) const requestBody = rawInput ? rawParams : rawRequestBody diff --git a/packages/hoppscotch-app/helpers/codegen/generators/python-http-client.js b/packages/hoppscotch-app/helpers/codegen/generators/python-http-client.js index 7c7795bfe..e061ac462 100644 --- a/packages/hoppscotch-app/helpers/codegen/generators/python-http-client.js +++ b/packages/hoppscotch-app/helpers/codegen/generators/python-http-client.js @@ -91,7 +91,7 @@ export const PythonHttpClientCodegen = { } } requestString.push( - `conn.request("${method}", "${pathName}${queryString}", payload, headers)\n` + `conn.request("${method}", "${pathName}?${queryString}", payload, headers)\n` ) requestString.push(`res = conn.getresponse()\n`) requestString.push(`data = res.read()\n`) diff --git a/packages/hoppscotch-app/helpers/codegen/generators/python-requests.js b/packages/hoppscotch-app/helpers/codegen/generators/python-requests.js index d5471cbbb..c02062774 100644 --- a/packages/hoppscotch-app/helpers/codegen/generators/python-requests.js +++ b/packages/hoppscotch-app/helpers/codegen/generators/python-requests.js @@ -31,7 +31,7 @@ export const PythonRequestsCodegen = { const genHeaders = [] requestString.push(`import requests\n\n`) - requestString.push(`url = '${url}${pathName}${queryString}'\n`) + requestString.push(`url = '${url}${pathName}?${queryString}'\n`) // auth headers if (auth === "Basic Auth") { @@ -58,7 +58,7 @@ export const PythonRequestsCodegen = { requestString.push(...printHeaders(genHeaders)) requestString.push(`response = requests.request(\n`) requestString.push(` '${method}',\n`) - requestString.push(` '${url}${pathName}${queryString}',\n`) + requestString.push(` '${url}${pathName}?${queryString}',\n`) } if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) { genHeaders.push(`'Content-Type': '${contentType}'`) @@ -83,7 +83,7 @@ export const PythonRequestsCodegen = { } requestString.push(`response = requests.request(\n`) requestString.push(` '${method}',\n`) - requestString.push(` '${url}${pathName}${queryString}',\n`) + requestString.push(` '${url}${pathName}?${queryString}',\n`) requestString.push(` data=data,\n`) } diff --git a/packages/hoppscotch-app/helpers/codegen/generators/ruby-net-http.js b/packages/hoppscotch-app/helpers/codegen/generators/ruby-net-http.js index e5486bd05..4487891f8 100644 --- a/packages/hoppscotch-app/helpers/codegen/generators/ruby-net-http.js +++ b/packages/hoppscotch-app/helpers/codegen/generators/ruby-net-http.js @@ -35,7 +35,7 @@ export const RubyNetHttpCodeGen = { // create URI and request const verb = verbs.find((v) => v.verb === method) - requestString.push(`uri = URI.parse('${url}${pathName}${queryString}')\n`) + requestString.push(`uri = URI.parse('${url}${pathName}?${queryString}')\n`) requestString.push(`request = Net::HTTP::${verb.rbMethod}.new(uri)`) // content type diff --git a/packages/hoppscotch-app/helpers/codegen/generators/salesforce-apex.js b/packages/hoppscotch-app/helpers/codegen/generators/salesforce-apex.js index 9112c766b..250f70313 100644 --- a/packages/hoppscotch-app/helpers/codegen/generators/salesforce-apex.js +++ b/packages/hoppscotch-app/helpers/codegen/generators/salesforce-apex.js @@ -30,7 +30,7 @@ export const SalesforceApexCodegen = { requestString.push(`HttpRequest request = new HttpRequest();\n`) requestString.push(`request.setMethod('${method}');\n`) requestString.push( - `request.setEndpoint('${url}${pathName}${queryString}');\n\n` + `request.setEndpoint('${url}${pathName}?${queryString}');\n\n` ) // authentification diff --git a/packages/hoppscotch-app/helpers/codegen/generators/shell-httpie.js b/packages/hoppscotch-app/helpers/codegen/generators/shell-httpie.js index dc0284935..0623847b5 100644 --- a/packages/hoppscotch-app/helpers/codegen/generators/shell-httpie.js +++ b/packages/hoppscotch-app/helpers/codegen/generators/shell-httpie.js @@ -37,7 +37,7 @@ export const ShellHttpieCodegen = { } // URL - let escapedUrl = `${url}${pathName}${queryString}` + let escapedUrl = `${url}${pathName}?${queryString}` escapedUrl = escapedUrl.replace(/'/g, "\\'") requestString.push(` ${method} $'${escapedUrl}'`) diff --git a/packages/hoppscotch-app/helpers/codegen/generators/shell-wget.js b/packages/hoppscotch-app/helpers/codegen/generators/shell-wget.js index 8620a6170..537d6fd97 100644 --- a/packages/hoppscotch-app/helpers/codegen/generators/shell-wget.js +++ b/packages/hoppscotch-app/helpers/codegen/generators/shell-wget.js @@ -19,7 +19,7 @@ export const ShellWgetCodegen = { }) => { const requestString = [] requestString.push(`wget -O - --method=${method}`) - requestString.push(` '${url}${pathName}${queryString}'`) + requestString.push(` '${url}${pathName}?${queryString}'`) if (auth === "Basic Auth") { const basic = `${httpUser}:${httpPassword}` requestString.push( diff --git a/packages/hoppscotch-app/helpers/editor/codemirror.ts b/packages/hoppscotch-app/helpers/editor/codemirror.ts new file mode 100644 index 000000000..7319c7413 --- /dev/null +++ b/packages/hoppscotch-app/helpers/editor/codemirror.ts @@ -0,0 +1,215 @@ +import CodeMirror from "codemirror" + +import "codemirror-theme-github/theme/github.css" +import "codemirror/theme/base16-dark.css" +import "codemirror/theme/tomorrow-night-bright.css" + +import "codemirror/lib/codemirror.css" +import "codemirror/addon/lint/lint.css" +import "codemirror/addon/dialog/dialog.css" +import "codemirror/addon/hint/show-hint.css" + +import "codemirror/addon/fold/foldgutter.css" +import "codemirror/addon/fold/foldgutter" +import "codemirror/addon/fold/brace-fold" +import "codemirror/addon/fold/comment-fold" +import "codemirror/addon/fold/indent-fold" +import "codemirror/addon/display/autorefresh" +import "codemirror/addon/lint/lint" +import "codemirror/addon/hint/show-hint" +import "codemirror/addon/display/placeholder" +import "codemirror/addon/edit/closebrackets" +import "codemirror/addon/search/search" +import "codemirror/addon/search/searchcursor" +import "codemirror/addon/search/jump-to-line" +import "codemirror/addon/dialog/dialog" +import "codemirror/addon/selection/active-line" + +import { watch, onMounted, ref, Ref, useContext } from "@nuxtjs/composition-api" +import { LinterDefinition } from "./linting/linter" +import { Completer } from "./completion" + +type CodeMirrorOptions = { + extendedEditorConfig: Omit + linter: LinterDefinition | null + completer: Completer | null +} + +const DEFAULT_EDITOR_CONFIG: CodeMirror.EditorConfiguration = { + autoRefresh: true, + lineNumbers: true, + foldGutter: true, + autoCloseBrackets: true, + gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"], + extraKeys: { + "Ctrl-Space": "autocomplete", + }, + viewportMargin: Infinity, + styleActiveLine: true, +} + +/** + * A Vue composable to mount and use Codemirror + * + * NOTE: Make sure to import all the necessary Codemirror modules, + * as this function doesn't import any other than the core + * @param el Reference to the dom node to attach to + * @param value Reference to value to read/write to + * @param options CodeMirror options to pass + */ +export function useCodemirror( + el: Ref, + value: Ref, + options: CodeMirrorOptions +): { cm: Ref; cursor: Ref } { + const { $colorMode } = useContext() as any + + const cm = ref(null) + const cursor = ref({ line: 0, ch: 0 }) + + const updateEditorConfig = () => { + Object.keys(options.extendedEditorConfig).forEach((key) => { + // Only update options which need updating + if ( + cm.value && + cm.value?.getOption(key as any) !== + (options.extendedEditorConfig as any)[key] + ) { + cm.value?.setOption( + key as any, + (options.extendedEditorConfig as any)[key] + ) + } + }) + } + + const updateLinterConfig = () => { + if (options.linter) { + cm.value?.setOption("lint", options.linter) + } + } + + const updateCompleterConfig = () => { + if (options.completer) { + cm.value?.setOption("hintOptions", { + completeSingle: false, + hint: async (editor: CodeMirror.Editor) => { + const pos = editor.getCursor() + const text = editor.getValue() + + const token = editor.getTokenAt(pos) + // It's not a word token, so, just increment to skip to next + if (token.string.toUpperCase() === token.string.toLowerCase()) + token.start += 1 + + const result = await options.completer!(text, pos) + + if (!result) return null + + return { + from: { line: pos.line, ch: token.start }, + to: { line: pos.line, ch: token.end }, + list: result.completions + .sort((a, b) => a.score - b.score) + .map((x) => x.text), + } + }, + }) + } + } + + const initialize = () => { + if (!el.value) return + + cm.value = CodeMirror(el.value!, DEFAULT_EDITOR_CONFIG) + + cm.value.setValue(value.value) + + setTheme() + updateEditorConfig() + updateLinterConfig() + updateCompleterConfig() + + cm.value.on("change", (instance) => { + // External update propagation (via watchers) should be ignored + if (instance.getValue() !== value.value) { + value.value = instance.getValue() + } + }) + + cm.value.on("cursorActivity", (instance) => { + cursor.value = instance.getCursor() + }) + } + + // Boot-up CodeMirror, set the value and listeners + onMounted(() => { + initialize() + }) + + // Reinitialize if the target ref updates + watch(el, () => { + if (cm.value) { + const parent = cm.value.getWrapperElement() + parent.remove() + cm.value = null + } + initialize() + }) + + const setTheme = () => { + if (cm.value) { + cm.value?.setOption("theme", getThemeName($colorMode.value)) + } + } + + const getThemeName = (mode: string) => { + switch (mode) { + case "system": + return "default" + case "light": + return "github" + case "dark": + return "base16-dark" + case "black": + return "tomorrow-night-bright" + default: + return "default" + } + } + + // If the editor properties are reactive, watch for updates + watch(() => options.extendedEditorConfig, updateEditorConfig, { + immediate: true, + deep: true, + }) + watch(() => options.linter, updateLinterConfig, { immediate: true }) + watch(() => options.completer, updateCompleterConfig, { immediate: true }) + + // Watch value updates + watch(value, (newVal) => { + // Check if we are mounted + if (cm.value) { + // Don't do anything on internal updates + if (cm.value.getValue() !== newVal) { + cm.value.setValue(newVal) + } + } + }) + + // Push cursor updates + watch(cursor, (value) => { + if (value !== cm.value?.getCursor()) { + cm.value?.focus() + cm.value?.setCursor(value) + } + }) + + // Watch color mode updates and update theme + watch(() => $colorMode.value, setTheme) + + return { + cm, + cursor, + } +} diff --git a/packages/hoppscotch-app/helpers/editor/completion/gqlQuery.ts b/packages/hoppscotch-app/helpers/editor/completion/gqlQuery.ts new file mode 100644 index 000000000..672ef134a --- /dev/null +++ b/packages/hoppscotch-app/helpers/editor/completion/gqlQuery.ts @@ -0,0 +1,27 @@ +import { Ref } from "@nuxtjs/composition-api" +import { GraphQLSchema } from "graphql" +import { getAutocompleteSuggestions } from "graphql-language-service-interface" +import { Completer, CompleterResult, CompletionEntry } from "." + +const completer: (schemaRef: Ref) => Completer = + (schemaRef: Ref) => (text, completePos) => { + if (!schemaRef.value) return Promise.resolve(null) + + const completions = getAutocompleteSuggestions(schemaRef.value, text, { + line: completePos.line, + character: completePos.ch, + } as any) + + return Promise.resolve({ + completions: completions.map( + (x, i) => + { + text: x.label!, + meta: x.detail!, + score: completions.length - i, + } + ), + }) + } + +export default completer diff --git a/packages/hoppscotch-app/helpers/editor/completion/index.ts b/packages/hoppscotch-app/helpers/editor/completion/index.ts new file mode 100644 index 000000000..bc78e6193 --- /dev/null +++ b/packages/hoppscotch-app/helpers/editor/completion/index.ts @@ -0,0 +1,23 @@ +export type CompletionEntry = { + text: string + meta: string + score: number +} + +export type CompleterResult = { + /** + * List of completions to display + */ + completions: CompletionEntry[] +} + +export type Completer = ( + /** + * The contents of the editor + */ + text: string, + /** + * Position where the completer is fired + */ + completePos: { line: number; ch: number } +) => Promise diff --git a/packages/hoppscotch-app/helpers/editor/completion/preRequest.ts b/packages/hoppscotch-app/helpers/editor/completion/preRequest.ts new file mode 100644 index 000000000..cc723155d --- /dev/null +++ b/packages/hoppscotch-app/helpers/editor/completion/preRequest.ts @@ -0,0 +1,24 @@ +import { Completer, CompletionEntry } from "." +import { getPreRequestScriptCompletions } from "~/helpers/tern" + +const completer: Completer = async (text, completePos) => { + const results = await getPreRequestScriptCompletions( + text, + completePos.line, + completePos.ch + ) + + const completions = results.completions.map((completion: any, i: number) => { + return { + text: completion.name, + meta: completion.isKeyword ? "keyword" : completion.type, + score: results.completions.length - i, + } + }) + + return { + completions, + } +} + +export default completer diff --git a/packages/hoppscotch-app/helpers/editor/completion/testScript.ts b/packages/hoppscotch-app/helpers/editor/completion/testScript.ts new file mode 100644 index 000000000..88286ac46 --- /dev/null +++ b/packages/hoppscotch-app/helpers/editor/completion/testScript.ts @@ -0,0 +1,24 @@ +import { Completer, CompletionEntry } from "." +import { getTestScriptCompletions } from "~/helpers/tern" + +export const completer: Completer = async (text, completePos) => { + const results = await getTestScriptCompletions( + text, + completePos.line, + completePos.ch + ) + + const completions = results.completions.map((completion: any, i: number) => { + return { + text: completion.name, + meta: completion.isKeyword ? "keyword" : completion.type, + score: results.completions.length - i, + } + }) + + return { + completions, + } +} + +export default completer diff --git a/packages/hoppscotch-app/helpers/editor/linting/gqlQuery.ts b/packages/hoppscotch-app/helpers/editor/linting/gqlQuery.ts new file mode 100644 index 000000000..648cfa732 --- /dev/null +++ b/packages/hoppscotch-app/helpers/editor/linting/gqlQuery.ts @@ -0,0 +1,58 @@ +import { Ref } from "@nuxtjs/composition-api" +import { + GraphQLError, + GraphQLSchema, + parse as gqlParse, + validate as gqlValidate, +} from "graphql" +import { LinterDefinition, LinterResult } from "./linter" + +/** + * Creates a Linter function that can lint a GQL query against a given + * schema + */ +export const createGQLQueryLinter: ( + schema: Ref +) => LinterDefinition = (schema: Ref) => (text) => { + if (text === "") return Promise.resolve([]) + if (!schema.value) return Promise.resolve([]) + + try { + const doc = gqlParse(text) + + const results = gqlValidate(schema.value, doc).map( + ({ locations, message }) => + { + from: { + line: locations![0].line - 1, + ch: locations![0].column - 1, + }, + to: { + line: locations![0].line - 1, + ch: locations![0].column, + }, + message, + severity: "error", + } + ) + + return Promise.resolve(results) + } catch (e) { + const err = e as GraphQLError + + return Promise.resolve([ + { + from: { + line: err.locations![0].line - 1, + ch: err.locations![0].column - 1, + }, + to: { + line: err.locations![0].line - 1, + ch: err.locations![0].column, + }, + message: err.message, + severity: "error", + }, + ]) + } +} diff --git a/packages/hoppscotch-app/helpers/editor/linting/json.ts b/packages/hoppscotch-app/helpers/editor/linting/json.ts new file mode 100644 index 000000000..46a690197 --- /dev/null +++ b/packages/hoppscotch-app/helpers/editor/linting/json.ts @@ -0,0 +1,21 @@ +import { convertIndexToLineCh } from "../utils" +import { LinterDefinition, LinterResult } from "./linter" +import jsonParse from "~/helpers/jsonParse" + +const linter: LinterDefinition = (text) => { + try { + jsonParse(text) + return Promise.resolve([]) + } catch (e: any) { + return Promise.resolve([ + { + from: convertIndexToLineCh(text, e.start), + to: convertIndexToLineCh(text, e.end), + message: e.message, + severity: "error", + }, + ]) + } +} + +export default linter diff --git a/packages/hoppscotch-app/helpers/editor/linting/linter.ts b/packages/hoppscotch-app/helpers/editor/linting/linter.ts new file mode 100644 index 000000000..704270cbe --- /dev/null +++ b/packages/hoppscotch-app/helpers/editor/linting/linter.ts @@ -0,0 +1,7 @@ +export type LinterResult = { + message: string + severity: "warning" | "error" + from: { line: number; ch: number } + to: { line: number; ch: number } +} +export type LinterDefinition = (text: string) => Promise diff --git a/packages/hoppscotch-app/helpers/editor/linting/preRequest.ts b/packages/hoppscotch-app/helpers/editor/linting/preRequest.ts new file mode 100644 index 000000000..db42d9864 --- /dev/null +++ b/packages/hoppscotch-app/helpers/editor/linting/preRequest.ts @@ -0,0 +1,69 @@ +import * as esprima from "esprima" +import { LinterDefinition, LinterResult } from "./linter" +import { performPreRequestLinting } from "~/helpers/tern" + +const linter: LinterDefinition = async (text) => { + let results: LinterResult[] = [] + + // Semantic linting + const semanticLints = await performPreRequestLinting(text) + + results = results.concat( + semanticLints.map((lint: any) => ({ + from: lint.from, + to: lint.to, + severity: "error", + message: `[semantic] ${lint.message}`, + })) + ) + + // Syntax linting + try { + const res: any = esprima.parseScript(text, { tolerant: true }) + if (res.errors && res.errors.length > 0) { + results = results.concat( + res.errors.map((err: any) => { + const fromPos: { line: number; ch: number } = { + line: err.lineNumber - 1, + ch: err.column - 1, + } + + const toPos: { line: number; ch: number } = { + line: err.lineNumber - 1, + ch: err.column, + } + + return { + from: fromPos, + to: toPos, + message: `[syntax] ${err.description}`, + severity: "error", + } + }) + ) + } + } catch (e) { + const fromPos: { line: number; ch: number } = { + line: e.lineNumber - 1, + ch: e.column - 1, + } + + const toPos: { line: number; ch: number } = { + line: e.lineNumber - 1, + ch: e.column, + } + + results = results.concat([ + { + from: fromPos, + to: toPos, + message: `[syntax] ${e.description}`, + severity: "error", + }, + ]) + } + + return results +} + +export default linter diff --git a/packages/hoppscotch-app/helpers/editor/linting/testScript.ts b/packages/hoppscotch-app/helpers/editor/linting/testScript.ts new file mode 100644 index 000000000..902d1778c --- /dev/null +++ b/packages/hoppscotch-app/helpers/editor/linting/testScript.ts @@ -0,0 +1,69 @@ +import * as esprima from "esprima" +import { LinterDefinition, LinterResult } from "./linter" +import { performTestLinting } from "~/helpers/tern" + +const linter: LinterDefinition = async (text) => { + let results: LinterResult[] = [] + + // Semantic linting + const semanticLints = await performTestLinting(text) + + results = results.concat( + semanticLints.map((lint: any) => ({ + from: lint.from, + to: lint.to, + severity: "error", + message: `[semantic] ${lint.message}`, + })) + ) + + // Syntax linting + try { + const res: any = esprima.parseScript(text, { tolerant: true }) + if (res.errors && res.errors.length > 0) { + results = results.concat( + res.errors.map((err: any) => { + const fromPos: { line: number; ch: number } = { + line: err.lineNumber - 1, + ch: err.column - 1, + } + + const toPos: { line: number; ch: number } = { + line: err.lineNumber - 1, + ch: err.column, + } + + return { + from: fromPos, + to: toPos, + message: `[syntax] ${err.description}`, + severity: "error", + } + }) + ) + } + } catch (e) { + const fromPos: { line: number; ch: number } = { + line: e.lineNumber - 1, + ch: e.column - 1, + } + + const toPos: { line: number; ch: number } = { + line: e.lineNumber - 1, + ch: e.column, + } + + results = results.concat([ + { + from: fromPos, + to: toPos, + message: `[syntax] ${e.description}`, + severity: "error", + }, + ]) + } + + return results +} + +export default linter diff --git a/packages/hoppscotch-app/helpers/editor/modes/graphql.ts b/packages/hoppscotch-app/helpers/editor/modes/graphql.ts new file mode 100644 index 000000000..2c4949882 --- /dev/null +++ b/packages/hoppscotch-app/helpers/editor/modes/graphql.ts @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +import CodeMirror from "codemirror" +import { + LexRules, + ParseRules, + isIgnored, + onlineParser, + State, +} from "graphql-language-service-parser" + +/** + * The GraphQL mode is defined as a tokenizer along with a list of rules, each + * of which is either a function or an array. + * + * * Function: Provided a token and the stream, returns an expected next step. + * * Array: A list of steps to take in order. + * + * A step is either another rule, or a terminal description of a token. If it + * is a rule, that rule is pushed onto the stack and the parsing continues from + * that point. + * + * If it is a terminal description, the token is checked against it using a + * `match` function. If the match is successful, the token is colored and the + * rule is stepped forward. If the match is unsuccessful, the remainder of the + * rule is skipped and the previous rule is advanced. + * + * This parsing algorithm allows for incremental online parsing within various + * levels of the syntax tree and results in a structured `state` linked-list + * which contains the relevant information to produce valuable typeaheads. + */ +CodeMirror.defineMode("graphql", (config) => { + const parser = onlineParser({ + eatWhitespace: (stream) => stream.eatWhile(isIgnored), + lexRules: LexRules, + parseRules: ParseRules, + editorConfig: { tabSize: 2 }, + }) + + return { + config, + startState: parser.startState, + token: parser.token as unknown as CodeMirror.Mode["token"], // TODO: Check if the types are indeed compatible + indent, + electricInput: /^\s*[})\]]/, + fold: "brace", + lineComment: "#", + closeBrackets: { + pairs: '()[]{}""', + explode: "()[]{}", + }, + } +}) + +// Seems the electricInput type in @types/codemirror is wrong (i.e it is written as electricinput instead of electricInput) +function indent( + this: CodeMirror.Mode & { + electricInput?: RegExp + config?: CodeMirror.EditorConfiguration + }, + state: State, + textAfter: string +) { + const levels = state.levels + // If there is no stack of levels, use the current level. + // Otherwise, use the top level, pre-emptively dedenting for close braces. + const level = + !levels || levels.length === 0 + ? state.indentLevel + : levels[levels.length - 1] - + (this.electricInput?.test(textAfter) ? 1 : 0) + return (level || 0) * (this.config?.indentUnit || 0) +} diff --git a/packages/hoppscotch-app/helpers/editor/utils.ts b/packages/hoppscotch-app/helpers/editor/utils.ts new file mode 100644 index 000000000..c7dcb1c12 --- /dev/null +++ b/packages/hoppscotch-app/helpers/editor/utils.ts @@ -0,0 +1,38 @@ +export function convertIndexToLineCh( + text: string, + i: number +): { line: number; ch: number } { + const lines = text.split("\n") + + let line = 0 + let counter = 0 + + while (line < lines.length) { + if (i > lines[line].length + counter) { + counter += lines[line].length + 1 + line++ + } else { + return { + line: line + 1, + ch: i - counter + 1, + } + } + } + + throw new Error("Invalid input") +} + +export function convertLineChToIndex( + text: string, + lineCh: { line: number; ch: number } +): number { + const textSplit = text.split("\n") + + if (textSplit.length < lineCh.line) throw new Error("Invalid position") + + const tillLineIndex = textSplit + .slice(0, lineCh.line) + .reduce((acc, line) => acc + line.length + 1, 0) + + return tillLineIndex + lineCh.ch +} diff --git a/packages/hoppscotch-app/helpers/editorutils.js b/packages/hoppscotch-app/helpers/editorutils.js index 0955bbf37..9dd46a4bf 100644 --- a/packages/hoppscotch-app/helpers/editorutils.js +++ b/packages/hoppscotch-app/helpers/editorutils.js @@ -1,12 +1,12 @@ const mimeToMode = { - "text/plain": "plain_text", - "text/html": "html", - "application/xml": "xml", - "application/hal+json": "json", - "application/vnd.api+json": "json", - "application/json": "json", + "text/plain": "text/x-yaml", + "text/html": "htmlmixed", + "application/xml": "application/xml", + "application/hal+json": "application/ld+json", + "application/vnd.api+json": "application/ld+json", + "application/json": "application/ld+json", } export function getEditorLangForMimeType(mimeType) { - return mimeToMode[mimeType] || "plain_text" + return mimeToMode[mimeType] || "text/x-yaml" } diff --git a/packages/hoppscotch-app/helpers/jsonParse.js b/packages/hoppscotch-app/helpers/jsonParse.ts similarity index 71% rename from packages/hoppscotch-app/helpers/jsonParse.js rename to packages/hoppscotch-app/helpers/jsonParse.ts index d196e3cc1..6262ba278 100644 --- a/packages/hoppscotch-app/helpers/jsonParse.js +++ b/packages/hoppscotch-app/helpers/jsonParse.ts @@ -19,7 +19,75 @@ * - end: int - the end exclusive offset of the syntax error * */ -export default function jsonParse(str) { +type JSONEOFValue = { + kind: "EOF" + start: number + end: number +} + +type JSONNullValue = { + kind: "Null" + start: number + end: number +} + +type JSONNumberValue = { + kind: "Number" + start: number + end: number + value: number +} + +type JSONStringValue = { + kind: "String" + start: number + end: number + value: string +} + +type JSONBooleanValue = { + kind: "Boolean" + start: number + end: number + value: boolean +} + +type JSONPrimitiveValue = + | JSONNullValue + | JSONEOFValue + | JSONStringValue + | JSONNumberValue + | JSONBooleanValue + +export type JSONObjectValue = { + kind: "Object" + start: number + end: number + // eslint-disable-next-line no-use-before-define + members: JSONObjectMember[] +} + +export type JSONArrayValue = { + kind: "Array" + start: number + end: number + // eslint-disable-next-line no-use-before-define + values: JSONValue[] +} + +export type JSONValue = JSONObjectValue | JSONArrayValue | JSONPrimitiveValue + +export type JSONObjectMember = { + kind: "Member" + start: number + end: number + key: JSONStringValue + value: JSONValue +} + +export default function jsonParse( + str: string +): JSONObjectValue | JSONArrayValue { string = str strLen = str.length start = end = lastEnd = -1 @@ -37,15 +105,15 @@ export default function jsonParse(str) { } } -let string -let strLen -let start -let end -let lastEnd -let code -let kind +let string: string +let strLen: number +let start: number +let end: number +let lastEnd: number +let code: number +let kind: string -function parseObj() { +function parseObj(): JSONObjectValue { const nodeStart = start const members = [] expect("{") @@ -63,9 +131,9 @@ function parseObj() { } } -function parseMember() { +function parseMember(): JSONObjectMember { const nodeStart = start - const key = kind === "String" ? curToken() : null + const key = kind === "String" ? (curToken() as JSONStringValue) : null expect("String") expect(":") const value = parseVal() @@ -73,14 +141,14 @@ function parseMember() { kind: "Member", start: nodeStart, end: lastEnd, - key, + key: key!, value, } } -function parseArr() { +function parseArr(): JSONArrayValue { const nodeStart = start - const values = [] + const values: JSONValue[] = [] expect("[") if (!skip("]")) { do { @@ -96,7 +164,7 @@ function parseArr() { } } -function parseVal() { +function parseVal(): JSONValue { switch (kind) { case "[": return parseArr() @@ -111,14 +179,19 @@ function parseVal() { lex() return token } - return expect("Value") + return expect("Value") as never } -function curToken() { - return { kind, start, end, value: JSON.parse(string.slice(start, end)) } +function curToken(): JSONPrimitiveValue { + return { + kind: kind as any, + start, + end, + value: JSON.parse(string.slice(start, end)), + } } -function expect(str) { +function expect(str: string) { if (kind === str) { lex() return @@ -137,11 +210,17 @@ function expect(str) { throw syntaxError(`Expected ${str} but found ${found}.`) } -function syntaxError(message) { +type SyntaxError = { + message: string + start: number + end: number +} + +function syntaxError(message: string): SyntaxError { return { message, start, end } } -function skip(k) { +function skip(k: string) { if (kind === k) { lex() return true @@ -227,7 +306,7 @@ function lex() { function readString() { ch() while (code !== 34 && code > 31) { - if (code === 92) { + if (code === (92 as any)) { // \ ch() switch (code) { @@ -299,7 +378,7 @@ function readNumber() { if (code === 69 || code === 101) { // E e ch() - if (code === 43 || code === 45) { + if (code === (43 as any) || code === (45 as any)) { // + - ch() } diff --git a/packages/hoppscotch-app/helpers/lenses/htmlLens.js b/packages/hoppscotch-app/helpers/lenses/htmlLens.ts similarity index 62% rename from packages/hoppscotch-app/helpers/lenses/htmlLens.js rename to packages/hoppscotch-app/helpers/lenses/htmlLens.ts index 7a18570eb..8c4f9da65 100644 --- a/packages/hoppscotch-app/helpers/lenses/htmlLens.js +++ b/packages/hoppscotch-app/helpers/lenses/htmlLens.ts @@ -1,10 +1,12 @@ -const htmlLens = { +import { Lens } from "./lenses" + +const htmlLens: Lens = { lensName: "response.html", isSupportedContentType: (contentType) => /\btext\/html|application\/xhtml\+xml\b/i.test(contentType), renderer: "htmlres", rendererImport: () => - import("~/components/lenses/renderers/HTMLLensRenderer"), + import("~/components/lenses/renderers/HTMLLensRenderer.vue"), } export default htmlLens diff --git a/packages/hoppscotch-app/helpers/lenses/imageLens.js b/packages/hoppscotch-app/helpers/lenses/imageLens.ts similarity index 67% rename from packages/hoppscotch-app/helpers/lenses/imageLens.js rename to packages/hoppscotch-app/helpers/lenses/imageLens.ts index 2c49b38df..d08d204ad 100644 --- a/packages/hoppscotch-app/helpers/lenses/imageLens.js +++ b/packages/hoppscotch-app/helpers/lenses/imageLens.ts @@ -1,4 +1,6 @@ -const imageLens = { +import { Lens } from "./lenses" + +const imageLens: Lens = { lensName: "response.image", isSupportedContentType: (contentType) => /\bimage\/(?:gif|jpeg|png|bmp|svg\+xml|x-icon|vnd\.microsoft\.icon)\b/i.test( @@ -6,7 +8,7 @@ const imageLens = { ), renderer: "imageres", rendererImport: () => - import("~/components/lenses/renderers/ImageLensRenderer"), + import("~/components/lenses/renderers/ImageLensRenderer.vue"), } export default imageLens diff --git a/packages/hoppscotch-app/helpers/lenses/jsonLens.js b/packages/hoppscotch-app/helpers/lenses/jsonLens.ts similarity index 62% rename from packages/hoppscotch-app/helpers/lenses/jsonLens.js rename to packages/hoppscotch-app/helpers/lenses/jsonLens.ts index 1d9135a24..28ad97348 100644 --- a/packages/hoppscotch-app/helpers/lenses/jsonLens.js +++ b/packages/hoppscotch-app/helpers/lenses/jsonLens.ts @@ -1,11 +1,12 @@ import { isJSONContentType } from "../utils/contenttypes" +import { Lens } from "./lenses" -const jsonLens = { +const jsonLens: Lens = { lensName: "response.json", isSupportedContentType: isJSONContentType, renderer: "json", rendererImport: () => - import("~/components/lenses/renderers/JSONLensRenderer"), + import("~/components/lenses/renderers/JSONLensRenderer.vue"), } export default jsonLens diff --git a/packages/hoppscotch-app/helpers/lenses/lenses.js b/packages/hoppscotch-app/helpers/lenses/lenses.js deleted file mode 100644 index 674797fce..000000000 --- a/packages/hoppscotch-app/helpers/lenses/lenses.js +++ /dev/null @@ -1,28 +0,0 @@ -import jsonLens from "./jsonLens" -import rawLens from "./rawLens" -import imageLens from "./imageLens" -import htmlLens from "./htmlLens" -import xmlLens from "./xmlLens" - -export const lenses = [jsonLens, imageLens, htmlLens, xmlLens, rawLens] - -export function getSuitableLenses(response) { - const contentType = response.headers.find((h) => h.key === "content-type") - - if (!contentType) return [rawLens] - - const result = [] - for (const lens of lenses) { - if (lens.isSupportedContentType(contentType.value)) result.push(lens) - } - - return result -} - -export function getLensRenderers() { - const response = {} - for (const lens of lenses) { - response[lens.renderer] = lens.rendererImport - } - return response -} diff --git a/packages/hoppscotch-app/helpers/lenses/lenses.ts b/packages/hoppscotch-app/helpers/lenses/lenses.ts new file mode 100644 index 000000000..a6fe10728 --- /dev/null +++ b/packages/hoppscotch-app/helpers/lenses/lenses.ts @@ -0,0 +1,42 @@ +import { HoppRESTResponse } from "../types/HoppRESTResponse" +import jsonLens from "./jsonLens" +import rawLens from "./rawLens" +import imageLens from "./imageLens" +import htmlLens from "./htmlLens" +import xmlLens from "./xmlLens" + +export type Lens = { + lensName: string + isSupportedContentType: (contentType: string) => boolean + renderer: string + rendererImport: () => Promise +} + +export const lenses: Lens[] = [jsonLens, imageLens, htmlLens, xmlLens, rawLens] + +export function getSuitableLenses(response: HoppRESTResponse): Lens[] { + // return empty array if response is loading or error + if (response.type === "loading" || response.type === "network_fail") return [] + + const contentType = response.headers.find((h) => h.key === "content-type") + + if (!contentType) return [rawLens] + + const result = [] + for (const lens of lenses) { + if (lens.isSupportedContentType(contentType.value)) result.push(lens) + } + return result +} + +type LensRenderers = { + [key: string]: Lens["rendererImport"] +} + +export function getLensRenderers(): LensRenderers { + const response: LensRenderers = {} + for (const lens of lenses) { + response[lens.renderer] = lens.rendererImport + } + return response +} diff --git a/packages/hoppscotch-app/helpers/lenses/rawLens.js b/packages/hoppscotch-app/helpers/lenses/rawLens.js deleted file mode 100644 index f8a18e193..000000000 --- a/packages/hoppscotch-app/helpers/lenses/rawLens.js +++ /dev/null @@ -1,8 +0,0 @@ -const rawLens = { - lensName: "response.raw", - isSupportedContentType: () => true, - renderer: "raw", - rendererImport: () => import("~/components/lenses/renderers/RawLensRenderer"), -} - -export default rawLens diff --git a/packages/hoppscotch-app/helpers/lenses/rawLens.ts b/packages/hoppscotch-app/helpers/lenses/rawLens.ts new file mode 100644 index 000000000..ec3a7c64f --- /dev/null +++ b/packages/hoppscotch-app/helpers/lenses/rawLens.ts @@ -0,0 +1,11 @@ +import { Lens } from "./lenses" + +const rawLens: Lens = { + lensName: "response.raw", + isSupportedContentType: () => true, + renderer: "raw", + rendererImport: () => + import("~/components/lenses/renderers/RawLensRenderer.vue"), +} + +export default rawLens diff --git a/packages/hoppscotch-app/helpers/lenses/xmlLens.js b/packages/hoppscotch-app/helpers/lenses/xmlLens.ts similarity index 50% rename from packages/hoppscotch-app/helpers/lenses/xmlLens.js rename to packages/hoppscotch-app/helpers/lenses/xmlLens.ts index d393be5f2..2f126bbb7 100644 --- a/packages/hoppscotch-app/helpers/lenses/xmlLens.js +++ b/packages/hoppscotch-app/helpers/lenses/xmlLens.ts @@ -1,8 +1,11 @@ -const xmlLens = { +import { Lens } from "./lenses" + +const xmlLens: Lens = { lensName: "response.xml", isSupportedContentType: (contentType) => /\bxml\b/i.test(contentType), renderer: "xmlres", - rendererImport: () => import("~/components/lenses/renderers/XMLLensRenderer"), + rendererImport: () => + import("~/components/lenses/renderers/XMLLensRenderer.vue"), } export default xmlLens diff --git a/packages/hoppscotch-app/helpers/newOutline.ts b/packages/hoppscotch-app/helpers/newOutline.ts new file mode 100644 index 000000000..b40efaa7b --- /dev/null +++ b/packages/hoppscotch-app/helpers/newOutline.ts @@ -0,0 +1,100 @@ +import { + JSONArrayValue, + JSONObjectMember, + JSONObjectValue, + JSONValue, +} from "./jsonParse" + +type RootEntry = + | { + kind: "RootObject" + astValue: JSONObjectValue + } + | { + kind: "RootArray" + astValue: JSONArrayValue + } + +type ObjectMemberEntry = { + kind: "ObjectMember" + name: string + astValue: JSONObjectMember + astParent: JSONObjectValue +} + +type ArrayMemberEntry = { + kind: "ArrayMember" + index: number + astValue: JSONValue + astParent: JSONArrayValue +} + +type PathEntry = RootEntry | ObjectMemberEntry | ArrayMemberEntry + +export function getJSONOutlineAtPos( + jsonRootAst: JSONObjectValue | JSONArrayValue, + posIndex: number +): PathEntry[] | null { + try { + const rootObj = jsonRootAst + + if (posIndex > rootObj.end || posIndex < rootObj.start) + throw new Error("Invalid position") + + let current: JSONValue = rootObj + + const path: PathEntry[] = [] + + if (rootObj.kind === "Object") { + path.push({ + kind: "RootObject", + astValue: rootObj, + }) + } else { + path.push({ + kind: "RootArray", + astValue: rootObj, + }) + } + + while (current.kind === "Object" || current.kind === "Array") { + if (current.kind === "Object") { + const next: JSONObjectMember | undefined = current.members.find( + (member) => member.start <= posIndex && member.end >= posIndex + ) + + if (!next) throw new Error("Couldn't find child") + + path.push({ + kind: "ObjectMember", + name: next.key.value, + astValue: next, + astParent: current, + }) + + current = next.value + } else { + const nextIndex = current.values.findIndex( + (value) => value.start <= posIndex && value.end >= posIndex + ) + + if (nextIndex < 0) throw new Error("Couldn't find child") + + const next: JSONValue = current.values[nextIndex] + + path.push({ + kind: "ArrayMember", + index: nextIndex, + astValue: next, + astParent: current, + }) + + current = next + } + } + + return path + } catch (e: any) { + return null + } +} diff --git a/packages/hoppscotch-app/helpers/outline.js b/packages/hoppscotch-app/helpers/outline.js deleted file mode 100644 index 1eb3e31d5..000000000 --- a/packages/hoppscotch-app/helpers/outline.js +++ /dev/null @@ -1,124 +0,0 @@ -import jsonParse from "./jsonParse" - -export default () => { - let jsonAST = {} - let path = [] - - const init = (jsonStr) => { - jsonAST = jsonParse(jsonStr) - linkParents(jsonAST) - } - - const setNewText = (jsonStr) => { - init(jsonStr) - path = [] - } - - const linkParents = (node) => { - if (node.kind === "Object") { - if (node.members) { - node.members.forEach((m) => { - m.parent = node - linkParents(m) - }) - } - } else if (node.kind === "Array") { - if (node.values) { - node.values.forEach((v) => { - v.parent = node - linkParents(v) - }) - } - } else if (node.kind === "Member") { - if (node.value) { - node.value.parent = node - linkParents(node.value) - } - } - } - - const genPath = (index) => { - let output = {} - path = [] - let current = jsonAST - if (current.kind === "Object") { - path.push({ label: "{}", obj: "root" }) - } else if (current.kind === "Array") { - path.push({ label: "[]", obj: "root" }) - } - let over = false - - try { - while (!over) { - if (current.kind === "Object") { - let i = 0 - let found = false - while (i < current.members.length) { - const m = current.members[i] - if (m.start <= index && m.end >= index) { - path.push({ label: m.key.value, obj: m }) - current = current.members[i] - found = true - break - } - i++ - } - if (!found) over = true - } else if (current.kind === "Array") { - if (current.values) { - let i = 0 - let found = false - while (i < current.values.length) { - const m = current.values[i] - if (m.start <= index && m.end >= index) { - path.push({ label: `[${i.toString()}]`, obj: m }) - current = current.values[i] - found = true - break - } - i++ - } - if (!found) over = true - } else over = true - } else if (current.kind === "Member") { - if (current.value) { - if (current.value.start <= index && current.value.end >= index) { - current = current.value - } else over = true - } else over = true - } else if ( - current.kind === "String" || - current.kind === "Number" || - current.kind === "Boolean" || - current.kind === "Null" - ) { - if (current.start <= index && current.end >= index) { - path.push({ label: `${current.value}`, obj: current }) - } - over = true - } - } - output = { success: true, res: path.map((p) => p.label) } - } catch (e) { - output = { success: false, res: e } - } - return output - } - - const getSiblings = (index) => { - const parent = path[index]?.obj?.parent - if (!parent) return [] - else if (parent.kind === "Object") { - return parent.members - } else if (parent.kind === "Array") { - return parent.values - } else return [] - } - - return { - init, - genPath, - getSiblings, - setNewText, - } -} diff --git a/packages/hoppscotch-app/helpers/types/HoppRequestSaveContext.ts b/packages/hoppscotch-app/helpers/types/HoppRequestSaveContext.ts index 6293b583a..f80def03b 100644 --- a/packages/hoppscotch-app/helpers/types/HoppRequestSaveContext.ts +++ b/packages/hoppscotch-app/helpers/types/HoppRequestSaveContext.ts @@ -28,4 +28,12 @@ export type HoppRequestSaveContext = * ID of the request in the team */ requestID: string + /** + * ID of the team + */ + teamID?: string + /** + * ID of the collection loaded + */ + collectionID?: string } diff --git a/packages/hoppscotch-app/helpers/utils/StreamUtils.ts b/packages/hoppscotch-app/helpers/utils/StreamUtils.ts index fad17f58e..342e9b4b3 100644 --- a/packages/hoppscotch-app/helpers/utils/StreamUtils.ts +++ b/packages/hoppscotch-app/helpers/utils/StreamUtils.ts @@ -8,9 +8,9 @@ import { map } from "rxjs/operators" * * @returns The constructed object observable */ -export function constructFromStreams( - streamObj: { [key in keyof T]: Observable } -): Observable { +export function constructFromStreams(streamObj: { + [key in keyof T]: Observable +}): Observable { return combineLatest(Object.values>(streamObj)).pipe( map((streams) => { const keys = Object.keys(streamObj) as (keyof T)[] diff --git a/packages/hoppscotch-app/helpers/utils/b64.js b/packages/hoppscotch-app/helpers/utils/b64.ts similarity index 91% rename from packages/hoppscotch-app/helpers/utils/b64.js rename to packages/hoppscotch-app/helpers/utils/b64.ts index 942b003c4..3b364c0e9 100644 --- a/packages/hoppscotch-app/helpers/utils/b64.js +++ b/packages/hoppscotch-app/helpers/utils/b64.ts @@ -1,4 +1,4 @@ -export const decodeB64StringToArrayBuffer = (input) => { +export function decodeB64StringToArrayBuffer(input: string): ArrayBuffer { const bytes = Math.floor((input.length / 4) * 3) const ab = new ArrayBuffer(bytes) const uarray = new Uint8Array(ab) diff --git a/packages/hoppscotch-app/helpers/utils/string.js b/packages/hoppscotch-app/helpers/utils/string.js deleted file mode 100644 index 6967e1c35..000000000 --- a/packages/hoppscotch-app/helpers/utils/string.js +++ /dev/null @@ -1,12 +0,0 @@ -export function getSourcePrefix(source) { - const sourceEmojis = { - // Source used for info messages. - info: "\tℹ️ [INFO]:\t", - // Source used for client to server messages. - client: "\t⬅️ [SENT]:\t", - // Source used for server to client messages. - server: "\t➡️ [RECEIVED]:\t", - } - if (Object.keys(sourceEmojis).includes(source)) return sourceEmojis[source] - return "" -} diff --git a/packages/hoppscotch-app/helpers/utils/string.ts b/packages/hoppscotch-app/helpers/utils/string.ts new file mode 100644 index 000000000..f23107591 --- /dev/null +++ b/packages/hoppscotch-app/helpers/utils/string.ts @@ -0,0 +1,12 @@ +const sourceEmojis = { + // Source used for info messages. + info: "\tℹ️ [INFO]:\t", + // Source used for client to server messages. + client: "\t⬅️ [SENT]:\t", + // Source used for server to client messages. + server: "\t➡️ [RECEIVED]:\t", +} + +export function getSourcePrefix(source: keyof typeof sourceEmojis) { + return sourceEmojis[source] +} diff --git a/packages/hoppscotch-app/layouts/default.vue b/packages/hoppscotch-app/layouts/default.vue index 5bc5bfdcd..544568052 100644 --- a/packages/hoppscotch-app/layouts/default.vue +++ b/packages/hoppscotch-app/layouts/default.vue @@ -24,7 +24,7 @@ >
- +
diff --git a/packages/hoppscotch-app/locales/en.json b/packages/hoppscotch-app/locales/en.json index a8f11b80c..8af61b8b7 100644 --- a/packages/hoppscotch-app/locales/en.json +++ b/packages/hoppscotch-app/locales/en.json @@ -421,6 +421,7 @@ "file_imported": "File imported", "finished_in": "Finished in {duration}ms", "history_deleted": "History deleted", + "linewrap": "Wrap lines", "loading": "Loading...", "none": "None", "nothing_found": "Nothing found for", diff --git a/packages/hoppscotch-app/modules/emit-volar-types.ts b/packages/hoppscotch-app/modules/emit-volar-types.ts new file mode 100644 index 000000000..cad34e011 --- /dev/null +++ b/packages/hoppscotch-app/modules/emit-volar-types.ts @@ -0,0 +1,134 @@ +import { resolve } from "path" +import { Module } from "@nuxt/types" +import ts from "typescript" +import chokidar from "chokidar" + +const { readdir, writeFile } = require("fs").promises + +function titleCase(str: string): string { + return str[0].toUpperCase() + str.substring(1) +} + +async function* getFilesInDir(dir: string): AsyncIterable { + const dirents = await readdir(dir, { withFileTypes: true }) + for (const dirent of dirents) { + const res = resolve(dir, dirent.name) + if (dirent.isDirectory()) { + yield* getFilesInDir(res) + } else { + yield res + } + } +} + +async function getAllVueComponentPaths(): Promise { + const vueFilePaths: string[] = [] + + for await (const f of getFilesInDir("./components")) { + if (f.endsWith(".vue")) { + const componentsIndex = f.split("/").indexOf("components") + + vueFilePaths.push(`./${f.split("/").slice(componentsIndex).join("/")}`) + } + } + + return vueFilePaths +} + +function resolveComponentName(filename: string): string { + const index = filename.split("/").indexOf("components") + + return filename + .split("/") + .slice(index + 1) + .filter((x) => x !== "index.vue") // Remove index.vue + .map((x) => x.split(".vue")[0]) // Remove extension + .filter((x) => x.toUpperCase() !== x.toLowerCase()) // Remove non-word stuff + .map((x) => titleCase(x)) // titlecase it + .join("") +} + +function createTSImports(components: [string, string][]) { + return components.map(([componentName, componentPath]) => { + return ts.factory.createImportDeclaration( + undefined, + undefined, + ts.factory.createImportClause( + false, + ts.factory.createIdentifier(componentName), + undefined + ), + ts.factory.createStringLiteral(componentPath) + ) + }) +} + +function createTSProps(components: [string, string][]) { + return components.map(([componentName]) => { + return ts.factory.createPropertySignature( + undefined, + ts.factory.createIdentifier(componentName), + undefined, + ts.factory.createTypeQueryNode(ts.factory.createIdentifier(componentName)) + ) + }) +} + +function generateTypeScriptDef(components: [string, string][]) { + const statements = [ + ...createTSImports(components), + ts.factory.createModuleDeclaration( + undefined, + [ts.factory.createModifier(ts.SyntaxKind.DeclareKeyword)], + ts.factory.createIdentifier("global"), + ts.factory.createModuleBlock([ + ts.factory.createInterfaceDeclaration( + undefined, + undefined, + ts.factory.createIdentifier("__VLS_GlobalComponents"), + undefined, + undefined, + [...createTSProps(components)] + ), + ]), + ts.NodeFlags.ExportContext | + ts.NodeFlags.GlobalAugmentation | + ts.NodeFlags.ContextFlags + ), + ] + + const source = ts.factory.createSourceFile( + statements, + ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), + ts.NodeFlags.None + ) + + const printer = ts.createPrinter({ + newLine: ts.NewLineKind.LineFeed, + }) + + return printer.printFile(source) +} + +async function generateShim() { + const results = await getAllVueComponentPaths() + const fileComponentNameCombo: [string, string][] = results.map((x) => [ + resolveComponentName(x), + x, + ]) + const typescriptString = generateTypeScriptDef(fileComponentNameCombo) + + await writeFile(resolve("shims-volar.d.ts"), typescriptString) +} + +const module: Module<{}> = async function () { + if (!this.nuxt.options.dev) return + + await generateShim() + + chokidar.watch(resolve("../components/")).on("all", async () => { + await generateShim() + }) +} + +export default module diff --git a/packages/hoppscotch-app/nuxt.config.js b/packages/hoppscotch-app/nuxt.config.js index b77f39f75..39da30434 100644 --- a/packages/hoppscotch-app/nuxt.config.js +++ b/packages/hoppscotch-app/nuxt.config.js @@ -133,6 +133,7 @@ export default { "@nuxtjs/composition-api/module", // https://github.com/antfu/unplugin-vue2-script-setup "unplugin-vue2-script-setup/nuxt", + "~/modules/emit-volar-types.ts", ], // Modules (https://go.nuxtjs.dev/config-modules) @@ -280,7 +281,7 @@ export default { config.module.rules.push({ test: /\.js$/, include: /(node_modules)/, - exclude: /(node_modules)\/(ace-builds)|(@firebase)/, + exclude: /(node_modules)\/(@firebase)/, loader: "babel-loader", options: { plugins: [ diff --git a/packages/hoppscotch-app/package.json b/packages/hoppscotch-app/package.json index 4afec6ad8..456b7e797 100644 --- a/packages/hoppscotch-app/package.json +++ b/packages/hoppscotch-app/package.json @@ -20,29 +20,26 @@ "lintfix": "eslint --ext .ts,.js,.vue --ignore-path .gitignore . --fix", "test": "jest" }, - "lint-staged": { - "*.{ts,js,vue}": "eslint", - "*.{css,scss,vue}": "stylelint" - }, "dependencies": { - "@apollo/client": "^3.4.10", + "@apollo/client": "^3.4.11", "@nuxtjs/axios": "^5.13.6", - "@nuxtjs/composition-api": "^0.28.0", + "@nuxtjs/composition-api": "^0.29.0", "@nuxtjs/gtm": "^2.4.0", "@nuxtjs/i18n": "^7.0.3", "@nuxtjs/robots": "^2.5.0", "@nuxtjs/sitemap": "^2.4.0", "@nuxtjs/toast": "^3.3.1", - "ace-builds": "^1.4.12", "acorn": "^8.5.0", "acorn-walk": "^8.2.0", - "axios": "^0.21.4", - "core-js": "^3.17.2", + "codemirror": "^5.62.3", + "codemirror-theme-github": "^1.0.0", + "core-js": "^3.17.3", "esprima": "^4.0.1", - "firebase": "^9.0.1", + "firebase": "^9.0.2", "fuse.js": "^6.4.6", "graphql": "^15.5.0", "graphql-language-service-interface": "^2.8.4", + "graphql-language-service-parser": "^1.9.2", "json-loader": "^0.5.7", "lodash": "^4.17.21", "mustache": "^4.2.0", @@ -65,7 +62,7 @@ }, "devDependencies": { "@babel/core": "^7.15.5", - "@babel/preset-env": "^7.15.4", + "@babel/preset-env": "^7.15.6", "@commitlint/cli": "^13.1.0", "@commitlint/config-conventional": "^13.1.0", "@nuxt/types": "^2.15.8", @@ -79,33 +76,34 @@ "@nuxtjs/stylelint-module": "^4.0.0", "@nuxtjs/svg": "^0.2.0", "@testing-library/jest-dom": "^5.14.1", + "@types/codemirror": "^5.60.2", "@types/cookie": "^0.4.1", + "@types/esprima": "^4.0.3", "@types/lodash": "^4.14.172", "@types/splitpanes": "^2.2.1", - "@vue/runtime-dom": "^3.2.10", + "@vue/runtime-dom": "^3.2.11", "@vue/test-utils": "^1.2.2", "babel-core": "^7.0.0-bridge.0", - "babel-jest": "^27.1.0", + "babel-jest": "^27.2.0", "eslint": "^7.32.0", "eslint-config-prettier": "^8.1.0", "eslint-plugin-nuxt": ">=2.0.0", "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-vue": "^7.17.0", - "jest": "^27.1.0", + "jest": "^27.2.0", "jest-serializer-vue": "^2.0.2", - "lint-staged": "^11.1.2", - "nuxt-windicss": "^1.2.3", - "prettier": "^2.3.2", + "nuxt-windicss": "^1.2.4", + "prettier": "^2.4.0", "pretty-quick": "^3.1.1", "raw-loader": "^4.0.2", - "sass": "^1.39.0", + "sass": "^1.40.1", "sass-loader": "^10.2.0", "stylelint": "^13.12.0", "stylelint-config-prettier": "^8.0.2", "stylelint-config-standard": "^22.0.0", "ts-jest": "^27.0.5", "typescript": "^4.2", - "unplugin-vue2-script-setup": "^0.5.8", + "unplugin-vue2-script-setup": "^0.6.1", "vue-jest": "^3.0.7", "worker-loader": "^3.0.8" } diff --git a/packages/hoppscotch-app/pages/documentation.vue b/packages/hoppscotch-app/pages/documentation.vue index 266a6efa3..2feb14210 100644 --- a/packages/hoppscotch-app/pages/documentation.vue +++ b/packages/hoppscotch-app/pages/documentation.vue @@ -61,17 +61,12 @@ @click.native="collectionJSON = '[]'" />
-
+ + + + + + + + + + + + - - - - - - - - - - - - - - - -
+ + +