refactor: monorepo+pnpm (removed husky)

This commit is contained in:
Andrew Bastin
2021-09-10 00:28:28 +05:30
parent 917550ff4d
commit b28f82a881
445 changed files with 81301 additions and 63752 deletions

View File

@@ -0,0 +1,149 @@
import { combineLatest, Observable } from "rxjs"
import { map } from "rxjs/operators"
import { FormDataKeyValue, HoppRESTRequest } from "../types/HoppRESTRequest"
import parseTemplateString from "../templating"
import { Environment, getGlobalVariables } from "~/newstore/environments"
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 }[]
effectiveFinalParams: { key: string; value: string }[]
effectiveFinalBody: FormData | string | null
}
function getFinalBodyFromRequest(
request: HoppRESTRequest,
env: Environment
): FormData | string | null {
if (request.body.contentType === null) {
return null
}
if (request.body.contentType === "multipart/form-data") {
const formData = new FormData()
request.body.body
.filter((x) => x.key !== "" && x.active) // Remove empty keys
.map(
(x) =>
<FormDataKeyValue>{
active: x.active,
isFile: x.isFile,
key: parseTemplateString(x.key, env.variables),
value: x.isFile
? x.value
: parseTemplateString(x.value, env.variables),
}
)
.forEach((entry) => {
if (!entry.isFile) formData.append(entry.key, entry.value)
else entry.value.forEach((blob) => formData.append(entry.key, blob))
})
return formData
} else return request.body.body
}
/**
* 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
): EffectiveHoppRESTRequest {
const envVariables = [...environment.variables, ...getGlobalVariables()]
const effectiveFinalHeaders = request.headers
.filter(
(x) =>
x.key !== "" && // Remove empty keys
x.active // Only active
)
.map((x) => ({
// Parse out environment template strings
active: true,
key: parseTemplateString(x.key, envVariables),
value: parseTemplateString(x.value, envVariables),
}))
// 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
)}`,
})
}
}
const effectiveFinalBody = getFinalBodyFromRequest(request, environment)
if (request.body.contentType)
effectiveFinalHeaders.push({
active: true,
key: "content-type",
value: request.body.contentType,
})
return {
...request,
effectiveFinalURL: parseTemplateString(request.endpoint, envVariables),
effectiveFinalHeaders,
effectiveFinalParams: request.params
.filter(
(x) =>
x.key !== "" && // Remove empty keys
x.active // Only active
)
.map((x) => ({
active: true,
key: parseTemplateString(x.key, envVariables),
value: parseTemplateString(x.value, envVariables),
})),
effectiveFinalBody,
}
}
/**
* Creates an Observable Stream that emits HoppRESTRequests whenever
* the input streams emit a value
*
* @param request$ The request stream containing request data
* @param environment$ The environment stream containing environment data to apply
*
* @returns Observable Stream for the Effective Request Object
*/
export function getEffectiveRESTRequestStream(
request$: Observable<HoppRESTRequest>,
environment$: Observable<Environment>
): Observable<EffectiveHoppRESTRequest> {
return combineLatest([request$, environment$]).pipe(
map(([request, env]) => getEffectiveRESTRequest(request, env))
)
}

View File

@@ -0,0 +1,24 @@
import { combineLatest, Observable } from "rxjs"
import { map } from "rxjs/operators"
/**
* Constructs a stream of a object from a collection of other observables
*
* @param streamObj The object containing key of observables to assemble from
*
* @returns The constructed object observable
*/
export function constructFromStreams<T>(
streamObj: { [key in keyof T]: Observable<T[key]> }
): Observable<T> {
return combineLatest(Object.values<Observable<T[keyof T]>>(streamObj)).pipe(
map((streams) => {
const keys = Object.keys(streamObj) as (keyof T)[]
return keys.reduce(
(acc, s, i) => Object.assign(acc, { [s]: streams[i] }),
{}
) as T
})
)
}

View File

@@ -0,0 +1,15 @@
import { TextDecoder } from "util"
import { decodeB64StringToArrayBuffer } from "../b64"
describe("decodeB64StringToArrayBuffer", () => {
test("decodes content correctly", () => {
const decoder = new TextDecoder("utf-8")
expect(
decoder.decode(
decodeB64StringToArrayBuffer("aG9wcHNjb3RjaCBpcyBhd2Vzb21lIQ==")
)
).toMatch("hoppscotch is awesome!")
})
// TODO : More tests for binary data ?
})

View File

@@ -0,0 +1,40 @@
import { isJSONContentType } from "../contenttypes"
describe("isJSONContentType", () => {
test("returns true for JSON content types", () => {
expect(isJSONContentType("application/json")).toBe(true)
expect(isJSONContentType("application/vnd.api+json")).toBe(true)
expect(isJSONContentType("application/hal+json")).toBe(true)
expect(isJSONContentType("application/ld+json")).toBe(true)
})
test("returns true for JSON types with charset specified", () => {
expect(isJSONContentType("application/json; charset=utf-8")).toBe(true)
expect(isJSONContentType("application/vnd.api+json; charset=utf-8")).toBe(
true
)
expect(isJSONContentType("application/hal+json; charset=utf-8")).toBe(true)
expect(isJSONContentType("application/ld+json; charset=utf-8")).toBe(true)
})
test("returns false for non-JSON content types", () => {
expect(isJSONContentType("application/xml")).toBe(false)
expect(isJSONContentType("text/html")).toBe(false)
expect(isJSONContentType("application/x-www-form-urlencoded")).toBe(false)
expect(isJSONContentType("foo/jsoninword")).toBe(false)
})
test("returns false for non-JSON content types with charset", () => {
expect(isJSONContentType("application/xml; charset=utf-8")).toBe(false)
expect(isJSONContentType("text/html; charset=utf-8")).toBe(false)
expect(
isJSONContentType("application/x-www-form-urlencoded; charset=utf-8")
).toBe(false)
expect(isJSONContentType("foo/jsoninword; charset=utf-8")).toBe(false)
})
test("returns false for null/undefined", () => {
expect(isJSONContentType(null)).toBe(false)
expect(isJSONContentType(undefined)).toBe(false)
})
})

View File

@@ -0,0 +1,38 @@
import debounce from "../debounce"
describe("debounce", () => {
test("doesn't call function right after calling", () => {
const fn = jest.fn()
const debFunc = debounce(fn, 100)
debFunc()
expect(fn).not.toHaveBeenCalled()
})
test("calls the function after the given timeout", () => {
const fn = jest.fn()
jest.useFakeTimers()
const debFunc = debounce(fn, 100)
debFunc()
jest.runAllTimers()
expect(fn).toHaveBeenCalled()
// expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 100)
})
test("calls the function only one time within the timeframe", () => {
const fn = jest.fn()
const debFunc = debounce(fn, 1000)
for (let i = 0; i < 100; i++) debFunc()
jest.runAllTimers()
expect(fn).toHaveBeenCalledTimes(1)
})
})

View File

@@ -0,0 +1,21 @@
import { parseUrlAndPath } from "../uri"
describe("parseUrlAndPath", () => {
test("has url and path fields", () => {
const result = parseUrlAndPath("https://hoppscotch.io/")
expect(result).toHaveProperty("url")
expect(result).toHaveProperty("path")
})
test("parses out URL correctly", () => {
const result = parseUrlAndPath("https://hoppscotch.io/test/page")
expect(result.url).toBe("https://hoppscotch.io")
})
test("parses out Path correctly", () => {
const result = parseUrlAndPath("https://hoppscotch.io/test/page")
expect(result.path).toBe("/test/page")
})
})

View File

@@ -0,0 +1,144 @@
import { wsValid, httpValid, socketioValid } from "../valid"
describe("wsValid", () => {
test("returns true for valid URL with IP address", () => {
expect(wsValid("wss://174.129.224.73/")).toBe(true)
expect(wsValid("wss://174.129.224.73")).toBe(true)
})
test("returns true for valid URL with Hostname", () => {
expect(wsValid("wss://echo.websocket.org/")).toBe(true)
expect(wsValid("wss://echo.websocket.org")).toBe(true)
})
test("returns false for invalid URL with IP address", () => {
expect(wsValid("wss://174.129.")).toBe(false)
expect(wsValid("wss://174.129./")).toBe(false)
})
test("returns false for invalid URL with hostname", () => {
expect(wsValid("wss://echo.websocket./")).toBe(false)
expect(wsValid("wss://echo.websocket.")).toBe(false)
})
test("returns false for non-wss protocol URLs", () => {
expect(wsValid("http://echo.websocket.org/")).toBe(false)
expect(wsValid("http://echo.websocket.org")).toBe(false)
expect(wsValid("http://174.129.224.73/")).toBe(false)
expect(wsValid("http://174.129.224.73")).toBe(false)
})
test("returns true for wss protocol URLs", () => {
expect(wsValid("wss://echo.websocket.org/")).toBe(true)
expect(wsValid("wss://echo.websocket.org")).toBe(true)
expect(wsValid("wss://174.129.224.73/")).toBe(true)
expect(wsValid("wss://174.129.224.73")).toBe(true)
})
test("returns true for ws protocol URLs", () => {
expect(wsValid("ws://echo.websocket.org/")).toBe(true)
expect(wsValid("ws://echo.websocket.org")).toBe(true)
expect(wsValid("ws://174.129.224.73/")).toBe(true)
expect(wsValid("ws://174.129.224.73")).toBe(true)
})
})
describe("httpValid", () => {
test("returns true for valid URL with IP address", () => {
expect(httpValid("http://174.129.224.73/")).toBe(true)
expect(httpValid("http://174.129.224.73")).toBe(true)
})
test("returns true for valid URL with Hostname", () => {
expect(httpValid("http://echo.websocket.org/")).toBe(true)
expect(httpValid("http://echo.websocket.org")).toBe(true)
})
test("returns false for invalid URL with IP address", () => {
expect(httpValid("http://174.129./")).toBe(false)
expect(httpValid("http://174.129.")).toBe(false)
})
test("returns false for invalid URL with hostname", () => {
expect(httpValid("http://echo.websocket./")).toBe(false)
expect(httpValid("http://echo.websocket.")).toBe(false)
})
test("returns false for non-http(s) protocol URLs", () => {
expect(httpValid("wss://echo.websocket.org/")).toBe(false)
expect(httpValid("wss://echo.websocket.org")).toBe(false)
expect(httpValid("wss://174.129.224.73/")).toBe(false)
expect(httpValid("wss://174.129.224.73")).toBe(false)
})
test("returns true for HTTP protocol URLs", () => {
expect(httpValid("http://echo.websocket.org/")).toBe(true)
expect(httpValid("http://echo.websocket.org")).toBe(true)
expect(httpValid("http://174.129.224.73/")).toBe(true)
expect(httpValid("http://174.129.224.73")).toBe(true)
})
test("returns true for HTTPS protocol URLs", () => {
expect(httpValid("https://echo.websocket.org/")).toBe(true)
expect(httpValid("https://echo.websocket.org")).toBe(true)
expect(httpValid("https://174.129.224.73/")).toBe(true)
expect(httpValid("https://174.129.224.73")).toBe(true)
})
})
describe("socketioValid", () => {
test("returns true for valid URL with IP address", () => {
expect(socketioValid("http://174.129.224.73/")).toBe(true)
expect(socketioValid("http://174.129.224.73")).toBe(true)
})
test("returns true for valid URL with Hostname", () => {
expect(socketioValid("http://echo.websocket.org/")).toBe(true)
expect(socketioValid("http://echo.websocket.org")).toBe(true)
})
test("returns false for invalid URL with IP address", () => {
expect(socketioValid("http://174.129./")).toBe(false)
expect(socketioValid("http://174.129.")).toBe(false)
})
test("returns false for invalid URL with hostname", () => {
expect(socketioValid("http://echo.websocket./")).toBe(false)
expect(socketioValid("http://echo.websocket.")).toBe(false)
})
test("returns false for non-http(s) and non-wss protocol URLs", () => {
expect(socketioValid("ftp://echo.websocket.org/")).toBe(false)
expect(socketioValid("ftp://echo.websocket.org")).toBe(false)
expect(socketioValid("ftp://174.129.224.73/")).toBe(false)
expect(socketioValid("ftp://174.129.224.73")).toBe(false)
})
test("returns true for HTTP protocol URLs", () => {
expect(socketioValid("http://echo.websocket.org/")).toBe(true)
expect(socketioValid("http://echo.websocket.org")).toBe(true)
expect(socketioValid("http://174.129.224.73/")).toBe(true)
expect(socketioValid("http://174.129.224.73")).toBe(true)
})
test("returns true for HTTPS protocol URLs", () => {
expect(socketioValid("https://echo.websocket.org/")).toBe(true)
expect(socketioValid("https://echo.websocket.org")).toBe(true)
expect(socketioValid("https://174.129.224.73/")).toBe(true)
expect(socketioValid("https://174.129.224.73")).toBe(true)
})
test("returns true for wss protocol URLs", () => {
expect(socketioValid("wss://echo.websocket.org/")).toBe(true)
expect(socketioValid("wss://echo.websocket.org")).toBe(true)
expect(socketioValid("wss://174.129.224.73/")).toBe(true)
expect(socketioValid("wss://174.129.224.73")).toBe(true)
})
test("returns true for ws protocol URLs", () => {
expect(socketioValid("ws://echo.websocket.org/")).toBe(true)
expect(socketioValid("ws://echo.websocket.org")).toBe(true)
expect(socketioValid("ws://174.129.224.73/")).toBe(true)
expect(socketioValid("ws://174.129.224.73")).toBe(true)
})
})

View File

@@ -0,0 +1,32 @@
export const decodeB64StringToArrayBuffer = (input) => {
const bytes = Math.floor((input.length / 4) * 3)
const ab = new ArrayBuffer(bytes)
const uarray = new Uint8Array(ab)
const keyStr =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
let chr1, chr2, chr3
let enc1, enc2, enc3, enc4
let j = 0
input = input.replace(/[^A-Za-z0-9+/=]/g, "")
for (let i = 0; i < bytes; i += 3) {
// get the 3 octets in 4 ASCII chars
enc1 = keyStr.indexOf(input.charAt(j++))
enc2 = keyStr.indexOf(input.charAt(j++))
enc3 = keyStr.indexOf(input.charAt(j++))
enc4 = keyStr.indexOf(input.charAt(j++))
chr1 = (enc1 << 2) | (enc2 >> 4)
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2)
chr3 = ((enc3 & 3) << 6) | enc4
uarray[i] = chr1
if (enc3 !== 64) uarray[i + 1] = chr2
if (enc4 !== 64) uarray[i + 2] = chr3
}
return ab
}

View File

@@ -0,0 +1,18 @@
/**
* Copies a given string to the clipboard using
* the legacy exec method
*
* @param content The content to be copied
*/
export function copyToClipboard(content: string) {
if (navigator.clipboard) {
navigator.clipboard.writeText(content)
} else {
const dummy = document.createElement("textarea")
document.body.appendChild(dummy)
dummy.value = content
dummy.select()
document.execCommand("copy")
document.body.removeChild(dummy)
}
}

View File

@@ -0,0 +1,134 @@
import {
customRef,
DeepReadonly,
onBeforeUnmount,
readonly,
Ref,
ref,
watch,
wrapProperty,
} from "@nuxtjs/composition-api"
import { Observable, Subscription } from "rxjs"
export const useNuxt = wrapProperty("$nuxt")
export function useReadonlyStream<T>(
stream$: Observable<T>,
initialValue: T
): Ref<DeepReadonly<T>> {
let sub: Subscription | null = null
onBeforeUnmount(() => {
if (sub) {
sub.unsubscribe()
}
})
const targetRef = ref(initialValue) as Ref<T>
sub = stream$.subscribe((value) => {
targetRef.value = value
})
return readonly(targetRef)
}
export function useStream<T>(
stream$: Observable<T>,
initialValue: T,
setter: (val: T) => void
) {
let sub: Subscription | null = null
onBeforeUnmount(() => {
if (sub) {
sub.unsubscribe()
}
})
return customRef((track, trigger) => {
let value = initialValue
sub = stream$.subscribe((val) => {
value = val
trigger()
})
return {
get() {
track()
return value
},
set(value: T) {
trigger()
setter(value)
},
}
})
}
export function pluckRef<T, K extends keyof T>(ref: Ref<T>, key: K): Ref<T[K]> {
return customRef((track, trigger) => {
const stopWatching = watch(ref, (newVal, oldVal) => {
if (newVal[key] !== oldVal[key]) {
trigger()
}
})
onBeforeUnmount(() => {
stopWatching()
})
return {
get() {
track()
return ref.value[key]
},
set(value: T[K]) {
trigger()
ref.value = Object.assign(ref.value, { [key]: value })
},
}
})
}
export function pluckMultipleFromRef<T, K extends Array<keyof T>>(
sourceRef: Ref<T>,
keys: K
): { [key in K[number]]: Ref<T[key]> } {
return Object.fromEntries(keys.map((x) => [x, pluckRef(sourceRef, x)])) as any
}
/**
* A composable that provides the ability to run streams
* and subscribe to them and respect the component lifecycle.
*/
export function useStreamSubscriber() {
const subs: Subscription[] = []
const runAndSubscribe = <T>(
stream: Observable<T>,
next?: (value: T) => void,
error?: (e: any) => void,
complete?: () => void
) => {
const sub = stream.subscribe({
next,
error,
complete: () => {
if (complete) complete()
subs.splice(subs.indexOf(sub), 1)
},
})
subs.push(sub)
}
onBeforeUnmount(() => {
subs.forEach((sub) => sub.unsubscribe())
})
return {
subscribeToStream: runAndSubscribe,
}
}

View File

@@ -0,0 +1,17 @@
export const knownContentTypes = {
"application/json": "json",
"application/ld+json": "json",
"application/hal+json": "json",
"application/vnd.api+json": "json",
"application/xml": "xml",
"application/x-www-form-urlencoded": "multipart",
"multipart/form-data": "multipart",
"text/html": "html",
"text/plain": "plain",
}
export type ValidContentTypes = keyof typeof knownContentTypes
export function isJSONContentType(contentType: string) {
return /\bjson\b/i.test(contentType)
}

View File

@@ -0,0 +1,15 @@
// Debounce is a higher order function which makes its enclosed function be executed
// only if the function wasn't called again till 'delay' time has passed, this helps reduce impact of heavy working
// functions which might be called frequently
// NOTE : Don't use lambda functions as this doesn't get bound properly in them, use the 'function (args) {}' format
const debounce = (func, delay) => {
let inDebounce
return function () {
const context = this
const args = arguments
clearTimeout(inDebounce)
inDebounce = setTimeout(() => func.apply(context, args), delay)
}
}
export default debounce

View File

@@ -0,0 +1,19 @@
export function isDOMElement(el: any): el is HTMLElement {
return !!el && (el instanceof Element || el instanceof HTMLElement)
}
export function isTypableElement(el: HTMLElement): boolean {
// If content editable, then it is editable
if (el.isContentEditable) return true
// If element is an input and the input is enabled, then it is typable
if (el.tagName === "INPUT") {
return !(el as HTMLInputElement).disabled
}
// If element is a textarea and the input is enabled, then it is typable
if (el.tagName === "TEXTAREA") {
return !(el as HTMLTextAreaElement).disabled
}
return false
}

View File

@@ -0,0 +1,12 @@
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 ""
}

View File

@@ -0,0 +1,15 @@
export function parseUrlAndPath(value) {
const result = {}
try {
const url = new URL(value)
result.url = url.origin
result.path = url.pathname
} catch (e) {
const uriRegex = value.match(
/^((http[s]?:\/\/)?(<<[^/]+>>)?[^/]*|)(\/?.*)$/
)
result.url = uriRegex[1]
result.path = uriRegex[4]
}
return result
}

View File

@@ -0,0 +1,25 @@
import {
Ref,
onMounted,
onUnmounted,
reactive,
toRefs,
} from "@nuxtjs/composition-api"
interface WindowSize {
x: Ref<number>
y: Ref<number>
}
export function useWindowSize(): WindowSize {
const windowSize = reactive({ x: 0, y: 0 })
const resizeListener = () => {
;({ innerWidth: windowSize.x, innerHeight: windowSize.y } = window)
}
onMounted(() => window.addEventListener("resize", resizeListener))
onUnmounted(() => window.removeEventListener("resize", resizeListener))
resizeListener()
return toRefs(windowSize)
}
export default useWindowSize

View File

@@ -0,0 +1,38 @@
const [wsRegexIP, wsRegexHostname] = generateREForProtocol("^(wss?:\\/\\/)?")
const [sseRegexIP, sseRegexHostname] =
generateREForProtocol("^(https?:\\/\\/)?")
const [socketioRegexIP, socketioRegexHostname] = generateREForProtocol(
"^((wss?:\\/\\/)|(https?:\\/\\/))?"
)
function generateREForProtocol(protocol) {
return [
new RegExp(
`${protocol}(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]).){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$`
),
new RegExp(
`${protocol}(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]).)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9/])$`
),
]
}
/**
* valid url for ws/wss
*/
export function wsValid(url) {
return wsRegexIP.test(url) || wsRegexHostname.test(url)
}
/**
* valid url for http/https
*/
export function httpValid(url) {
return sseRegexIP.test(url) || sseRegexHostname.test(url)
}
/**
* valid url for ws/wss/http/https
*/
export function socketioValid(url) {
return socketioRegexIP.test(url) || socketioRegexHostname.test(url)
}