feat: migrate ESLint to v9 across packages (#5773)

Co-authored-by: curiouscorrelation <curiouscorrelation@gmail.com>
This commit is contained in:
James George 2026-01-20 14:48:55 +05:30 committed by GitHub
parent 992579e285
commit 27b817f627
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
88 changed files with 3223 additions and 3263 deletions

View file

@ -0,0 +1,67 @@
import pluginVue from "eslint-plugin-vue"
import {
defineConfigWithVueTs,
vueTsConfigs,
} from "@vue/eslint-config-typescript"
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"
import globals from "globals"
export default defineConfigWithVueTs(
{
ignores: [
"**/*.d.ts",
"dist/**",
"node_modules/**",
"src-tauri/**",
],
},
pluginVue.configs["flat/recommended"],
vueTsConfigs.recommended,
eslintPluginPrettierRecommended,
{
files: ["**/*.ts", "**/*.js", "**/*.vue"],
linterOptions: {
reportUnusedDisableDirectives: false,
},
languageOptions: {
sourceType: "module",
ecmaVersion: "latest",
globals: {
...globals.browser,
...globals.node,
},
parserOptions: {
requireConfigFile: false,
ecmaFeatures: {
jsx: false,
},
},
},
rules: {
semi: [2, "never"],
"no-console": "off",
"no-debugger": process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
"prettier/prettier":
process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
"vue/multi-word-component-names": "off",
"vue/no-side-effects-in-computed-properties": "off",
"@typescript-eslint/no-unused-vars": [
process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
{
args: "all",
argsIgnorePattern: "^_",
caughtErrors: "all",
caughtErrorsIgnorePattern: "^_",
destructuredArrayIgnorePattern: "^_",
varsIgnorePattern: "^_",
ignoreRestSiblings: true,
},
],
"@typescript-eslint/no-unused-expressions": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unsafe-function-type": "off",
"no-undef": "off",
},
}
)

View file

@ -7,7 +7,14 @@
"dev": "vite", "dev": "vite",
"build": "vue-tsc --noEmit && vite build", "build": "vue-tsc --noEmit && vite build",
"preview": "vite preview", "preview": "vite preview",
"tauri": "tauri" "tauri": "tauri",
"lint": "eslint src",
"lint:ts": "vue-tsc --noEmit",
"lintfix": "eslint --fix src",
"prod-lint": "cross-env HOPP_LINT_FOR_PROD=true pnpm run lint",
"do-lint": "pnpm run prod-lint",
"do-typecheck": "pnpm run lint:ts",
"do-lintfix": "pnpm run lintfix"
}, },
"dependencies": { "dependencies": {
"@hoppscotch/ui": "0.2.5", "@hoppscotch/ui": "0.2.5",
@ -24,8 +31,16 @@
"@tauri-apps/cli": "2.9.3", "@tauri-apps/cli": "2.9.3",
"@types/lodash-es": "4.17.12", "@types/lodash-es": "4.17.12",
"@types/node": "24.10.1", "@types/node": "24.10.1",
"@typescript-eslint/eslint-plugin": "8.50.0",
"@typescript-eslint/parser": "8.50.0",
"@vitejs/plugin-vue": "6.0.3", "@vitejs/plugin-vue": "6.0.3",
"@vue/eslint-config-typescript": "14.6.0",
"autoprefixer": "10.4.23", "autoprefixer": "10.4.23",
"cross-env": "10.1.0",
"eslint": "9.39.2",
"eslint-plugin-prettier": "5.5.4",
"eslint-plugin-vue": "10.6.2",
"globals": "16.5.0",
"postcss": "8.5.6", "postcss": "8.5.6",
"tailwindcss": "3.4.16", "tailwindcss": "3.4.16",
"typescript": "5.9.3", "typescript": "5.9.3",

View file

@ -5,21 +5,33 @@
<template v-if="O.isSome(state().otp)"> <template v-if="O.isSome(state().otp)">
<div class="flex-grow"> <div class="flex-grow">
<p class="tracking-wide"> <p class="tracking-wide">
An app is trying to register against the Hoppscotch Agent. If this was intentional, copy the given code into An app is trying to register against the Hoppscotch Agent. If this was
the app to complete the registration process. Please cancel the registration if you did not initiate this request. intentional, copy the given code into the app to complete the
The window will hide automatically once registration succeeds. If you minimize this window during registration, registration process. Please cancel the registration if you did not
you can access it again from the tray by selecting "Maximize Window". initiate this request. The window will hide automatically once
registration succeeds. If you minimize this window during
registration, you can access it again from the tray by selecting
"Maximize Window".
</p> </p>
<p <p
class="font-bold text-5xl tracking-wider text-center pt-10 text-white" class="font-bold text-5xl tracking-wider text-center pt-10 text-white"
>{{ pipe(state().otp, O.getOrElse(() => "")) }}</p> >
{{
pipe(
state().otp,
O.getOrElse(() => "")
)
}}
</p>
</div> </div>
</template> </template>
<template v-else> <template v-else>
<div class="flex-grow overflow-auto"> <div class="flex-grow overflow-auto">
<HoppSmartTable :headings="tableHeadings" :list="state().registrations"> <HoppSmartTable :headings="tableHeadings" :list="state().registrations">
<template #registered_at="{ item }">{{ formatDate(String(item.registered_at)) }}</template> <template #registered_at="{ item }">{{
formatDate(String(item.registered_at))
}}</template>
</HoppSmartTable> </HoppSmartTable>
</div> </div>
</template> </template>
@ -65,9 +77,9 @@ import {
HoppButtonSecondary, HoppButtonSecondary,
HoppSmartTable, HoppSmartTable,
} from "@hoppscotch/ui" } from "@hoppscotch/ui"
// @ts-ignore // @ts-expect-error - Icon import has no types
import IconCopy from "~icons/lucide/copy" import IconCopy from "~icons/lucide/copy"
// @ts-ignore // @ts-expect-error - Icon import has no types
import IconCheck from "~icons/lucide/check" import IconCheck from "~icons/lucide/check"
import { useClipboard, refAutoReset } from "@vueuse/core" import { useClipboard, refAutoReset } from "@vueuse/core"
import { getCurrentWindow } from "@tauri-apps/api/window" import { getCurrentWindow } from "@tauri-apps/api/window"
@ -183,7 +195,7 @@ onMounted(async () => {
if (otp) { if (otp) {
appState.value = { ...state(), otp: O.some(otp) } appState.value = { ...state(), otp: O.some(otp) }
} else { } else {
updateRegistrations(); updateRegistrations()
} }
}) })
)() )()
@ -204,12 +216,12 @@ onMounted(async () => {
getOtp, getOtp,
TE.map((otp: string) => { TE.map((otp: string) => {
if (otp) { if (otp) {
appState.value = { ...state(), otp: O.some(otp) }; appState.value = { ...state(), otp: O.some(otp) }
} else { } else {
updateRegistrations(); updateRegistrations()
} }
}) })
)(); )()
}), }),
]) ])
}) })

View file

@ -1,6 +1,6 @@
import { createApp } from 'vue' import { createApp } from "vue"
import App from './App.vue' import App from "./App.vue"
import './index.css' import "./index.css"
import { plugin as HoppUI } from "@hoppscotch/ui" import { plugin as HoppUI } from "@hoppscotch/ui"
@ -8,6 +8,4 @@ import "@hoppscotch/ui/themes.css"
import "@hoppscotch/ui/style.css" import "@hoppscotch/ui/style.css"
createApp(App) createApp(App).use(HoppUI).mount("#app")
.use(HoppUI)
.mount('#app')

View file

@ -3,17 +3,21 @@
<h1 class="font-bold text-lg text-white">Agent Registration Request</h1> <h1 class="font-bold text-lg text-white">Agent Registration Request</h1>
<div v-if="otpCode"> <div v-if="otpCode">
<p class="tracking-wide"> <p class="tracking-wide">
An app is trying to register against the Hoppscotch Agent. If this was intentional, copy the given code into An app is trying to register against the Hoppscotch Agent. If this was
the app to complete the registration process. Please hide the window if you did not initiate this request. intentional, copy the given code into the app to complete the
Do not hide this window until the verification code is entered. The window will hide automatically once done. registration process. Please hide the window if you did not initiate
this request. Do not hide this window until the verification code is
entered. The window will hide automatically once done.
</p>
<p class="font-bold text-5xl tracking-wider text-center pt-10 text-white">
{{ otpCode }}
</p> </p>
<p class="font-bold text-5xl tracking-wider text-center pt-10 text-white">{{ otpCode }}</p>
</div> </div>
<div v-else class="text-center pt-10"> <div v-else class="text-center pt-10">
<p class="tracking-wide">Waiting for registration requests...</p> <p class="tracking-wide">Waiting for registration requests...</p>
<p <p class="text-sm text-gray-400 mt-2">
class="text-sm text-gray-400 mt-2" You can hide this window and access it again from the tray icon.
>You can hide this window and access it again from the tray icon.</p> </p>
</div> </div>
<div class="border-t border-divider p-5 flex justify-between"> <div class="border-t border-divider p-5 flex justify-between">
<HoppButtonSecondary <HoppButtonSecondary
@ -35,10 +39,12 @@
</div> </div>
</template> </template>
<script setup> <script setup lang="ts">
import { ref, markRaw, onMounted } from "vue" import { ref, markRaw, onMounted } from "vue"
import { HoppButtonPrimary, HoppButtonSecondary } from "@hoppscotch/ui" import { HoppButtonSecondary } from "@hoppscotch/ui"
// @ts-expect-error - Icon import has no types
import IconCopy from "~icons/lucide/copy" import IconCopy from "~icons/lucide/copy"
// @ts-expect-error - Icon import has no types
import IconCheck from "~icons/lucide/check" import IconCheck from "~icons/lucide/check"
import { useClipboard, refAutoReset } from "@vueuse/core" import { useClipboard, refAutoReset } from "@vueuse/core"
import { getCurrentWindow } from "@tauri-apps/api/window" import { getCurrentWindow } from "@tauri-apps/api/window"
@ -64,12 +70,12 @@ onMounted(async () => {
const currentWindow = getCurrentWindow() const currentWindow = getCurrentWindow()
currentWindow.setAlwaysOnTop(true) currentWindow.setAlwaysOnTop(true)
const initialOtp = await invoke("get_otp", {}) const initialOtp = await invoke<string>("get_otp", {})
if (initialOtp) { if (initialOtp) {
otpCode.value = initialOtp otpCode.value = initialOtp
} }
await listen("registration-received", (event) => { await listen<string>("registration-received", (event) => {
otpCode.value = event.payload otpCode.value = event.payload
currentWindow.setFocus() currentWindow.setFocus()
}) })

View file

@ -9,7 +9,9 @@
]" ]"
:list="registrations" :list="registrations"
> >
<template #registered_at="{ item }">{{ formatDate(item.registered_at) }}</template> <template #registered_at="{ item }">{{
formatDate(item.registered_at)
}}</template>
</HoppSmartTable> </HoppSmartTable>
</div> </div>
<div class="border-t border-divider p-5 flex justify-between"> <div class="border-t border-divider p-5 flex justify-between">
@ -18,17 +20,26 @@
</div> </div>
</template> </template>
<script setup> <script setup lang="ts">
import { ref, markRaw, onMounted } from "vue" import { ref, onMounted } from "vue"
import { HoppButtonPrimary, HoppSmartTable } from "@hoppscotch/ui" import { HoppButtonPrimary, HoppSmartTable } from "@hoppscotch/ui"
import { getCurrentWindow } from "@tauri-apps/api/window" import { getCurrentWindow } from "@tauri-apps/api/window"
import { invoke } from "@tauri-apps/api/core" import { invoke } from "@tauri-apps/api/core"
import { listen } from "@tauri-apps/api/event" import { listen } from "@tauri-apps/api/event"
import { orderBy } from "lodash-es" import { orderBy } from "lodash-es"
const registrations = ref([]) interface Registration {
auth_key_hash: string
registered_at: string
}
function formatDate(date) { interface ListRegistrationsResult {
registrations: Registration[]
}
const registrations = ref<Registration[]>([])
function formatDate(date: string): string {
return new Date(date).toLocaleString() return new Date(date).toLocaleString()
} }
@ -38,7 +49,7 @@ function hideWindow() {
} }
async function loadRegistrations() { async function loadRegistrations() {
const result = await invoke("list_registrations", {}) const result = await invoke<ListRegistrationsResult>("list_registrations", {})
registrations.value = orderBy(result.registrations, "registered_at", "desc") registrations.value = orderBy(result.registrations, "registered_at", "desc")
} }

View file

@ -1,83 +0,0 @@
/* eslint-env node */
require("@rushstack/eslint-patch/modern-module-resolution")
module.exports = {
root: true,
env: {
browser: true,
node: true,
},
parserOptions: {
sourceType: "module",
requireConfigFile: false,
ecmaFeatures: {
jsx: false,
},
},
extends: [
"@vue/typescript/recommended",
"plugin:vue/recommended",
"plugin:prettier/recommended",
],
ignorePatterns: [
"static/**/*",
"./helpers/backend/graphql.ts",
"**/*.d.ts",
"types/**/*",
],
plugins: ["vue", "prettier"],
// add your custom rules here
rules: {
semi: [2, "never"],
"import/named": "off", // because, named import issue with typescript see: https://github.com/typescript-eslint/typescript-eslint/issues/154
"no-console": "off",
"no-debugger": process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
"prettier/prettier": [
process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
{
semi: false,
trailingComma: "es5",
singleQuote: false,
printWidth: 80,
useTabs: false,
tabWidth: 2,
},
],
"vue/multi-word-component-names": "off",
"vue/no-side-effects-in-computed-properties": "off",
"import/no-named-as-default": "off",
"import/no-named-as-default-member": "off",
"@typescript-eslint/no-unused-vars": [
process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-explicit-any": "off",
"import/default": "off",
"no-undef": "off",
// localStorage block
"no-restricted-globals": [
"error",
{
name: "localStorage",
message:
"Do not use 'localStorage' directly. Please use the PersistenceService",
},
],
// window.localStorage block
"no-restricted-syntax": [
"error",
{
selector: "CallExpression[callee.object.property.name='localStorage']",
message:
"Do not use 'localStorage' directly. Please use the PersistenceService",
},
],
eqeqeq: 1,
"no-else-return": 1,
},
}

View file

@ -0,0 +1,96 @@
import pluginVue from "eslint-plugin-vue"
import {
defineConfigWithVueTs,
vueTsConfigs,
} from "@vue/eslint-config-typescript"
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"
import globals from "globals"
export default defineConfigWithVueTs(
{
ignores: [
"static/**",
"src/helpers/backend/graphql.ts",
"**/*.d.ts",
"types/**",
"dist/**",
"node_modules/**",
],
},
pluginVue.configs["flat/recommended"],
vueTsConfigs.recommended,
eslintPluginPrettierRecommended,
{
files: ["**/*.ts", "**/*.js", "**/*.vue"],
linterOptions: {
reportUnusedDisableDirectives: false,
},
languageOptions: {
sourceType: "module",
ecmaVersion: "latest",
globals: {
...globals.browser,
...globals.node,
},
parserOptions: {
requireConfigFile: false,
ecmaFeatures: {
jsx: false,
},
},
},
rules: {
semi: [2, "never"],
"import/named": "off",
"no-console": "off",
"no-debugger": process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
"prettier/prettier": [
process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
{
semi: false,
trailingComma: "es5",
singleQuote: false,
printWidth: 80,
useTabs: false,
tabWidth: 2,
},
],
"vue/multi-word-component-names": "off",
"vue/no-side-effects-in-computed-properties": "off",
"import/no-named-as-default": "off",
"import/no-named-as-default-member": "off",
"@typescript-eslint/no-unused-vars": [
process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
"@typescript-eslint/no-unused-expressions": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unsafe-function-type": "off",
"import/default": "off",
"no-undef": "off",
"no-restricted-globals": [
"error",
{
name: "localStorage",
message:
"Do not use 'localStorage' directly. Please use the PersistenceService",
},
],
"no-restricted-syntax": [
"error",
{
selector: "CallExpression[callee.object.property.name='localStorage']",
message:
"Do not use 'localStorage' directly. Please use the PersistenceService",
},
],
eqeqeq: 1,
"no-else-return": 1,
},
}
)

View file

@ -8,10 +8,10 @@
"test:watch": "vitest", "test:watch": "vitest",
"dev:vite": "vite", "dev:vite": "vite",
"dev:gql-codegen": "graphql-codegen --require dotenv/config --config gql-codegen.yml --watch dotenv_config_path=\"../../.env\"", "dev:gql-codegen": "graphql-codegen --require dotenv/config --config gql-codegen.yml --watch dotenv_config_path=\"../../.env\"",
"lint": "eslint src --ext .ts,.js,.vue --ignore-path .gitignore .", "lint": "eslint src",
"lint:ts": "vue-tsc --noEmit", "lint:ts": "vue-tsc --noEmit",
"prod-lint": "cross-env HOPP_LINT_FOR_PROD=true pnpm run lint", "prod-lint": "cross-env HOPP_LINT_FOR_PROD=true pnpm run lint",
"lintfix": "eslint --fix src --ext .ts,.js,.vue --ignore-path .gitignore .", "lintfix": "eslint --fix src",
"preview": "vite preview", "preview": "vite preview",
"gql-codegen": "graphql-codegen --require dotenv/config --config gql-codegen.yml dotenv_config_path=\"../../.env\"", "gql-codegen": "graphql-codegen --require dotenv/config --config gql-codegen.yml dotenv_config_path=\"../../.env\"",
"postinstall": "pnpm run gql-codegen", "postinstall": "pnpm run gql-codegen",
@ -127,6 +127,8 @@
"devDependencies": { "devDependencies": {
"@esbuild-plugins/node-globals-polyfill": "0.2.3", "@esbuild-plugins/node-globals-polyfill": "0.2.3",
"@esbuild-plugins/node-modules-polyfill": "0.2.2", "@esbuild-plugins/node-modules-polyfill": "0.2.2",
"@eslint/eslintrc": "3.3.3",
"@eslint/js": "9.39.2",
"@graphql-codegen/add": "6.0.0", "@graphql-codegen/add": "6.0.0",
"@graphql-codegen/cli": "6.1.0", "@graphql-codegen/cli": "6.1.0",
"@graphql-codegen/typed-document-node": "6.1.5", "@graphql-codegen/typed-document-node": "6.1.5",
@ -153,15 +155,16 @@
"@typescript-eslint/parser": "8.50.0", "@typescript-eslint/parser": "8.50.0",
"@vitejs/plugin-vue": "6.0.3", "@vitejs/plugin-vue": "6.0.3",
"@vue/compiler-sfc": "3.5.26", "@vue/compiler-sfc": "3.5.26",
"@vue/eslint-config-typescript": "13.0.0", "@vue/eslint-config-typescript": "14.6.0",
"@vue/runtime-core": "3.5.26", "@vue/runtime-core": "3.5.26",
"autoprefixer": "10.4.23", "autoprefixer": "10.4.23",
"cross-env": "10.1.0", "cross-env": "10.1.0",
"dotenv": "17.2.3", "dotenv": "17.2.3",
"eslint": "8.57.0", "eslint": "9.39.2",
"eslint-plugin-prettier": "5.5.4", "eslint-plugin-prettier": "5.5.4",
"eslint-plugin-vue": "10.6.2", "eslint-plugin-vue": "10.6.2",
"glob": "13.0.0", "glob": "13.0.0",
"globals": "16.5.0",
"jsdom": "27.3.0", "jsdom": "27.3.0",
"npm-run-all": "4.1.5", "npm-run-all": "4.1.5",
"openapi-types": "12.1.3", "openapi-types": "12.1.3",

View file

@ -97,7 +97,7 @@ const fetchAccessTokens = async () => {
if (tokensListFetchErrored.value) { if (tokensListFetchErrored.value) {
tokensListFetchErrored.value = false tokensListFetchErrored.value = false
} }
} catch (err) { } catch (_err) {
toast.error(t("error.fetching_access_tokens_list")) toast.error(t("error.fetching_access_tokens_list"))
tokensListFetchErrored.value = true tokensListFetchErrored.value = true
} finally { } finally {
@ -136,7 +136,7 @@ const generateAccessToken = async ({
if (tokensListFetchErrored.value) { if (tokensListFetchErrored.value) {
tokensListFetchErrored.value = false tokensListFetchErrored.value = false
} }
} catch (err) { } catch (_err) {
toast.error(t("error.generate_access_token")) toast.error(t("error.generate_access_token"))
showAccessTokensGenerateModal.value = false showAccessTokensGenerateModal.value = false
} finally { } finally {
@ -175,7 +175,7 @@ const deleteAccessToken = async () => {
if (tokensListFetchErrored.value) { if (tokensListFetchErrored.value) {
tokensListFetchErrored.value = false tokensListFetchErrored.value = false
} }
} catch (err) { } catch (_err) {
toast.error(t("error.delete_access_token")) toast.error(t("error.delete_access_token"))
} finally { } finally {
tokenDeleteActionLoading.value = false tokenDeleteActionLoading.value = false

View file

@ -60,7 +60,6 @@ watch(
doc: props.contentRight.content, doc: props.contentRight.content,
extensions: [jsonLanguage, baseTheme, basicSetup], extensions: [jsonLanguage, baseTheme, basicSetup],
}, },
// @ts-expect-error attribute mismatch
parent: diffEditor.value, parent: diffEditor.value,
highlightChanges: false, highlightChanges: false,
}) })

View file

@ -58,7 +58,7 @@ const request = computed(() => {
return pathFolders.value[pathFolders.value.length - 1].requests[ return pathFolders.value[pathFolders.value.length - 1].requests[
requestIndex requestIndex
] ]
} catch (e) { } catch (_e) {
return null return null
} }
}) })

View file

@ -64,7 +64,7 @@ const request = computed(() => {
return pathFolders.value[pathFolders.value.length - 1].requests[ return pathFolders.value[pathFolders.value.length - 1].requests[
requestIndex requestIndex
] ]
} catch (e) { } catch (_e) {
return null return null
} }
}) })

View file

@ -253,7 +253,7 @@ const countPostmanScripts = (
if (collection?.item && Array.isArray(collection.item)) { if (collection?.item && Array.isArray(collection.item)) {
collection.item.forEach(countInItem) collection.item.forEach(countInItem)
} }
} catch (e) { } catch (_e) {
// Invalid JSON, skip // Invalid JSON, skip
} }
}) })
@ -337,7 +337,7 @@ const HoppAllCollectionImporter: ImporterOrExporter = {
exporter: "import_to_teams", exporter: "import_to_teams",
platform: "rest", platform: "rest",
}) })
} catch (e) { } catch (_e) {
showImportFailedError() showImportFailedError()
unsetCurrentImportSummary() unsetCurrentImportSummary()
} }

View file

@ -342,7 +342,7 @@ const fetchTeamCollection = async () => {
props.collection as TeamCollection props.collection as TeamCollection
) )
} }
} catch (error) { } catch (_error) {
fullCollectionData.value = teamCollToHoppRESTColl( fullCollectionData.value = teamCollToHoppRESTColl(
props.collection as TeamCollection props.collection as TeamCollection
) )

View file

@ -81,7 +81,7 @@ function formatJSON(jsonString: string): string {
try { try {
const parsed = JSON.parse(jsonString || "{}") const parsed = JSON.parse(jsonString || "{}")
return JSON.stringify(parsed, null, 2) return JSON.stringify(parsed, null, 2)
} catch (e) { } catch (_e) {
return jsonString || "" return jsonString || ""
} }
} }
@ -94,7 +94,7 @@ function formatJSON(jsonString: string): string {
function parseFormData(formData: string): { key: string; value: string }[] { function parseFormData(formData: string): { key: string; value: string }[] {
try { try {
return typeof formData === "string" ? parseRawKeyValueEntries(formData) : [] return typeof formData === "string" ? parseRawKeyValueEntries(formData) : []
} catch (e) { } catch (_e) {
return [] return []
} }
} }

View file

@ -212,7 +212,7 @@ function isJsonResponse(example: ResponseExample): boolean {
try { try {
JSON.parse(example.body || "") JSON.parse(example.body || "")
return true return true
} catch (e) { } catch (_e) {
return false return false
} }
} }
@ -226,7 +226,7 @@ function formatJSON(jsonString: string): string {
try { try {
const parsed = JSON.parse(jsonString || "{}") const parsed = JSON.parse(jsonString || "{}")
return JSON.stringify(parsed, null, 2) return JSON.stringify(parsed, null, 2)
} catch (e) { } catch (_e) {
return jsonString || "" return jsonString || ""
} }
} }

View file

@ -174,7 +174,7 @@ const debouncedOnUpdateQueryState = debounce((update: ViewUpdate) => {
const { start, end } = def.loc! const { start, end } = def.loc!
return selectedPos >= start && selectedPos <= end return selectedPos >= start && selectedPos <= end
}) as OperationDefinitionNode) ?? null }) as OperationDefinitionNode) ?? null
} catch (error) { } catch (_error) {
if (queryString.trim() === "") { if (queryString.trim() === "") {
operationDefinitions.value = [] operationDefinitions.value = []
} }
@ -193,7 +193,7 @@ onMounted(() => {
selectedOperation.value = ast.definitions[0] as OperationDefinitionNode selectedOperation.value = ast.definitions[0] as OperationDefinitionNode
return return
} }
} catch (error) {} } catch (_error) {}
}) })
const cmQueryEditor = useCodemirror( const cmQueryEditor = useCodemirror(
@ -238,7 +238,7 @@ const prettifyQuery = () => {
}) })
) )
prettifyQueryIcon.value = IconCheck prettifyQueryIcon.value = IconCheck
} catch (e) { } catch (_e) {
toast.error(`${t("error.gql_prettify_invalid_query")}`) toast.error(`${t("error.gql_prettify_invalid_query")}`)
prettifyQueryIcon.value = IconInfo prettifyQueryIcon.value = IconInfo
} }

View file

@ -77,14 +77,14 @@ import { platform } from "~/platform"
import { KernelInterceptorService } from "~/services/kernel-interceptor.service" import { KernelInterceptorService } from "~/services/kernel-interceptor.service"
import { GQLTabService } from "~/services/tab/graphql" import { GQLTabService } from "~/services/tab/graphql"
const VALID_GQL_OPERATIONS = [ const _VALID_GQL_OPERATIONS = [
"query", "query",
"headers", "headers",
"variables", "variables",
"authorization", "authorization",
] as const ] as const
export type GQLOptionTabs = (typeof VALID_GQL_OPERATIONS)[number] export type GQLOptionTabs = (typeof _VALID_GQL_OPERATIONS)[number]
const interceptorService = useService(KernelInterceptorService) const interceptorService = useService(KernelInterceptorService)

View file

@ -600,7 +600,7 @@ const saveRequest = async () => {
}) })
toast.success(`${t("request.saved")}`) toast.success(`${t("request.saved")}`)
} catch (e) { } catch (_e) {
tab.value.document.saveContext = undefined tab.value.document.saveContext = undefined
saveRequest() saveRequest()
} }

View file

@ -110,7 +110,7 @@ import { defineActionHandler } from "~/helpers/actions"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties" import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { AggregateEnvironment } from "~/newstore/environments" import { AggregateEnvironment } from "~/newstore/environments"
const VALID_OPTION_TABS = [ const _VALID_OPTION_TABS = [
"params", "params",
"bodyParams", "bodyParams",
"headers", "headers",
@ -120,7 +120,7 @@ const VALID_OPTION_TABS = [
"requestVariables", "requestVariables",
] as const ] as const
export type RESTOptionTabs = (typeof VALID_OPTION_TABS)[number] export type RESTOptionTabs = (typeof _VALID_OPTION_TABS)[number]
const t = useI18n() const t = useI18n()

View file

@ -172,7 +172,7 @@ function getCurrentPageCategory(): "graphql" | "rest" | "other" {
return "rest" return "rest"
} }
return "other" return "other"
} catch (e) { } catch (_e) {
return "other" return "other"
} }
} }

View file

@ -168,7 +168,7 @@ const retryWithProxy = async () => {
} else { } else {
toast.error(t("import.failed")) toast.error(t("import.failed"))
} }
} catch (error) { } catch (_error) {
toast.error(t("import.failed")) toast.error(t("import.failed"))
} finally { } finally {
isFetchingUrl.value = false isFetchingUrl.value = false

View file

@ -73,7 +73,7 @@ async function register() {
try { try {
await agentService.initiateRegistration() await agentService.initiateRegistration()
registrationStatus.value = "otp_required" registrationStatus.value = "otp_required"
} catch (error) { } catch (_error) {
toast.error("Failed to initiate registration. Please try again.") toast.error("Failed to initiate registration. Please try again.")
registrationStatus.value = "initial" registrationStatus.value = "initial"
} }
@ -85,7 +85,7 @@ async function verifyOTP(otp: string) {
await agentService.verifyRegistration(otp) await agentService.verifyRegistration(otp)
toast.success("Registration successful!") toast.success("Registration successful!")
hideModal() hideModal()
} catch (error) { } catch (_error) {
toast.error("Failed to verify OTP. Please try again.") toast.error("Failed to verify OTP. Please try again.")
registrationStatus.value = "otp_required" registrationStatus.value = "otp_required"
} }

View file

@ -294,7 +294,7 @@ const copyToClipboardHandler = async (text: string) => {
setTimeout(() => { setTimeout(() => {
copyIcon.value = IconCopy copyIcon.value = IconCopy
}, 1000) }, 1000)
} catch (error) { } catch (_error) {
toast.error(t("error.copy_failed")) toast.error(t("error.copy_failed"))
} }
} }

View file

@ -354,7 +354,7 @@ const copyToClipboardHandler = async (text: string) => {
setTimeout(() => { setTimeout(() => {
copyIcon.value = IconCopy copyIcon.value = IconCopy
}, 1000) }, 1000)
} catch (error) { } catch (_error) {
toast.error(t("error.copy_failed")) toast.error(t("error.copy_failed"))
} }
} }

View file

@ -668,7 +668,7 @@ async function updateMaskedAuthKey() {
try { try {
const registration = await store.fetchRegistrationInfo() const registration = await store.fetchRegistrationInfo()
maskedAuthKey.value = registration.auth_key_hash maskedAuthKey.value = registration.auth_key_hash
} catch (e) { } catch (_e) {
toast.error(t("settings.agent_registration_fetch_failed")) toast.error(t("settings.agent_registration_fetch_failed"))
} }
} }

View file

@ -126,7 +126,7 @@ const register = async () => {
await updateMaskedAuthKey() await updateMaskedAuthKey()
toast.success(t("settings.agent_registration_successful")) toast.success(t("settings.agent_registration_successful"))
store.registrationOTP.value = "" store.registrationOTP.value = ""
} catch (e) { } catch (_e) {
} finally { } finally {
store.isRegistering.value = false store.isRegistering.value = false
} }
@ -146,7 +146,7 @@ const updateMaskedAuthKey = async () => {
try { try {
const registration = await store.fetchRegistrationInfo() const registration = await store.fetchRegistrationInfo()
store.maskedAuthKey.value = registration.auth_key_hash store.maskedAuthKey.value = registration.auth_key_hash
} catch (e) {} } catch (_e) {}
} }
onMounted(async () => { onMounted(async () => {

View file

@ -13,7 +13,7 @@ export function initializeApp() {
platform.analytics?.initAnalytics() platform.analytics?.initAnalytics()
initialized = true initialized = true
} catch (e) { } catch (_e) {
// initializeApp throws exception if we reinitialize // initializeApp throws exception if we reinitialize
initialized = true initialized = true
} }

View file

@ -45,7 +45,7 @@ type TypeFromPrimitiveArray<P extends JSPrimitive | undefined> =
: P extends "symbol" : P extends "symbol"
? symbol[] ? symbol[]
: P extends "function" : P extends "function"
? Function[] // eslint-disable-line @typescript-eslint/ban-types ? Function[]
: unknown[] : unknown[]
// The ban-types silence is because in this case, // The ban-types silence is because in this case,

View file

@ -24,7 +24,7 @@ export type TypeFromPrimitive<P extends JSPrimitive | undefined> =
: P extends "symbol" : P extends "symbol"
? symbol ? symbol
: P extends "function" : P extends "function"
? Function // eslint-disable-line @typescript-eslint/ban-types ? Function
: unknown : unknown
// The ban-types silence is because in this case, // The ban-types silence is because in this case,

View file

@ -206,7 +206,7 @@ const parseOpenAPIV3Responses = (
try { try {
stringifiedBody = JSON.stringify(body ?? "") stringifiedBody = JSON.stringify(body ?? "")
// the parsing will fail for a circular response schema // the parsing will fail for a circular response schema
} catch (e) { } catch (_e) {
// eat five star, do nothing // eat five star, do nothing
} }
@ -435,7 +435,7 @@ const parseOpenAPIV3Body = (
? sampleBody ? sampleBody
: JSON.stringify(sampleBody, null, 2), : JSON.stringify(sampleBody, null, 2),
} }
} catch (e) { } catch (_e) {
// If we can't generate a sample, check for examples // If we can't generate a sample, check for examples
if (media.example !== undefined) { if (media.example !== undefined) {
return { return {
@ -1152,7 +1152,7 @@ export const hoppOpenAPIImporter = (fileContents: string[]) =>
try { try {
const validatedDoc = await dereferenceDocs(docObj) const validatedDoc = await dereferenceDocs(docObj)
resultDoc.push(validatedDoc) resultDoc.push(validatedDoc)
} catch (error) { } catch (_error) {
// Check if the document has unresolved references // Check if the document has unresolved references
if (hasUnresolvedRefs(docObj)) { if (hasUnresolvedRefs(docObj)) {
console.warn( console.warn(

View file

@ -12,12 +12,11 @@ import * as E from "fp-ts/Either"
const validateDocs = async (docs: any) => { const validateDocs = async (docs: any) => {
try { try {
const res = await SwaggerParser.validate(docs, { const res = await SwaggerParser.validate(docs, {
// @ts-expect-error - this is a valid option, but seems like the types are not updated
continueOnError: true, continueOnError: true,
}) })
return E.right(res) return E.right(res)
} catch (error) { } catch (_error) {
return E.left("COULD_NOT_VALIDATE" as const) return E.left("COULD_NOT_VALIDATE" as const)
} }
} }
@ -27,7 +26,7 @@ const dereferenceDocs = async (docs: any) => {
const res = await SwaggerParser.dereference(docs) const res = await SwaggerParser.dereference(docs)
return E.right(res) return E.right(res)
} catch (error) { } catch (_error) {
return E.left("COULD_NOT_DEREFERENCE" as const) return E.left("COULD_NOT_DEREFERENCE" as const)
} }
} }

View file

@ -61,7 +61,7 @@ export const isValidUser = async (): Promise<ValidUserResponse> => {
// For platforms without token verification capability // For platforms without token verification capability
return { valid: true, error: "" } return { valid: true, error: "" }
} catch (error) { } catch (_error) {
// Handle errors from token verification // Handle errors from token verification
return attemptTokenRefresh() return attemptTokenRefresh()
} }

View file

@ -97,7 +97,7 @@ export default function jsonParse(
const ast = parseObj() const ast = parseObj()
expect("EOF") expect("EOF")
return ast return ast
} catch (e) { } catch (_e) {
// Try parsing expecting a root array // Try parsing expecting a root array
const ast = parseArr() const ast = parseArr()
expect("EOF") expect("EOF")

View file

@ -120,7 +120,7 @@ export default function jsonParse(
const ast = parseObj() const ast = parseObj()
expect("EOF") expect("EOF")
return ast return ast
} catch (e) { } catch (_e) {
pendingComments = [] // Reset pending comments pendingComments = [] // Reset pending comments
const ast = parseArr() const ast = parseArr()
expect("EOF") expect("EOF")

View file

@ -22,7 +22,7 @@ export function isValidJSONResponse(contents: string | ArrayBuffer): boolean {
try { try {
JSON.parse(resolvedStr) JSON.parse(resolvedStr)
return true return true
} catch (e) { } catch (_e) {
return false return false
} }
} }

View file

@ -71,7 +71,7 @@ export function createRESTNetworkRequestStream(
try { try {
const result = await execResult const result = await execResult
if (result) await result.cancel() if (result) await result.cancel()
} catch (error) { } catch (_error) {
// Ignore cancel errors - request may have already completed // Ignore cancel errors - request may have already completed
// This is expected behavior and not an actual error // This is expected behavior and not an actual error
} }

View file

@ -94,7 +94,7 @@ export function getJSONOutlineAtPos(
} }
return path return path
} catch (e: any) { } catch (_e: any) {
return null return null
} }
} }

View file

@ -195,7 +195,7 @@ export class MQTTConnection {
message, message,
}, },
}) })
} catch (e) { } catch (_e) {
this.addEvent({ this.addEvent({
time: Date.now(), time: Date.now(),
type: "ERROR", type: "ERROR",
@ -216,7 +216,7 @@ export class MQTTConnection {
onFailure: this.usubFailure.bind(this, topic.name), onFailure: this.usubFailure.bind(this, topic.name),
qos: topic.qos, qos: topic.qos,
}) })
} catch (e) { } catch (_e) {
this.subscribing$.next(false) this.subscribing$.next(false)
this.addEvent({ this.addEvent({
time: Date.now(), time: Date.now(),

View file

@ -365,7 +365,7 @@ function makeVisitors(server, query, file, messages) {
infer.findRefs(cur.ast, cur.scope, name, scope, searchRef(cur)) infer.findRefs(cur.ast, cur.scope, name, scope, searchRef(cur))
} }
} }
} catch (e) {} } catch (_e) {}
return hasRef return hasRef
} }

View file

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

View file

@ -174,7 +174,7 @@ export function loadMockServers(skip?: number, take?: number) {
} }
) )
)() )()
} catch (error) { } catch (_error) {
// Fallback to user mock servers if workspace service is not available // Fallback to user mock servers if workspace service is not available
return pipe( return pipe(
getMyMockServers(skip, take), getMyMockServers(skip, take),

View file

@ -289,8 +289,8 @@ export function applySetting<K extends keyof SettingsDef>(
) { ) {
settingsStore.dispatch({ settingsStore.dispatch({
dispatcher: "applySetting", dispatcher: "applySetting",
payload: {
// @ts-expect-error TS is not able to understand the type semantics here // @ts-expect-error TS is not able to understand the type semantics here
payload: {
settingKey, settingKey,
value, value,
}, },

View file

@ -328,7 +328,7 @@ export class AgentInterceptorService extends Service implements Interceptor {
try { try {
const proxyInfo = JSON.parse(persistedProxyInfo) const proxyInfo = JSON.parse(persistedProxyInfo)
this.proxyInfo.value = proxyInfo this.proxyInfo.value = proxyInfo
} catch (e) {} } catch (_e) {}
} }
// Load SSL Validation // Load SSL Validation
@ -557,7 +557,7 @@ export class AgentInterceptorService extends Service implements Interceptor {
try { try {
await this.performHandshake() await this.performHandshake()
this.isAgentRunning.value = true this.isAgentRunning.value = true
} catch (error) { } catch (_error) {
this.isAgentRunning.value = false this.isAgentRunning.value = false
} }
} }

View file

@ -6,7 +6,7 @@ import { useSetting } from "~/composables/settings"
const isEncoded = (value: string) => { const isEncoded = (value: string) => {
try { try {
return value !== decodeURIComponent(value) return value !== decodeURIComponent(value)
} catch (e) { } catch (_e) {
return false // in case of malformed URI sequence return false // in case of malformed URI sequence
} }
} }
@ -42,7 +42,7 @@ export const preProcessRequest = (
// decode the URL to prevent double encoding // decode the URL to prevent double encoding
reqClone.url = decodeURIComponent(url.toString()) reqClone.url = decodeURIComponent(url.toString())
} catch (e) { } catch (_e) {
// making this a non-empty block, so we can make the linter happy. // making this a non-empty block, so we can make the linter happy.
// we should probably use, allowEmptyCatch, or take the time to do something with the caught errors :) // we should probably use, allowEmptyCatch, or take the time to do something with the caught errors :)
} }

View file

@ -32,7 +32,7 @@ describe("URLMenuService", () => {
try { try {
new URL(url) new URL(url)
return true return true
} catch (error) { } catch (_error) {
// Fallback to regular expression check // Fallback to regular expression check
const pattern = /^(https?:\/\/)?([\w.-]+)(\.[\w.-]+)+([/?].*)?$/ const pattern = /^(https?:\/\/)?([\w.-]+)(\.[\w.-]+)+([/?].*)?$/
return pattern.test(url) return pattern.test(url)

View file

@ -22,7 +22,7 @@ function isValidURL(url: string) {
// this will fail for endpoints like "localhost:3000", ie without a protocol // this will fail for endpoints like "localhost:3000", ie without a protocol
new URL(url) new URL(url)
return true return true
} catch (error) { } catch (_error) {
// Fallback to regular expression check // Fallback to regular expression check
const pattern = /^(https?:\/\/)?([\w.-]+)(\.[\w.-]+)+([/?].*)?$/ const pattern = /^(https?:\/\/)?([\w.-]+)(\.[\w.-]+)+([/?].*)?$/
return pattern.test(url) return pattern.test(url)

View file

@ -185,7 +185,7 @@ const initAuthCodeOauthFlow = async ({
try { try {
url = new URL(authEndpoint) url = new URL(authEndpoint)
} catch (e) { } catch (_e) {
return E.left("INVALID_AUTH_ENDPOINT") return E.left("INVALID_AUTH_ENDPOINT")
} }

View file

@ -354,7 +354,7 @@ export class PersistenceService extends Service {
) )
} }
} }
} catch (e) { } catch (_e) {
console.error(`Failed parsing persisted LOCAL_STATE:`, loadResult) console.error(`Failed parsing persisted LOCAL_STATE:`, loadResult)
} }
@ -386,7 +386,7 @@ export class PersistenceService extends Service {
) )
} }
} }
} catch (e) { } catch (_e) {
console.error(`Failed parsing persisted SETTINGS:`, loadResult) console.error(`Failed parsing persisted SETTINGS:`, loadResult)
} }
@ -418,7 +418,7 @@ export class PersistenceService extends Service {
) )
} }
} }
} catch (e) { } catch (_e) {
console.error(`Failed parsing persisted REST_HISTORY:`, restLoadResult) console.error(`Failed parsing persisted REST_HISTORY:`, restLoadResult)
} }
@ -450,7 +450,7 @@ export class PersistenceService extends Service {
) )
} }
} }
} catch (e) { } catch (_e) {
console.error(`Failed parsing persisted GQL_HISTORY:`, gqlLoadResult) console.error(`Failed parsing persisted GQL_HISTORY:`, gqlLoadResult)
} }
@ -486,7 +486,7 @@ export class PersistenceService extends Service {
setRESTCollections(data) setRESTCollections(data)
} }
} }
} catch (e) { } catch (_e) {
console.error( console.error(
`Failed parsing persisted REST_COLLECTIONS:`, `Failed parsing persisted REST_COLLECTIONS:`,
restLoadResult restLoadResult
@ -523,7 +523,7 @@ export class PersistenceService extends Service {
setGraphqlCollections(data) setGraphqlCollections(data)
} }
} }
} catch (e) { } catch (_e) {
console.error(`Failed parsing persisted GQL_COLLECTIONS:`, gqlLoadResult) console.error(`Failed parsing persisted GQL_COLLECTIONS:`, gqlLoadResult)
} }
@ -569,7 +569,7 @@ export class PersistenceService extends Service {
) )
} }
} }
} catch (e) { } catch (_e) {
console.error(`Failed parsing persisted ENVIRONMENTS:`, loadResult) console.error(`Failed parsing persisted ENVIRONMENTS:`, loadResult)
} }
@ -607,7 +607,7 @@ export class PersistenceService extends Service {
) )
} }
} }
} catch (e) { } catch (_e) {
console.error(`Failed parsing persisted SECRET_ENVIRONMENTS:`, loadResult) console.error(`Failed parsing persisted SECRET_ENVIRONMENTS:`, loadResult)
} }
@ -653,7 +653,7 @@ export class PersistenceService extends Service {
) )
} }
} }
} catch (e) { } catch (_e) {
console.error( console.error(
`Failed parsing persisted CURRENT_ENVIRONMENT_VALUE:`, `Failed parsing persisted CURRENT_ENVIRONMENT_VALUE:`,
loadResult loadResult
@ -699,7 +699,7 @@ export class PersistenceService extends Service {
) )
} }
} }
} catch (e) { } catch (_e) {
console.error(`Failed parsing persisted SELECTED_ENV:`, loadResult) console.error(`Failed parsing persisted SELECTED_ENV:`, loadResult)
} }
@ -735,7 +735,7 @@ export class PersistenceService extends Service {
) )
} }
} }
} catch (e) { } catch (_e) {
console.error(`Failed parsing persisted CURRENT_SORT_VALUES:`, loadResult) console.error(`Failed parsing persisted CURRENT_SORT_VALUES:`, loadResult)
} }
@ -780,7 +780,7 @@ export class PersistenceService extends Service {
} }
} }
} }
} catch (e) { } catch (_e) {
console.error(`Failed parsing persisted WEBSOCKET:`, loadResult) console.error(`Failed parsing persisted WEBSOCKET:`, loadResult)
} }
@ -817,7 +817,7 @@ export class PersistenceService extends Service {
} }
} }
} }
} catch (e) { } catch (_e) {
console.error(`Failed parsing persisted SOCKETIO:`, loadResult) console.error(`Failed parsing persisted SOCKETIO:`, loadResult)
} }
@ -847,7 +847,7 @@ export class PersistenceService extends Service {
} }
} }
} }
} catch (e) { } catch (_e) {
console.error(`Failed parsing persisted SSE:`, loadResult) console.error(`Failed parsing persisted SSE:`, loadResult)
} }
@ -877,7 +877,7 @@ export class PersistenceService extends Service {
} }
} }
} }
} catch (e) { } catch (_e) {
console.error(`Failed parsing persisted MQTT:`, loadResult) console.error(`Failed parsing persisted MQTT:`, loadResult)
} }
@ -908,7 +908,7 @@ export class PersistenceService extends Service {
) )
} }
} }
} catch (e) { } catch (_e) {
console.error(`Failed parsing persisted GLOBAL_ENV:`, loadResult) console.error(`Failed parsing persisted GLOBAL_ENV:`, loadResult)
} }
@ -955,7 +955,7 @@ export class PersistenceService extends Service {
this.restTabService.loadTabsFromPersistedState(loadResult.right) this.restTabService.loadTabsFromPersistedState(loadResult.right)
} }
} }
} catch (e) { } catch (_e) {
console.error(`Failed parsing persisted REST_TABS:`, loadResult) console.error(`Failed parsing persisted REST_TABS:`, loadResult)
} }
@ -998,7 +998,7 @@ export class PersistenceService extends Service {
this.gqlTabService.loadTabsFromPersistedState(loadResult.right) this.gqlTabService.loadTabsFromPersistedState(loadResult.right)
} }
} }
} catch (e) { } catch (_e) {
console.error(`Failed parsing persisted GQL_TABS:`, loadResult) console.error(`Failed parsing persisted GQL_TABS:`, loadResult)
} }

View file

@ -119,7 +119,7 @@ export class CollectionsSpotlightSearcherService
return "rest" return "rest"
} }
return "other" return "other"
} catch (e) { } catch (_e) {
return "other" return "other"
} }
} }

View file

@ -54,7 +54,7 @@ export class TeamsSpotlightSearcherService
return "rest" return "rest"
} }
return "other" return "other"
} catch (e) { } catch (_e) {
return "other" return "other"
} }
} }

View file

@ -194,7 +194,7 @@ export class TestRunnerService extends Service {
if (options.delay && options.delay > 0) { if (options.delay && options.delay > 0) {
try { try {
await delay(options.delay) await delay(options.delay)
} catch (error) { } catch (_error) {
if (options.stopRef?.value) { if (options.stopRef?.value) {
tab.value.document.status = "stopped" tab.value.document.status = "stopped"
throw new Error("Test execution stopped") throw new Error("Test execution stopped")

View file

@ -1,67 +0,0 @@
/* eslint-env node */
require("@rushstack/eslint-patch/modern-module-resolution")
module.exports = {
root: true,
env: {
browser: true,
node: true,
jest: true,
},
parserOptions: {
sourceType: "module",
requireConfigFile: false,
ecmaFeatures: {
jsx: false,
},
},
extends: [
"@vue/typescript/recommended",
"plugin:vue/recommended",
"plugin:prettier/recommended",
],
ignorePatterns: [
"static/**/*",
"./helpers/backend/graphql.ts",
"**/*.d.ts",
"types/**/*",
],
plugins: ["vue", "prettier"],
// add your custom rules here
rules: {
semi: [2, "never"],
"import/named": "off", // because, named import issue with typescript see: https://github.com/typescript-eslint/typescript-eslint/issues/154
"no-console": "off",
"no-debugger": process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
"prettier/prettier":
process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
"vue/multi-word-component-names": "off",
"vue/no-side-effects-in-computed-properties": "off",
"import/no-named-as-default": "off",
"import/no-named-as-default-member": "off",
"@typescript-eslint/no-unused-vars":
process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-explicit-any": "off",
"import/default": "off",
"no-undef": "off",
// localStorage block
"no-restricted-globals": [
"error",
{
name: "localStorage",
message:
"Do not use 'localStorage' directly. Please use localpersistence.ts functions or stores",
},
],
// window.localStorage block
"no-restricted-syntax": [
"error",
{
selector: "CallExpression[callee.object.property.name='localStorage']",
message:
"Do not use 'localStorage' directly. Please use localpersistence.ts functions or stores",
},
],
},
}

View file

@ -0,0 +1,79 @@
import pluginVue from "eslint-plugin-vue"
import {
defineConfigWithVueTs,
vueTsConfigs,
} from "@vue/eslint-config-typescript"
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"
import globals from "globals"
export default defineConfigWithVueTs(
{
ignores: [
"static/**",
"src/helpers/backend/graphql.ts",
"**/*.d.ts",
"types/**",
"dist/**",
"node_modules/**",
],
},
pluginVue.configs["flat/recommended"],
vueTsConfigs.recommended,
eslintPluginPrettierRecommended,
{
files: ["**/*.ts", "**/*.js", "**/*.vue"],
linterOptions: {
reportUnusedDisableDirectives: false,
},
languageOptions: {
sourceType: "module",
ecmaVersion: "latest",
globals: {
...globals.browser,
...globals.node,
},
parserOptions: {
requireConfigFile: false,
ecmaFeatures: {
jsx: false,
},
},
},
rules: {
semi: [2, "never"],
"import/named": "off",
"no-console": "off",
"no-debugger": process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
"prettier/prettier":
process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
"vue/multi-word-component-names": "off",
"vue/no-side-effects-in-computed-properties": "off",
"import/no-named-as-default": "off",
"import/no-named-as-default-member": "off",
"@typescript-eslint/no-unused-vars":
process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
"@typescript-eslint/no-unused-expressions": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unsafe-function-type": "off",
"import/default": "off",
"no-undef": "off",
"no-restricted-globals": [
"error",
{
name: "localStorage",
message:
"Do not use 'localStorage' directly. Please use the PersistenceService",
},
],
"no-restricted-syntax": [
"error",
{
selector: "CallExpression[callee.object.property.name='localStorage']",
message:
"Do not use 'localStorage' directly. Please use the PersistenceService",
},
],
},
}
)

View file

@ -8,10 +8,13 @@
"build": "vue-tsc --noEmit && vite build", "build": "vue-tsc --noEmit && vite build",
"preview": "vite preview", "preview": "vite preview",
"tauri": "tauri", "tauri": "tauri",
"lint": "eslint src --ext .ts,.js,.vue --ignore-path .gitignore .", "lint": "eslint src",
"lint:ts": "vue-tsc --noEmit", "lint:ts": "vue-tsc --noEmit",
"lintfix": "eslint --fix src --ext .ts,.js,.vue --ignore-path .gitignore .", "lintfix": "eslint --fix src",
"prod-lint": "cross-env HOPP_LINT_FOR_PROD=true pnpm run lint", "prod-lint": "cross-env HOPP_LINT_FOR_PROD=true pnpm run lint",
"do-lint": "pnpm run prod-lint",
"do-typecheck": "pnpm run lint:ts",
"do-lintfix": "pnpm run lintfix",
"prepare-web": "(cd ../hoppscotch-selfhost-web && pnpm install && pnpm generate) && (cd crates/webapp-bundler && cargo build --release && cd target/release && ./webapp-bundler --input ../../../../../hoppscotch-selfhost-web/dist --output ../../../../bundle.zip --manifest ../../../../manifest.json)", "prepare-web": "(cd ../hoppscotch-selfhost-web && pnpm install && pnpm generate) && (cd crates/webapp-bundler && cargo build --release && cd target/release && ./webapp-bundler --input ../../../../../hoppscotch-selfhost-web/dist --output ../../../../bundle.zip --manifest ../../../../manifest.json)",
"dev:full": "pnpm tauri dev", "dev:full": "pnpm tauri dev",
"build:full": "pnpm tauri build", "build:full": "pnpm tauri build",
@ -20,7 +23,7 @@
}, },
"dependencies": { "dependencies": {
"@fontsource-variable/inter": "5.2.8", "@fontsource-variable/inter": "5.2.8",
"@fontsource-variable/material-symbols-rounded": "5.2.24", "@fontsource-variable/material-symbols-rounded": "5.2.30",
"@fontsource-variable/roboto-mono": "5.2.8", "@fontsource-variable/roboto-mono": "5.2.8",
"@hoppscotch/common": "workspace:^", "@hoppscotch/common": "workspace:^",
"@hoppscotch/kernel": "workspace:^", "@hoppscotch/kernel": "workspace:^",
@ -29,36 +32,38 @@
"@tauri-apps/api": "2.1.1", "@tauri-apps/api": "2.1.1",
"@tauri-apps/plugin-fs": "2.0.2", "@tauri-apps/plugin-fs": "2.0.2",
"@tauri-apps/plugin-process": "2.2.0", "@tauri-apps/plugin-process": "2.2.0",
"@tauri-apps/plugin-shell": "2.2.1", "@tauri-apps/plugin-shell": "2.3.3",
"@tauri-apps/plugin-store": "2.2.0", "@tauri-apps/plugin-store": "2.4.1",
"@tauri-apps/plugin-updater": "2.5.1", "@tauri-apps/plugin-updater": "2.9.0",
"@vueuse/core": "13.7.0",
"fp-ts": "2.16.11", "fp-ts": "2.16.11",
"rxjs": "7.8.2", "rxjs": "7.8.2",
"vue": "3.5.22", "vue": "3.5.26",
"vue-router": "4.6.3", "vue-router": "4.6.4",
"vue-tippy": "6.7.1", "vue-tippy": "6.7.1",
"zod": "3.25.32" "zod": "3.25.32"
}, },
"devDependencies": { "devDependencies": {
"@iconify-json/lucide": "1.2.68", "@eslint/eslintrc": "3.3.3",
"@rushstack/eslint-patch": "1.14.0", "@eslint/js": "9.39.2",
"@tauri-apps/cli": "^2", "@iconify-json/lucide": "1.2.81",
"@typescript-eslint/eslint-plugin": "8.44.1", "@rushstack/eslint-patch": "1.15.0",
"@typescript-eslint/parser": "8.44.1", "@tauri-apps/cli": "2.9.3",
"@vitejs/plugin-vue": "5.1.4", "@typescript-eslint/eslint-plugin": "8.50.0",
"@vue/eslint-config-typescript": "13.0.0", "@typescript-eslint/parser": "8.50.0",
"autoprefixer": "10.4.21", "@vitejs/plugin-vue": "6.0.3",
"eslint": "8.57.0", "@vue/eslint-config-typescript": "14.6.0",
"autoprefixer": "10.4.23",
"eslint": "9.39.2",
"eslint-plugin-prettier": "5.5.4", "eslint-plugin-prettier": "5.5.4",
"eslint-plugin-vue": "10.5.1", "eslint-plugin-vue": "10.6.2",
"globals": "16.5.0",
"postcss": "8.5.6", "postcss": "8.5.6",
"sass": "1.93.2", "sass": "1.97.0",
"tailwindcss": "3.4.16", "tailwindcss": "3.4.16",
"typescript": "5.9.3", "typescript": "5.9.3",
"unplugin-icons": "22.2.0", "unplugin-icons": "22.5.0",
"unplugin-vue-components": "29.0.0", "unplugin-vue-components": "30.0.0",
"vite": "6.3.5", "vite": "7.3.0",
"vue-tsc": "2.2.0" "vue-tsc": "2.2.0"
} }
} }

View file

@ -1,8 +1,11 @@
/* eslint-disable */ /* eslint-disable */
// @ts-nocheck // @ts-nocheck
// biome-ignore lint: disable
// oxlint-disable
// ------
// Generated by unplugin-vue-components // Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399 // Read more: https://github.com/vuejs/core/pull/3399
// biome-ignore lint: disable
export {} export {}
/* prettier-ignore */ /* prettier-ignore */

View file

@ -1,42 +0,0 @@
module.exports = {
root: true,
env: {
node: true,
jest: true,
browser: true,
},
parser: "@typescript-eslint/parser",
parserOptions: {
sourceType: "module",
requireConfigFile: false,
ecmaVersion: 2021,
},
plugins: ["prettier"],
extends: [
"prettier/prettier",
"eslint:recommended",
"plugin:prettier/recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
],
rules: {
semi: [2, "never"],
"prettier/prettier": ["warn", { semi: false, trailingComma: "es5" }],
"import/no-named-as-default": "off",
"no-undef": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
args: "all",
argsIgnorePattern: "^_",
caughtErrors: "all",
caughtErrorsIgnorePattern: "^_",
destructuredArrayIgnorePattern: "^_",
varsIgnorePattern: "^_",
ignoreRestSiblings: true,
},
],
},
}

View file

@ -0,0 +1,70 @@
const { FlatCompat } = require("@eslint/eslintrc")
const js = require("@eslint/js")
const tsParser = require("@typescript-eslint/parser")
const typescriptEslintPlugin = require("@typescript-eslint/eslint-plugin")
const prettierPlugin = require("eslint-plugin-prettier")
const globals = require("globals")
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
})
module.exports = [
{
ignores: [
"dist/**",
"node_modules/**",
"**/*.d.ts",
"eslint.config.cjs",
".prettierrc.cjs",
"src/bootstrap-code/**",
],
},
...compat.extends(
"eslint:recommended",
"plugin:prettier/recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
),
{
files: ["**/*.ts", "**/*.js"],
linterOptions: {
reportUnusedDisableDirectives: false,
},
languageOptions: {
parser: tsParser,
sourceType: "module",
ecmaVersion: 2021,
globals: {
...globals.node,
...globals.jest,
...globals.browser,
},
},
plugins: {
"@typescript-eslint": typescriptEslintPlugin,
prettier: prettierPlugin,
},
rules: {
semi: [2, "never"],
"prettier/prettier": ["warn", { semi: false, trailingComma: "es5" }],
"no-undef": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
args: "all",
argsIgnorePattern: "^_",
caughtErrors: "all",
caughtErrorsIgnorePattern: "^_",
destructuredArrayIgnorePattern: "^_",
varsIgnorePattern: "^_",
ignoreRestSiblings: true,
},
],
},
},
]

View file

@ -27,8 +27,8 @@
"node": ">=22" "node": ">=22"
}, },
"scripts": { "scripts": {
"lint": "eslint --ext .ts,.js --ignore-path .gitignore .", "lint": "eslint .",
"lintfix": "eslint --fix --ext .ts,.js --ignore-path .gitignore .", "lintfix": "eslint --fix .",
"test": "vitest run", "test": "vitest run",
"build": "vite build && tsc --emitDeclarationOnly", "build": "vite build && tsc --emitDeclarationOnly",
"clean": "pnpm tsc --build --clean", "clean": "pnpm tsc --build --clean",
@ -67,7 +67,10 @@
"@types/node": "24.10.1", "@types/node": "24.10.1",
"@typescript-eslint/eslint-plugin": "8.50.0", "@typescript-eslint/eslint-plugin": "8.50.0",
"@typescript-eslint/parser": "8.50.0", "@typescript-eslint/parser": "8.50.0",
"eslint": "8.57.0", "@eslint/eslintrc": "3.3.3",
"@eslint/js": "9.39.2",
"eslint": "9.39.2",
"globals": "16.5.0",
"eslint-config-prettier": "10.1.8", "eslint-config-prettier": "10.1.8",
"eslint-plugin-prettier": "5.5.4", "eslint-plugin-prettier": "5.5.4",
"io-ts": "2.2.22", "io-ts": "2.2.22",

View file

@ -0,0 +1,52 @@
import js from "@eslint/js"
import tsParser from "@typescript-eslint/parser"
import typescriptEslintPlugin from "@typescript-eslint/eslint-plugin"
import prettierPlugin from "eslint-plugin-prettier"
import globals from "globals"
export default [
{
ignores: [
"dist/**",
"node_modules/**",
"**/*.d.ts",
"vite.config.ts",
],
},
js.configs.recommended,
{
files: ["src/**/*.ts"],
languageOptions: {
parser: tsParser,
sourceType: "module",
ecmaVersion: 2022,
globals: {
...globals.browser,
...globals.node,
},
},
plugins: {
"@typescript-eslint": typescriptEslintPlugin,
prettier: prettierPlugin,
},
rules: {
...typescriptEslintPlugin.configs.recommended.rules,
"prettier/prettier": "warn",
"no-undef": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
args: "all",
argsIgnorePattern: "^_",
caughtErrors: "all",
caughtErrorsIgnorePattern: "^_",
destructuredArrayIgnorePattern: "^_",
varsIgnorePattern: "^_",
ignoreRestSiblings: true,
},
],
},
},
]

View file

@ -15,7 +15,11 @@
"build:decl": "tsc --project tsconfig.decl.json", "build:decl": "tsc --project tsconfig.decl.json",
"build": "pnpm run build:code && pnpm run build:decl", "build": "pnpm run build:code && pnpm run build:decl",
"prepare": "pnpm run build:code && pnpm run build:decl", "prepare": "pnpm run build:code && pnpm run build:decl",
"do-typecheck": "pnpm exec tsc --noEmit" "do-typecheck": "pnpm exec tsc --noEmit",
"lint": "eslint src",
"lintfix": "eslint --fix src",
"do-lint": "pnpm run lint",
"do-lintfix": "pnpm run lintfix"
}, },
"exports": { "exports": {
".": { ".": {
@ -35,7 +39,13 @@
}, },
"homepage": "https://github.com/hoppscotch/hoppscotch#readme", "homepage": "https://github.com/hoppscotch/hoppscotch#readme",
"devDependencies": { "devDependencies": {
"@eslint/js": "9.39.2",
"@types/node": "24.9.1", "@types/node": "24.9.1",
"@typescript-eslint/eslint-plugin": "8.50.0",
"@typescript-eslint/parser": "8.50.0",
"eslint": "9.39.2",
"eslint-plugin-prettier": "5.5.4",
"globals": "16.5.0",
"typescript": "5.9.3", "typescript": "5.9.3",
"vite": "7.3.0" "vite": "7.3.0"
}, },

View file

@ -1,16 +1,16 @@
import type { Version } from './type/versioning' import type { Version } from "./type/versioning"
import { VERSIONS as IO_VERSIONS } from './io' import { VERSIONS as IO_VERSIONS } from "./io"
import { IO_IMPLS as WEB_IO_IMPLS } from './io/impl/web' import { IO_IMPLS as WEB_IO_IMPLS } from "./io/impl/web"
import { IO_IMPLS as DESKTOP_IO_IMPLS } from './io/impl/desktop' import { IO_IMPLS as DESKTOP_IO_IMPLS } from "./io/impl/desktop"
import { VERSIONS as RELAY_VERSIONS } from './relay' import { VERSIONS as RELAY_VERSIONS } from "./relay"
import { RELAY_IMPLS as WEB_RELAY_IMPLS } from './relay/impl/web' import { RELAY_IMPLS as WEB_RELAY_IMPLS } from "./relay/impl/web"
import { RELAY_IMPLS as DESKTOP_RELAY_IMPLS } from './relay/impl/desktop' import { RELAY_IMPLS as DESKTOP_RELAY_IMPLS } from "./relay/impl/desktop"
import { VERSIONS as STORE_VERSIONS } from './store' import { VERSIONS as STORE_VERSIONS } from "./store"
import { STORE_IMPLS as WEB_STORE_IMPLS } from './store/impl/web' import { STORE_IMPLS as WEB_STORE_IMPLS } from "./store/impl/web"
import { STORE_IMPLS as DESKTOP_STORE_IMPLS } from './store/impl/desktop' import { STORE_IMPLS as DESKTOP_STORE_IMPLS } from "./store/impl/desktop"
export interface KernelInfo { export interface KernelInfo {
name: string name: string
@ -25,7 +25,7 @@ export interface KernelAPI {
store: typeof STORE_VERSIONS.v1.api store: typeof STORE_VERSIONS.v1.api
} }
export type KernelMode = 'web' | 'desktop' export type KernelMode = "web" | "desktop"
declare global { declare global {
interface Window { interface Window {
@ -39,16 +39,16 @@ export function getKernelMode(): KernelMode {
} }
export function initKernel(mode?: KernelMode): KernelAPI { export function initKernel(mode?: KernelMode): KernelAPI {
if (mode === 'desktop') { if (mode === "desktop") {
const kernel: KernelAPI = { const kernel: KernelAPI = {
info: { info: {
name: "desktop-kernel", name: "desktop-kernel",
version: { major: 1, minor: 0, patch: 0 }, version: { major: 1, minor: 0, patch: 0 },
capabilities: ["basic-io"] capabilities: ["basic-io"],
}, },
io: DESKTOP_IO_IMPLS.v1.api, io: DESKTOP_IO_IMPLS.v1.api,
relay: DESKTOP_RELAY_IMPLS.v1.api, relay: DESKTOP_RELAY_IMPLS.v1.api,
store: DESKTOP_STORE_IMPLS.v1.api store: DESKTOP_STORE_IMPLS.v1.api,
} }
window.__KERNEL__ = kernel window.__KERNEL__ = kernel
@ -58,11 +58,11 @@ export function initKernel(mode?: KernelMode): KernelAPI {
info: { info: {
name: "web-kernel", name: "web-kernel",
version: { major: 1, minor: 0, patch: 0 }, version: { major: 1, minor: 0, patch: 0 },
capabilities: ["basic-io"] capabilities: ["basic-io"],
}, },
io: WEB_IO_IMPLS.v1.api, io: WEB_IO_IMPLS.v1.api,
relay: WEB_RELAY_IMPLS.v1.api, relay: WEB_RELAY_IMPLS.v1.api,
store: WEB_STORE_IMPLS.v1.api store: WEB_STORE_IMPLS.v1.api,
} }
window.__KERNEL__ = kernel window.__KERNEL__ = kernel
@ -79,7 +79,7 @@ export type {
EventCallback, EventCallback,
UnlistenFn, UnlistenFn,
IoV1, IoV1,
} from '@io/v/1' } from "@io/v/1"
export type { export type {
RelayRequest, RelayRequest,
@ -98,15 +98,15 @@ export type {
RelayCapabilities, RelayCapabilities,
RelayEventEmitter, RelayEventEmitter,
RelayRequestEvents, RelayRequestEvents,
StatusCode StatusCode,
} from '@relay/v/1' } from "@relay/v/1"
export { export {
content, content,
body, body,
MediaType, MediaType,
relayRequestToNativeAdapter relayRequestToNativeAdapter,
} from '@relay/v/1' } from "@relay/v/1"
export type { export type {
StoreCapability, StoreCapability,
@ -120,4 +120,4 @@ export type {
StoredData, StoredData,
StoreEventEmitter, StoreEventEmitter,
StoreV1, StoreV1,
} from '@store/v/1' } from "@store/v/1"

View file

@ -1,4 +1,4 @@
import { implementation as ioV1 } from './v/1' import { implementation as ioV1 } from "./v/1"
export const IO_IMPLS = { export const IO_IMPLS = {
v1: ioV1, v1: ioV1,

View file

@ -1,10 +1,18 @@
import { VersionedAPI } from '@type/versioning' import { VersionedAPI } from "@type/versioning"
import { IoV1, SaveFileWithDialogOptions, OpenExternalLinkOptions } from '@io/v/1' import {
IoV1,
SaveFileWithDialogOptions,
OpenExternalLinkOptions,
} from "@io/v/1"
import { save } from "@tauri-apps/plugin-dialog" import { save } from "@tauri-apps/plugin-dialog"
import { writeFile, writeTextFile } from "@tauri-apps/plugin-fs" import { writeFile, writeTextFile } from "@tauri-apps/plugin-fs"
import { open } from "@tauri-apps/plugin-shell" import { open } from "@tauri-apps/plugin-shell"
import { listen as tauriListen, emit as tauriEmit, Event } from '@tauri-apps/api/event' import {
listen as tauriListen,
emit as tauriEmit,
Event,
} from "@tauri-apps/api/event"
export const implementation: VersionedAPI<IoV1> = { export const implementation: VersionedAPI<IoV1> = {
version: { major: 1, minor: 0, patch: 0 }, version: { major: 1, minor: 0, patch: 0 },
@ -47,6 +55,6 @@ export const implementation: VersionedAPI<IoV1> = {
async emit(event: string, payload?: unknown) { async emit(event: string, payload?: unknown) {
await tauriEmit(event, payload) await tauriEmit(event, payload)
} },
} },
} }

View file

@ -1,4 +1,4 @@
import { implementation as ioV1 } from './v/1' import { implementation as ioV1 } from "./v/1"
export const IO_IMPLS = { export const IO_IMPLS = {
v1: ioV1, v1: ioV1,

View file

@ -1,8 +1,13 @@
import { VersionedAPI } from '@type/versioning' import { VersionedAPI } from "@type/versioning"
import { IoV1, SaveFileWithDialogOptions, OpenExternalLinkOptions, Event } from '@io/v/1' import {
import { pipe } from 'fp-ts/function' IoV1,
import * as S from 'fp-ts/string' SaveFileWithDialogOptions,
import * as RNEA from 'fp-ts/ReadonlyNonEmptyArray' OpenExternalLinkOptions,
Event,
} from "@io/v/1"
import { pipe } from "fp-ts/function"
import * as S from "fp-ts/string"
import * as RNEA from "fp-ts/ReadonlyNonEmptyArray"
export const implementation: VersionedAPI<IoV1> = { export const implementation: VersionedAPI<IoV1> = {
version: { major: 1, minor: 0, patch: 0 }, version: { major: 1, minor: 0, patch: 0 },
@ -20,7 +25,9 @@ export const implementation: VersionedAPI<IoV1> = {
const url = URL.createObjectURL(file) const url = URL.createObjectURL(file)
a.href = url a.href = url
a.download = opts.suggestedFilename ?? pipe( a.download =
opts.suggestedFilename ??
pipe(
url, url,
S.split("/"), S.split("/"),
RNEA.last, RNEA.last,
@ -49,42 +56,42 @@ export const implementation: VersionedAPI<IoV1> = {
}, },
async listen<T>(event: string, handler: (event: Event<T>) => void) { async listen<T>(event: string, handler: (event: Event<T>) => void) {
const listener = (e: HashChangeEvent) => { const listener = (_e: HashChangeEvent) => {
const hash = window.location.hash const hash = window.location.hash
if (hash && hash.startsWith(`#${event}:`)) { if (hash && hash.startsWith(`#${event}:`)) {
const payload = hash.slice(event.length + 2) // Remove #event: const payload = hash.slice(event.length + 2) // Remove #event:
handler({ handler({
event, event,
id: Date.now(), id: Date.now(),
payload: JSON.parse(payload) as T payload: JSON.parse(payload) as T,
}) })
} }
} }
window.addEventListener('hashchange', listener) window.addEventListener("hashchange", listener)
return () => window.removeEventListener('hashchange', listener) return () => window.removeEventListener("hashchange", listener)
}, },
async once<T>(event: string, handler: (event: Event<T>) => void) { async once<T>(event: string, handler: (event: Event<T>) => void) {
const listener = (e: HashChangeEvent) => { const listener = (_e: HashChangeEvent) => {
const hash = window.location.hash const hash = window.location.hash
if (hash && hash.startsWith(`#${event}:`)) { if (hash && hash.startsWith(`#${event}:`)) {
const payload = hash.slice(event.length + 2) // Remove #event: const payload = hash.slice(event.length + 2) // Remove #event:
window.removeEventListener('hashchange', listener) window.removeEventListener("hashchange", listener)
handler({ handler({
event, event,
id: Date.now(), id: Date.now(),
payload: JSON.parse(payload) as T payload: JSON.parse(payload) as T,
}) })
} }
} }
window.addEventListener('hashchange', listener) window.addEventListener("hashchange", listener)
return () => window.removeEventListener('hashchange', listener) return () => window.removeEventListener("hashchange", listener)
}, },
async emit(event: string, payload?: unknown) { async emit(event: string, payload?: unknown) {
window.location.hash = `${event}:${JSON.stringify(payload)}` window.location.hash = `${event}:${JSON.stringify(payload)}`
} },
} },
} }

View file

@ -1,4 +1,4 @@
import { v1 } from './v/1' import { v1 } from "./v/1"
export type { export type {
IoV1, IoV1,
@ -8,8 +8,8 @@ export type {
OpenExternalLinkResponse, OpenExternalLinkResponse,
Event, Event,
EventCallback, EventCallback,
UnlistenFn UnlistenFn,
} from './v/1' } from "./v/1"
export const VERSIONS = { export const VERSIONS = {
v1, v1,

View file

@ -1,4 +1,4 @@
import type { VersionedAPI } from '@type/versioning' import type { VersionedAPI } from "@type/versioning"
export interface Event<T> { export interface Event<T> {
event: string event: string
@ -42,20 +42,11 @@ export interface IoV1 {
opts: OpenExternalLinkOptions opts: OpenExternalLinkOptions
) => Promise<OpenExternalLinkResponse> ) => Promise<OpenExternalLinkResponse>
listen: <T>( listen: <T>(event: string, handler: EventCallback<T>) => Promise<UnlistenFn>
event: string,
handler: EventCallback<T>
) => Promise<UnlistenFn>
once: <T>( once: <T>(event: string, handler: EventCallback<T>) => Promise<UnlistenFn>
event: string,
handler: EventCallback<T>
) => Promise<UnlistenFn>
emit: ( emit: (event: string, payload?: unknown) => Promise<void>
event: string,
payload?: unknown
) => Promise<void>
} }
export const v1: VersionedAPI<IoV1> = { export const v1: VersionedAPI<IoV1> = {
@ -66,5 +57,5 @@ export const v1: VersionedAPI<IoV1> = {
listen: async () => () => {}, listen: async () => () => {},
once: async () => () => {}, once: async () => () => {},
emit: async () => {}, emit: async () => {},
} },
} }

View file

@ -1,4 +1,4 @@
import { implementation as relayV1 } from './v/1' import { implementation as relayV1 } from "./v/1"
export const RELAY_IMPLS = { export const RELAY_IMPLS = {
v1: relayV1, v1: relayV1,

View file

@ -1,4 +1,4 @@
import type { VersionedAPI } from '@type/versioning' import type { VersionedAPI } from "@type/versioning"
import { import {
type RelayV1, type RelayV1,
type RelayRequest, type RelayRequest,
@ -8,76 +8,61 @@ import {
type RelayError, type RelayError,
body, body,
relayRequestToNativeAdapter, relayRequestToNativeAdapter,
} from '@relay/v/1' } from "@relay/v/1"
import * as E from 'fp-ts/Either' import * as E from "fp-ts/Either"
import { import {
execute, execute,
cancel, cancel,
type Request, type Request as _Request,
type RequestResult type RequestResult,
} from '@hoppscotch/plugin-relay' } from "@hoppscotch/plugin-relay"
export const implementation: VersionedAPI<RelayV1> = { export const implementation: VersionedAPI<RelayV1> = {
version: { major: 1, minor: 0, patch: 0 }, version: { major: 1, minor: 0, patch: 0 },
api: { api: {
id: 'desktop', id: "desktop",
capabilities: { capabilities: {
method: new Set([ method: new Set([
'GET', "GET",
'POST', "POST",
'PUT', "PUT",
'DELETE', "DELETE",
'PATCH', "PATCH",
'HEAD', "HEAD",
'OPTIONS' "OPTIONS",
]),
header: new Set([
'stringvalue',
'arrayvalue',
'multivalue'
]), ]),
header: new Set(["stringvalue", "arrayvalue", "multivalue"]),
content: new Set([ content: new Set([
'text', "text",
'json', "json",
'xml', "xml",
'form', "form",
'binary', "binary",
'multipart', "multipart",
'urlencoded', "urlencoded",
'stream', "stream",
'compression' "compression",
]),
auth: new Set([
'basic',
'bearer',
'digest',
'oauth2',
'apikey'
]), ]),
auth: new Set(["basic", "bearer", "digest", "oauth2", "apikey"]),
security: new Set([ security: new Set([
'clientcertificates', "clientcertificates",
'cacertificates', "cacertificates",
'certificatevalidation', "certificatevalidation",
'hostverification', "hostverification",
'peerverification' "peerverification",
]),
proxy: new Set([
'http',
'https',
'authentication',
'certificates'
]), ]),
proxy: new Set(["http", "https", "authentication", "certificates"]),
advanced: new Set([ advanced: new Set([
'retry', "retry",
'redirects', "redirects",
'timeout', "timeout",
'cookies', "cookies",
'keepalive', "keepalive",
'tcpoptions', "tcpoptions",
'http2', "http2",
'http3' "http3",
]) ]),
}, },
canHandle(request: RelayRequest) { canHandle(request: RelayRequest) {
@ -86,16 +71,19 @@ export const implementation: VersionedAPI<RelayV1> = {
kind: "unsupported_feature", kind: "unsupported_feature",
feature: "method", feature: "method",
message: `Method ${request.method} is not supported`, message: `Method ${request.method} is not supported`,
relay: "desktop" relay: "desktop",
}) })
} }
if (request.content && !this.capabilities.content.has(request.content.kind)) { if (
request.content &&
!this.capabilities.content.has(request.content.kind)
) {
return E.left({ return E.left({
kind: "unsupported_feature", kind: "unsupported_feature",
feature: "content", feature: "content",
message: `Content type ${request.content.kind} is not supported`, message: `Content type ${request.content.kind} is not supported`,
relay: "desktop" relay: "desktop",
}) })
} }
@ -104,25 +92,33 @@ export const implementation: VersionedAPI<RelayV1> = {
kind: "unsupported_feature", kind: "unsupported_feature",
feature: "authentication", feature: "authentication",
message: `Authentication type ${request.auth.kind} is not supported`, message: `Authentication type ${request.auth.kind} is not supported`,
relay: "desktop" relay: "desktop",
}) })
} }
if (request.security?.certificates && !this.capabilities.security.has('clientcertificates')) { if (
request.security?.certificates &&
!this.capabilities.security.has("clientcertificates")
) {
return E.left({ return E.left({
kind: "unsupported_feature", kind: "unsupported_feature",
feature: "security", feature: "security",
message: "Client certificates are not supported", message: "Client certificates are not supported",
relay: "desktop" relay: "desktop",
}) })
} }
if (request.proxy && !this.capabilities.proxy.has(request.proxy.url.startsWith('https') ? 'https' : 'http')) { if (
request.proxy &&
!this.capabilities.proxy.has(
request.proxy.url.startsWith("https") ? "https" : "http"
)
) {
return E.left({ return E.left({
kind: "unsupported_feature", kind: "unsupported_feature",
feature: "proxy", feature: "proxy",
message: `Proxy protocol ${request.proxy.url.split(':')[0]} is not supported`, message: `Proxy protocol ${request.proxy.url.split(":")[0]} is not supported`,
relay: "desktop" relay: "desktop",
}) })
} }
@ -133,11 +129,11 @@ export const implementation: VersionedAPI<RelayV1> = {
const emitter: RelayEventEmitter<RelayRequestEvents> = { const emitter: RelayEventEmitter<RelayRequestEvents> = {
on: () => () => {}, on: () => () => {},
once: () => () => {}, once: () => () => {},
off: () => {} off: () => {},
} }
const responsePromise = relayRequestToNativeAdapter(request) const responsePromise = relayRequestToNativeAdapter(request)
.then(request => { .then((request) => {
// SAFETY: Type assertion is safe because: // SAFETY: Type assertion is safe because:
// 1. The capabilities system prevents requests with unsupported methods from reaching this point // 1. The capabilities system prevents requests with unsupported methods from reaching this point
// 2. Content types not supported by the plugin are filtered by capabilities // 2. Content types not supported by the plugin are filtered by capabilities
@ -160,7 +156,7 @@ export const implementation: VersionedAPI<RelayV1> = {
return execute(pluginRequest) return execute(pluginRequest)
}) })
.then((result: RequestResult): E.Either<RelayError, RelayResponse> => { .then((result: RequestResult): E.Either<RelayError, RelayResponse> => {
if (result.kind === 'success') { if (result.kind === "success") {
const response: RelayResponse = { const response: RelayResponse = {
id: result.response.id, id: result.response.id,
status: result.response.status, status: result.response.status,
@ -168,14 +164,17 @@ export const implementation: VersionedAPI<RelayV1> = {
version: result.response.version, version: result.response.version,
headers: result.response.headers, headers: result.response.headers,
cookies: result.response.cookies, cookies: result.response.cookies,
body: body.body(result.response.body.body, result.response.body.mediaType), body: body.body(
result.response.body.body,
result.response.body.mediaType
),
meta: { meta: {
timing: { timing: {
start: result.response.meta.timing.start, start: result.response.meta.timing.start,
end: result.response.meta.timing.end, end: result.response.meta.timing.end,
}, },
size: result.response.meta.size, size: result.response.meta.size,
} },
} }
return E.right(response) return E.right(response)
} }
@ -183,18 +182,21 @@ export const implementation: VersionedAPI<RelayV1> = {
}) })
.catch((error: unknown): E.Either<RelayError, RelayResponse> => { .catch((error: unknown): E.Either<RelayError, RelayResponse> => {
const networkError: RelayError = { const networkError: RelayError = {
kind: 'network', kind: "network",
message: error instanceof Error ? error.message : 'Unknown error occurred', message:
cause: error error instanceof Error ? error.message : "Unknown error occurred",
cause: error,
} }
return E.left(networkError) return E.left(networkError)
}) })
return { return {
cancel: async () => { await cancel(request.id) }, cancel: async () => {
await cancel(request.id)
},
emitter, emitter,
response: responsePromise response: responsePromise,
}
}
} }
},
},
} }

View file

@ -1,4 +1,4 @@
import { implementation as relayV1 } from './v/1' import { implementation as relayV1 } from "./v/1"
export const RELAY_IMPLS = { export const RELAY_IMPLS = {
v1: relayV1, v1: relayV1,

View file

@ -10,7 +10,7 @@ import {
} from "@relay/v/1" } from "@relay/v/1"
import type { VersionedAPI } from "@type/versioning" import type { VersionedAPI } from "@type/versioning"
import { AwsV4Signer } from "aws4fetch" import { AwsV4Signer as _AwsV4Signer } from "aws4fetch"
import axios, { AxiosRequestConfig } from "axios" import axios, { AxiosRequestConfig } from "axios"
import * as E from "fp-ts/Either" import * as E from "fp-ts/Either"

View file

@ -1,8 +1,6 @@
import { v1 } from './v/1' import { v1 } from "./v/1"
export type { export type { RelayV1 } from "./v/1"
RelayV1,
} from './v/1'
export const VERSIONS = { export const VERSIONS = {
v1, v1,

View file

@ -1,12 +1,12 @@
import { Request, Response } from '@hoppscotch/plugin-relay' import { Request, Response } from "@hoppscotch/plugin-relay"
import type { VersionedAPI } from '@type/versioning' import type { VersionedAPI } from "@type/versioning"
export type PluginRequest = Request export type PluginRequest = Request
export type PluginResponse = Response export type PluginResponse = Response
import * as E from 'fp-ts/Either' import * as E from "fp-ts/Either"
import * as O from 'fp-ts/Option' import * as O from "fp-ts/Option"
import { pipe } from 'fp-ts/function' import { pipe } from "fp-ts/function"
export type Method = export type Method =
| "GET" // Retrieve resource | "GET" // Retrieve resource
@ -101,7 +101,7 @@ export enum MediaType {
TEXT_XML = "text/xml", TEXT_XML = "text/xml",
APPLICATION_FORM = "application/x-www-form-urlencoded", APPLICATION_FORM = "application/x-www-form-urlencoded",
APPLICATION_OCTET = "application/octet-stream", APPLICATION_OCTET = "application/octet-stream",
MULTIPART_FORM = "multipart/form-data" MULTIPART_FORM = "multipart/form-data",
} }
export type ContentType = export type ContentType =
@ -109,13 +109,18 @@ export type ContentType =
| { kind: "json"; content: unknown; mediaType: MediaType | string } | { kind: "json"; content: unknown; mediaType: MediaType | string }
| { kind: "xml"; content: string; mediaType: MediaType | string } | { kind: "xml"; content: string; mediaType: MediaType | string }
| { kind: "form"; content: FormData; mediaType: MediaType | string } | { kind: "form"; content: FormData; mediaType: MediaType | string }
| { kind: "binary"; content: Uint8Array; mediaType: MediaType | string; filename?: string } | {
kind: "binary"
content: Uint8Array
mediaType: MediaType | string
filename?: string
}
| { kind: "multipart"; content: FormData; mediaType: MediaType | string } | { kind: "multipart"; content: FormData; mediaType: MediaType | string }
| { kind: "urlencoded"; content: string; mediaType: MediaType | string } | { kind: "urlencoded"; content: string; mediaType: MediaType | string }
| { kind: "stream"; content: ReadableStream; mediaType: MediaType | string } | { kind: "stream"; content: ReadableStream; mediaType: MediaType | string }
// TODO: Considering adding a "raw" kind for explicit pass-through content in the future, // TODO: Considering adding a "raw" kind for explicit pass-through content in the future,
// not required at the moment tho, needs some plumbing on the relay side. // not required at the moment tho, needs some plumbing on the relay side.
// | { kind: "raw"; content: string; mediaType: MediaType | string } // | { kind: "raw"; content: string; mediaType: MediaType | string }
export interface RelayResponseBody { export interface RelayResponseBody {
body: Uint8Array body: Uint8Array
@ -198,15 +203,21 @@ export type CertificateType =
export interface RelayRequestEvents { export interface RelayRequestEvents {
progress: { progress: {
phase: 'upload' | 'download' phase: "upload" | "download"
loaded: number loaded: number
total?: number total?: number
} }
stateChange: { stateChange: {
state: 'preparing' | 'connecting' | 'sending' | 'waiting' | 'receiving' | 'done' state:
| "preparing"
| "connecting"
| "sending"
| "waiting"
| "receiving"
| "done"
} }
authChallenge: { authChallenge: {
type: 'basic' | 'digest' | 'oauth2' type: "basic" | "digest" | "oauth2"
params: Record<string, string> params: Record<string, string>
} }
cookieReceived: { cookieReceived: {
@ -217,84 +228,84 @@ export interface RelayRequestEvents {
expires?: Date expires?: Date
secure?: boolean secure?: boolean
httpOnly?: boolean httpOnly?: boolean
sameSite?: 'Strict' | 'Lax' | 'None' sameSite?: "Strict" | "Lax" | "None"
} }
error: { error: {
phase: 'preparation' | 'connection' | 'request' | 'response' phase: "preparation" | "connection" | "request" | "response"
error: RelayError error: RelayError
} }
} }
export type RelayEventEmitter<T> = { export type RelayEventEmitter<T> = {
on<K extends keyof T>(event: K, handler: (payload: T[K]) => void): () => void on<K extends keyof T>(event: K, handler: (payload: T[K]) => void): () => void
once<K extends keyof T>(event: K, handler: (payload: T[K]) => void): () => void once<K extends keyof T>(
event: K,
handler: (payload: T[K]) => void
): () => void
off<K extends keyof T>(event: K, handler: (payload: T[K]) => void): void off<K extends keyof T>(event: K, handler: (payload: T[K]) => void): void
} }
// NOTE: RelayCapabilities and their corresponding objects being two separate types // NOTE: RelayCapabilities and their corresponding objects being two separate types
// even with sometimes identical contents is intentional. // even with sometimes identical contents is intentional.
export type MethodCapability = export type MethodCapability =
| 'GET' | "GET"
| 'POST' | "POST"
| 'PUT' | "PUT"
| 'DELETE' | "DELETE"
| 'PATCH' | "PATCH"
| 'HEAD' | "HEAD"
| 'OPTIONS' | "OPTIONS"
| 'CONNECT' | "CONNECT"
| 'TRACE' | "TRACE"
export type HeaderCapability = export type HeaderCapability = "stringvalue" | "arrayvalue" | "multivalue"
| 'stringvalue'
| 'arrayvalue'
| 'multivalue'
export type ContentCapability = export type ContentCapability =
| 'text' | "text"
| 'json' | "json"
| 'xml' | "xml"
| 'form' | "form"
| 'binary' | "binary"
| 'multipart' | "multipart"
| 'urlencoded' | "urlencoded"
| 'stream' | "stream"
| 'compression' | "compression"
export type AuthCapability = export type AuthCapability =
| 'none' | "none"
| 'basic' | "basic"
| 'bearer' | "bearer"
| 'digest' | "digest"
| 'oauth2' | "oauth2"
| 'apikey' | "apikey"
| 'aws' | "aws"
| 'mtls' | "mtls"
export type SecurityCapability = export type SecurityCapability =
| 'clientcertificates' | "clientcertificates"
| 'cacertificates' | "cacertificates"
| 'certificatevalidation' | "certificatevalidation"
| 'hostverification' | "hostverification"
| 'peerverification' | "peerverification"
export type ProxyCapability = export type ProxyCapability =
| 'http' | "http"
| 'https' | "https"
| 'socks' | "socks"
| 'authentication' | "authentication"
| 'certificates' | "certificates"
export type AdvancedCapability = export type AdvancedCapability =
| 'retry' | "retry"
| 'redirects' | "redirects"
| 'timeout' | "timeout"
| 'cookies' | "cookies"
| 'keepalive' | "keepalive"
| 'tcpoptions' | "tcpoptions"
| 'ipv6' | "ipv6"
| 'http2' | "http2"
| 'http3' | "http3"
| 'localaccess' | "localaccess"
export interface RelayCapabilities { export interface RelayCapabilities {
method: Set<MethodCapability> method: Set<MethodCapability>
@ -403,7 +414,7 @@ export interface RelayResponse {
expires?: Date expires?: Date
secure?: boolean secure?: boolean
httpOnly?: boolean httpOnly?: boolean
sameSite?: 'Strict' | 'Lax' | 'None' sameSite?: "Strict" | "Lax" | "None"
}> }>
body: RelayResponseBody body: RelayResponseBody
@ -457,17 +468,17 @@ export interface RelayV1 {
canHandle(request: RelayRequest): E.Either<UnsupportedFeatureError, true> canHandle(request: RelayRequest): E.Either<UnsupportedFeatureError, true>
execute( execute(request: RelayRequest): {
request: RelayRequest
): {
cancel: () => Promise<void> cancel: () => Promise<void>
emitter: RelayEventEmitter<RelayRequestEvents> emitter: RelayEventEmitter<RelayRequestEvents>
response: Promise<E.Either<RelayError, RelayResponse>> response: Promise<E.Either<RelayError, RelayResponse>>
} }
} }
export const hasCapability = <T>(capabilities: Set<T>, capability: T): boolean => export const hasCapability = <T>(
capabilities.has(capability) capabilities: Set<T>,
capability: T
): boolean => capabilities.has(capability)
export function findSuitableRelay( export function findSuitableRelay(
request: RelayRequest, request: RelayRequest,
@ -481,9 +492,9 @@ export function findSuitableRelay(
} }
const errors = relays const errors = relays
.map(i => i.canHandle(request)) .map((i) => i.canHandle(request))
.filter(E.isLeft) .filter(E.isLeft)
.map(e => e.left) .map((e) => e.left)
return E.left(errors[0]) return E.left(errors[0])
} }
@ -494,9 +505,11 @@ export const body = {
contentType?: MediaType | string contentType?: MediaType | string
): RelayResponseBody => ({ ): RelayResponseBody => ({
body: new Uint8Array(body), body: new Uint8Array(body),
mediaType: typeof contentType === 'string' mediaType:
? Object.values(MediaType).find(t => contentType.includes(t)) ?? MediaType.APPLICATION_OCTET typeof contentType === "string"
: contentType ?? MediaType.APPLICATION_OCTET ? (Object.values(MediaType).find((t) => contentType.includes(t)) ??
MediaType.APPLICATION_OCTET)
: (contentType ?? MediaType.APPLICATION_OCTET),
}), }),
} }
@ -511,22 +524,18 @@ export const transform = {
urlencoded: (arg: string | Record<string, any>): string => urlencoded: (arg: string | Record<string, any>): string =>
pipe( pipe(
arg, arg,
(input) => typeof input === 'string' (input) => (typeof input === "string" ? O.some(input) : O.none),
? O.some(input)
: O.none,
O.getOrElse(() => { O.getOrElse(() => {
const params = new URLSearchParams() const params = new URLSearchParams()
const obj = arg as Record<string, any> const obj = arg as Record<string, any>
Object.entries(obj) Object.entries(obj)
.filter(([_, value]) => value !== undefined && value !== null) .filter(([_, value]) => value !== undefined && value !== null)
.forEach(([key, value]) => .forEach(([key, value]) => params.append(key, value.toString()))
params.append(key, value.toString())
)
return params.toString() return params.toString()
}) })
) ),
} }
/** /**
@ -552,13 +561,10 @@ export const content = {
* - Pre-stringified JSON (to avoid encoding escapes) * - Pre-stringified JSON (to avoid encoding escapes)
* - Custom text formats * - Custom text formats
*/ */
text: ( text: (content: string, mediaType?: MediaType | string): ContentType => ({
content: string,
mediaType?: MediaType | string
): ContentType => ({
kind: "text", kind: "text",
content: transform.text(content), content: transform.text(content),
mediaType: mediaType ?? MediaType.TEXT_PLAIN mediaType: mediaType ?? MediaType.TEXT_PLAIN,
}), }),
/** /**
@ -566,25 +572,19 @@ export const content = {
* Note: If you already have a JSON string, consider using `text()` * Note: If you already have a JSON string, consider using `text()`
* with `APPLICATION_JSON` mediaType to avoid double-encoding. * with `APPLICATION_JSON` mediaType to avoid double-encoding.
*/ */
json: <T>( json: <T>(content: T, mediaType?: MediaType | string): ContentType => ({
content: T,
mediaType?: MediaType | string
): ContentType => ({
kind: "json", kind: "json",
content: transform.json(content), content: transform.json(content),
mediaType: mediaType ?? MediaType.APPLICATION_JSON mediaType: mediaType ?? MediaType.APPLICATION_JSON,
}), }),
/** /**
* Creates XML content. Currently processed as text. * Creates XML content. Currently processed as text.
*/ */
xml: ( xml: (content: string, mediaType?: MediaType | string): ContentType => ({
content: string,
mediaType?: MediaType | string
): ContentType => ({
kind: "xml", kind: "xml",
content: transform.xml(content), content: transform.xml(content),
mediaType: mediaType ?? MediaType.APPLICATION_XML mediaType: mediaType ?? MediaType.APPLICATION_XML,
}), }),
/** /**
@ -593,7 +593,7 @@ export const content = {
form: (content: FormData, mediaType?: MediaType | string): ContentType => ({ form: (content: FormData, mediaType?: MediaType | string): ContentType => ({
kind: "form", kind: "form",
content: transform.form(content), content: transform.form(content),
mediaType: mediaType ?? MediaType.APPLICATION_FORM mediaType: mediaType ?? MediaType.APPLICATION_FORM,
}), }),
/** /**
@ -607,35 +607,44 @@ export const content = {
kind: "binary", kind: "binary",
content: transform.binary(content), content: transform.binary(content),
mediaType, mediaType,
filename filename,
}), }),
/** /**
* Creates multipart form content with file upload support. * Creates multipart form content with file upload support.
*/ */
multipart: (content: FormData, mediaType?: MediaType | string): ContentType => ({ multipart: (
content: FormData,
mediaType?: MediaType | string
): ContentType => ({
kind: "multipart", kind: "multipart",
content: transform.multipart(content), content: transform.multipart(content),
mediaType: mediaType ?? MediaType.MULTIPART_FORM mediaType: mediaType ?? MediaType.MULTIPART_FORM,
}), }),
/** /**
* Creates URL-encoded content from string or object. * Creates URL-encoded content from string or object.
*/ */
urlencoded: (content: string | Record<string, any>, mediaType?: MediaType | string): ContentType => ({ urlencoded: (
content: string | Record<string, any>,
mediaType?: MediaType | string
): ContentType => ({
kind: "urlencoded", kind: "urlencoded",
content: transform.urlencoded(content), content: transform.urlencoded(content),
mediaType: mediaType ?? MediaType.APPLICATION_FORM mediaType: mediaType ?? MediaType.APPLICATION_FORM,
}), }),
/** /**
* Creates streaming content for large payloads. * Creates streaming content for large payloads.
*/ */
stream: (content: ReadableStream, mediaType: MediaType | string): ContentType => ({ stream: (
content: ReadableStream,
mediaType: MediaType | string
): ContentType => ({
kind: "stream", kind: "stream",
content: transform.stream(content), content: transform.stream(content),
mediaType mediaType,
}) }),
// TODO: Raw content type for pass-through scenarios: // TODO: Raw content type for pass-through scenarios:
// raw: (content: string, mediaType: MediaType | string): ContentType => ({ // raw: (content: string, mediaType: MediaType | string): ContentType => ({
@ -680,22 +689,21 @@ export const examples = {
// Custom XML schema // Custom XML schema
soapXml: content.xml( soapXml: content.xml(
'<soap:Envelope>...</soap:Envelope>', "<soap:Envelope>...</soap:Envelope>",
"application/soap+xml" "application/soap+xml"
), ),
// Custom binary format // Custom binary format
customBinary: content.binary( customBinary: content.binary(
new Uint8Array([0x89, 0x50, 0x4E, 0x47]), new Uint8Array([0x89, 0x50, 0x4e, 0x47]),
"image/png" "image/png"
), ),
// Backwards compatible - uses defaults // Backwards compatible - uses defaults
standardJson: content.json({ name: "John" }), standardJson: content.json({ name: "John" }),
standardText: content.text("Hello world") standardText: content.text("Hello world"),
} }
/** /**
* Helper function to convert standard `FormData` to array of arrays `[string, FormDataValue[]][]` * Helper function to convert standard `FormData` to array of arrays `[string, FormDataValue[]][]`
* *
@ -713,7 +721,9 @@ export const examples = {
* See: https://datatracker.ietf.org/doc/html/rfc7578#section-5.2 * See: https://datatracker.ietf.org/doc/html/rfc7578#section-5.2
* > Form processors given forms with a well-defined ordering SHOULD send back results in order. * > Form processors given forms with a well-defined ordering SHOULD send back results in order.
*/ */
const makeFormDataSerializable = async (formData: FormData): Promise<[string, FormDataValue[]][]> => { const makeFormDataSerializable = async (
formData: FormData
): Promise<[string, FormDataValue[]][]> => {
const m = new Map<string, FormDataValue[]>() const m = new Map<string, FormDataValue[]>()
// @ts-expect-error: `formData.entries` does exist but isn't visible, // @ts-expect-error: `formData.entries` does exist but isn't visible,
@ -725,7 +735,7 @@ const makeFormDataSerializable = async (formData: FormData): Promise<[string, Fo
kind: "file", kind: "file",
filename: value instanceof File ? value.name : "unknown", filename: value instanceof File ? value.name : "unknown",
contentType: value.type || "application/octet-stream", contentType: value.type || "application/octet-stream",
data: new Uint8Array(buffer) data: new Uint8Array(buffer),
} }
if (m.has(key)) { if (m.has(key)) {
@ -736,7 +746,7 @@ const makeFormDataSerializable = async (formData: FormData): Promise<[string, Fo
} else { } else {
const textEntry: FormDataValue = { const textEntry: FormDataValue = {
kind: "text", kind: "text",
value: value.toString() value: value.toString(),
} }
if (m.has(key)) { if (m.has(key)) {
@ -751,11 +761,18 @@ const makeFormDataSerializable = async (formData: FormData): Promise<[string, Fo
} }
// Helper function to adapt a relay request to work with the plugin // Helper function to adapt a relay request to work with the plugin
export const relayRequestToNativeAdapter = async (request: RelayRequest): Promise<Request> => { export const relayRequestToNativeAdapter = async (
const adaptedRequest = { ...request }; request: RelayRequest
): Promise<Request> => {
const adaptedRequest = { ...request }
if (adaptedRequest.content?.kind === "multipart" && adaptedRequest.content.content instanceof FormData) { if (
const serializableFormData = await makeFormDataSerializable(adaptedRequest.content.content); adaptedRequest.content?.kind === "multipart" &&
adaptedRequest.content.content instanceof FormData
) {
const serializableFormData = await makeFormDataSerializable(
adaptedRequest.content.content
)
adaptedRequest.content = { adaptedRequest.content = {
...adaptedRequest.content, ...adaptedRequest.content,
@ -766,11 +783,11 @@ export const relayRequestToNativeAdapter = async (request: RelayRequest): Promis
// `Maps` it seems like are serialized differently across platforms and serialization libraries, // `Maps` it seems like are serialized differently across platforms and serialization libraries,
// while `Array` of `Array` tend to maintain more consistent behavior by the sheer ubiquity of it. // while `Array` of `Array` tend to maintain more consistent behavior by the sheer ubiquity of it.
// @ts-expect-error: This is intentional to work around SuperJSON serialization // @ts-expect-error: This is intentional to work around SuperJSON serialization
content: serializableFormData content: serializableFormData,
}; }
} }
return adaptedRequest as Request; return adaptedRequest as Request
} }
export const v1: VersionedAPI<RelayV1> = { export const v1: VersionedAPI<RelayV1> = {
@ -784,27 +801,28 @@ export const v1: VersionedAPI<RelayV1> = {
auth: new Set<AuthCapability>(), auth: new Set<AuthCapability>(),
security: new Set<SecurityCapability>(), security: new Set<SecurityCapability>(),
proxy: new Set<ProxyCapability>(), proxy: new Set<ProxyCapability>(),
advanced: new Set<AdvancedCapability>() advanced: new Set<AdvancedCapability>(),
}, },
canHandle: () => E.left({ canHandle: () =>
E.left({
kind: "unsupported_feature", kind: "unsupported_feature",
feature: "execution", feature: "execution",
message: "Default relay cannot handle requests", message: "Default relay cannot handle requests",
relay: "default" relay: "default",
}), }),
execute: () => ({ execute: () => ({
cancel: async () => {}, cancel: async () => {},
emitter: { emitter: {
on: () => () => {}, on: () => () => {},
once: () => () => {}, once: () => () => {},
off: () => {} off: () => {},
}, },
response: Promise.resolve( response: Promise.resolve(
E.left({ E.left({
kind: "version", kind: "version",
message: "Not implemented" message: "Not implemented",
}) })
) ),
}) }),
} },
} }

View file

@ -1,4 +1,4 @@
import { implementation as storeV1 } from './v/1' import { implementation as storeV1 } from "./v/1"
export const STORE_IMPLS = { export const STORE_IMPLS = {
v1: storeV1, v1: storeV1,

View file

@ -1,8 +1,8 @@
import * as E from 'fp-ts/Either'; import * as E from "fp-ts/Either"
import { Store } from '@tauri-apps/plugin-store'; import { Store } from "@tauri-apps/plugin-store"
import { VersionedAPI } from '@type/versioning'; import { VersionedAPI } from "@type/versioning"
import { import {
StoreV1, StoreV1,
StoreError, StoreError,
@ -11,187 +11,204 @@ import {
StoredData, StoredData,
StoredDataSchema, StoredDataSchema,
StoreEventEmitter, StoreEventEmitter,
} from '@store/v/1'; } from "@store/v/1"
type NamespacedData = Record<string, Record<string, StoredData>>; type NamespacedData = Record<string, Record<string, StoredData>>
class TauriStoreManager { class TauriStoreManager {
private static instances: Map<string, TauriStoreManager> = new Map(); private static instances: Map<string, TauriStoreManager> = new Map()
private store: Store | null = null; private store: Store | null = null
private listeners = new Map<string, Set<(payload: StoreEvents['change']) => void>>(); private listeners = new Map<
private data: NamespacedData = {}; string,
private storePath: string; Set<(payload: StoreEvents["change"]) => void>
>()
private data: NamespacedData = {}
private storePath: string
private constructor(storePath: string) { private constructor(storePath: string) {
this.storePath = storePath; this.storePath = storePath
} }
static new(storePath: string): TauriStoreManager { static new(storePath: string): TauriStoreManager {
if (TauriStoreManager.instances.has(storePath)) { if (TauriStoreManager.instances.has(storePath)) {
return TauriStoreManager.instances.get(storePath)!; return TauriStoreManager.instances.get(storePath)!
} }
const instance = new TauriStoreManager(storePath); const instance = new TauriStoreManager(storePath)
TauriStoreManager.instances.set(storePath, instance); TauriStoreManager.instances.set(storePath, instance)
return instance; return instance
} }
static async closeAll(): Promise<void> { static async closeAll(): Promise<void> {
const closePromises = Array.from(TauriStoreManager.instances.values()) const closePromises = Array.from(TauriStoreManager.instances.values()).map(
.map(instance => instance.close()); (instance) => instance.close()
await Promise.all(closePromises); )
TauriStoreManager.instances.clear(); await Promise.all(closePromises)
TauriStoreManager.instances.clear()
} }
static async closeStore(storePath: string): Promise<void> { static async closeStore(storePath: string): Promise<void> {
const instance = TauriStoreManager.instances.get(storePath); const instance = TauriStoreManager.instances.get(storePath)
if (instance) { if (instance) {
await instance.close(); await instance.close()
TauriStoreManager.instances.delete(storePath); TauriStoreManager.instances.delete(storePath)
} }
} }
async init(): Promise<void> { async init(): Promise<void> {
if (!this.store) { if (!this.store) {
this.store = await Store.load(this.storePath); this.store = await Store.load(this.storePath)
const loadedData = await this.store.get<NamespacedData>('data'); const loadedData = await this.store.get<NamespacedData>("data")
this.data = loadedData ?? {}; this.data = loadedData ?? {}
this.store.onChange((_, value: NamespacedData | undefined) => { this.store.onChange((_, value: NamespacedData | undefined) => {
if (value) { if (value) {
this.data = value; this.data = value
this.notifyListeners(); this.notifyListeners()
} }
}); })
} }
} }
private notifyListeners(): void { private notifyListeners(): void {
for (const [key, listeners] of this.listeners.entries()) { for (const [key, listeners] of this.listeners.entries()) {
const [namespace, dataKey] = key.split(':'); const [namespace, dataKey] = key.split(":")
const value = this.data[namespace]?.[dataKey]; const value = this.data[namespace]?.[dataKey]
listeners.forEach(listener => listeners.forEach((listener) =>
listener({ listener({
namespace, namespace,
key: dataKey, key: dataKey,
value: value?.data, value: value?.data,
}) })
); )
} }
} }
async set(namespace: string, key: string, value: StoredData): Promise<void> { async set(namespace: string, key: string, value: StoredData): Promise<void> {
if (!this.store) throw new Error('Store not initialized'); if (!this.store) throw new Error("Store not initialized")
const validated = StoredDataSchema.parse(value); const validated = StoredDataSchema.parse(value)
this.data[namespace] = this.data[namespace] || {}; this.data[namespace] = this.data[namespace] || {}
this.data[namespace][key] = validated; this.data[namespace][key] = validated
await this.store.set('data', this.data); await this.store.set("data", this.data)
await this.store.save(); await this.store.save()
} }
async getRaw(namespace: string, key: string): Promise<StoredData | undefined> { async getRaw(
const rawValue = this.data[namespace]?.[key]; namespace: string,
if (!rawValue) return undefined; key: string
): Promise<StoredData | undefined> {
const rawValue = this.data[namespace]?.[key]
if (!rawValue) return undefined
const validated = StoredDataSchema.parse(rawValue); const validated = StoredDataSchema.parse(rawValue)
return validated; return validated
} }
async get<T>(namespace: string, key: string): Promise<T | undefined> { async get<T>(namespace: string, key: string): Promise<T | undefined> {
const storedData = await this.getRaw(namespace, key); const storedData = await this.getRaw(namespace, key)
return storedData?.data as T | undefined; return storedData?.data as T | undefined
} }
async has(namespace: string, key: string): Promise<boolean> { async has(namespace: string, key: string): Promise<boolean> {
return !!this.data[namespace]?.[key]; return !!this.data[namespace]?.[key]
} }
async delete(namespace: string, key: string): Promise<boolean> { async delete(namespace: string, key: string): Promise<boolean> {
if (!this.store) throw new Error('Store not initialized'); if (!this.store) throw new Error("Store not initialized")
if (this.data[namespace]?.[key]) { if (this.data[namespace]?.[key]) {
delete this.data[namespace][key]; delete this.data[namespace][key]
if (Object.keys(this.data[namespace]).length === 0) { if (Object.keys(this.data[namespace]).length === 0) {
delete this.data[namespace]; delete this.data[namespace]
} }
await this.store.set('data', this.data); await this.store.set("data", this.data)
await this.store.save(); await this.store.save()
return true; return true
} }
return false; return false
} }
async clear(namespace?: string): Promise<void> { async clear(namespace?: string): Promise<void> {
if (!this.store) throw new Error('Store not initialized'); if (!this.store) throw new Error("Store not initialized")
if (namespace) { if (namespace) {
delete this.data[namespace]; delete this.data[namespace]
} else { } else {
this.data = {}; this.data = {}
} }
await this.store.set('data', this.data); await this.store.set("data", this.data)
await this.store.save(); await this.store.save()
} }
async listNamespaces(): Promise<string[]> { async listNamespaces(): Promise<string[]> {
return Object.keys(this.data); return Object.keys(this.data)
} }
async listKeys(namespace: string): Promise<string[]> { async listKeys(namespace: string): Promise<string[]> {
return Object.keys(this.data[namespace] || {}); return Object.keys(this.data[namespace] || {})
} }
async watch(namespace: string, key: string): Promise<StoreEventEmitter<StoreEvents>> { async watch(
const watchKey = `${namespace}:${key}`; namespace: string,
key: string
): Promise<StoreEventEmitter<StoreEvents>> {
const watchKey = `${namespace}:${key}`
return { return {
on: <K extends keyof StoreEvents>( on: <K extends keyof StoreEvents>(
event: K, event: K,
handler: (payload: StoreEvents[K]) => void handler: (payload: StoreEvents[K]) => void
) => { ) => {
if (event !== 'change') return () => {}; if (event !== "change") return () => {}
if (!this.listeners.has(watchKey)) { if (!this.listeners.has(watchKey)) {
this.listeners.set(watchKey, new Set()); this.listeners.set(watchKey, new Set())
} }
this.listeners.get(watchKey)!.add(handler as (payload: StoreEvents['change']) => void); this.listeners
return () => this.listeners.get(watchKey)?.delete(handler as (payload: StoreEvents['change']) => void); .get(watchKey)!
.add(handler as (payload: StoreEvents["change"]) => void)
return () =>
this.listeners
.get(watchKey)
?.delete(handler as (payload: StoreEvents["change"]) => void)
}, },
once: <K extends keyof StoreEvents>( once: <K extends keyof StoreEvents>(
event: K, event: K,
handler: (payload: StoreEvents[K]) => void handler: (payload: StoreEvents[K]) => void
) => { ) => {
if (event !== 'change') return () => {}; if (event !== "change") return () => {}
const wrapper = (value: StoreEvents['change']) => { const wrapper = (value: StoreEvents["change"]) => {
handler(value as StoreEvents[K]); handler(value as StoreEvents[K])
this.listeners.get(watchKey)?.delete(wrapper); this.listeners.get(watchKey)?.delete(wrapper)
}; }
if (!this.listeners.has(watchKey)) { if (!this.listeners.has(watchKey)) {
this.listeners.set(watchKey, new Set()); this.listeners.set(watchKey, new Set())
} }
this.listeners.get(watchKey)!.add(wrapper); this.listeners.get(watchKey)!.add(wrapper)
return () => this.listeners.get(watchKey)?.delete(wrapper); return () => this.listeners.get(watchKey)?.delete(wrapper)
}, },
off: <K extends keyof StoreEvents>( off: <K extends keyof StoreEvents>(
event: K, event: K,
handler: (payload: StoreEvents[K]) => void handler: (payload: StoreEvents[K]) => void
) => { ) => {
if (event === 'change') { if (event === "change") {
this.listeners.get(watchKey)?.delete(handler as (payload: StoreEvents['change']) => void); this.listeners
.get(watchKey)
?.delete(handler as (payload: StoreEvents["change"]) => void)
} }
}, },
}; }
} }
async close(): Promise<void> { async close(): Promise<void> {
if (this.store) { if (this.store) {
await this.store.close(); await this.store.close()
this.store = null; this.store = null
this.data = {}; this.data = {}
this.listeners.clear(); this.listeners.clear()
TauriStoreManager.instances.delete(this.storePath); TauriStoreManager.instances.delete(this.storePath)
} }
} }
} }
@ -199,28 +216,41 @@ class TauriStoreManager {
export const implementation: VersionedAPI<StoreV1> = { export const implementation: VersionedAPI<StoreV1> = {
version: { major: 1, minor: 0, patch: 0 }, version: { major: 1, minor: 0, patch: 0 },
api: { api: {
id: 'tauri-store', id: "tauri-store",
capabilities: new Set(['permanent', 'structured', 'watch', 'namespace', 'secure']), capabilities: new Set([
"permanent",
"structured",
"watch",
"namespace",
"secure",
]),
async init(storePath: string) { async init(storePath: string) {
try { try {
const manager = TauriStoreManager.new(storePath); const manager = TauriStoreManager.new(storePath)
await manager.init(); await manager.init()
return E.right(undefined); return E.right(undefined)
} catch (error) { } catch (error) {
return E.left({ return E.left({
kind: 'storage', kind: "storage",
message: error instanceof Error ? error.message : 'Unknown error', message: error instanceof Error ? error.message : "Unknown error",
cause: error, cause: error,
}); })
} }
}, },
async set(storePath: string, namespace: string, key: string, value: unknown, options?: StorageOptions): Promise<E.Either<StoreError, void>> { async set(
storePath: string,
namespace: string,
key: string,
value: unknown,
options?: StorageOptions
): Promise<E.Either<StoreError, void>> {
try { try {
const manager = TauriStoreManager.new(storePath); const manager = TauriStoreManager.new(storePath)
const existingData = await manager.getRaw(namespace, key); const existingData = await manager.getRaw(namespace, key)
const createdAt = existingData?.metadata.createdAt || new Date().toISOString() const createdAt =
existingData?.metadata.createdAt || new Date().toISOString()
const updatedAt = new Date().toISOString() const updatedAt = new Date().toISOString()
const storedData: StoredData = { const storedData: StoredData = {
@ -234,101 +264,125 @@ export const implementation: VersionedAPI<StoreV1> = {
ttl: options?.ttl, ttl: options?.ttl,
}, },
data: value, data: value,
}; }
await manager.set(namespace, key, storedData); await manager.set(namespace, key, storedData)
return E.right(undefined); return E.right(undefined)
} catch (error) { } catch (error) {
return E.left({ return E.left({
kind: 'storage', kind: "storage",
message: error instanceof Error ? error.message : 'Unknown error', message: error instanceof Error ? error.message : "Unknown error",
cause: error, cause: error,
}); })
} }
}, },
async get<T>(storePath: string, namespace: string, key: string): Promise<E.Either<StoreError, T | undefined>> { async get<T>(
storePath: string,
namespace: string,
key: string
): Promise<E.Either<StoreError, T | undefined>> {
try { try {
const manager = TauriStoreManager.new(storePath); const manager = TauriStoreManager.new(storePath)
return E.right(await manager.get<T>(namespace, key)); return E.right(await manager.get<T>(namespace, key))
} catch (error) { } catch (error) {
return E.left({ return E.left({
kind: 'storage', kind: "storage",
message: error instanceof Error ? error.message : 'Unknown error', message: error instanceof Error ? error.message : "Unknown error",
cause: error, cause: error,
}); })
} }
}, },
async has(storePath: string, namespace: string, key: string): Promise<E.Either<StoreError, boolean>> { async has(
storePath: string,
namespace: string,
key: string
): Promise<E.Either<StoreError, boolean>> {
try { try {
const manager = TauriStoreManager.new(storePath); const manager = TauriStoreManager.new(storePath)
return E.right(await manager.has(namespace, key)); return E.right(await manager.has(namespace, key))
} catch (error) { } catch (error) {
return E.left({ return E.left({
kind: 'storage', kind: "storage",
message: error instanceof Error ? error.message : 'Unknown error', message: error instanceof Error ? error.message : "Unknown error",
cause: error, cause: error,
}); })
} }
}, },
async remove(storePath: string, namespace: string, key: string): Promise<E.Either<StoreError, boolean>> { async remove(
storePath: string,
namespace: string,
key: string
): Promise<E.Either<StoreError, boolean>> {
try { try {
const manager = TauriStoreManager.new(storePath); const manager = TauriStoreManager.new(storePath)
return E.right(await manager.delete(namespace, key)); return E.right(await manager.delete(namespace, key))
} catch (error) { } catch (error) {
return E.left({ return E.left({
kind: 'storage', kind: "storage",
message: error instanceof Error ? error.message : 'Unknown error', message: error instanceof Error ? error.message : "Unknown error",
cause: error, cause: error,
}); })
} }
}, },
async clear(storePath: string, namespace?: string): Promise<E.Either<StoreError, void>> { async clear(
storePath: string,
namespace?: string
): Promise<E.Either<StoreError, void>> {
try { try {
const manager = TauriStoreManager.new(storePath); const manager = TauriStoreManager.new(storePath)
await manager.clear(namespace); await manager.clear(namespace)
return E.right(undefined); return E.right(undefined)
} catch (error) { } catch (error) {
return E.left({ return E.left({
kind: 'storage', kind: "storage",
message: error instanceof Error ? error.message : 'Unknown error', message: error instanceof Error ? error.message : "Unknown error",
cause: error, cause: error,
}); })
} }
}, },
async listNamespaces(storePath: string): Promise<E.Either<StoreError, string[]>> { async listNamespaces(
storePath: string
): Promise<E.Either<StoreError, string[]>> {
try { try {
const manager = TauriStoreManager.new(storePath); const manager = TauriStoreManager.new(storePath)
return E.right(await manager.listNamespaces()); return E.right(await manager.listNamespaces())
} catch (error) { } catch (error) {
return E.left({ return E.left({
kind: 'storage', kind: "storage",
message: error instanceof Error ? error.message : 'Unknown error', message: error instanceof Error ? error.message : "Unknown error",
cause: error, cause: error,
}); })
} }
}, },
async listKeys(storePath: string, namespace: string): Promise<E.Either<StoreError, string[]>> { async listKeys(
storePath: string,
namespace: string
): Promise<E.Either<StoreError, string[]>> {
try { try {
const manager = TauriStoreManager.new(storePath); const manager = TauriStoreManager.new(storePath)
return E.right(await manager.listKeys(namespace)); return E.right(await manager.listKeys(namespace))
} catch (error) { } catch (error) {
return E.left({ return E.left({
kind: 'storage', kind: "storage",
message: error instanceof Error ? error.message : 'Unknown error', message: error instanceof Error ? error.message : "Unknown error",
cause: error, cause: error,
}); })
} }
}, },
async watch(storePath: string, namespace: string, key: string): Promise<StoreEventEmitter<StoreEvents>> { async watch(
const manager = TauriStoreManager.new(storePath); storePath: string,
return manager.watch(namespace, key); namespace: string,
key: string
): Promise<StoreEventEmitter<StoreEvents>> {
const manager = TauriStoreManager.new(storePath)
return manager.watch(namespace, key)
}, },
}, },
}; }

View file

@ -1,4 +1,4 @@
import { implementation as storeV1 } from './v/1' import { implementation as storeV1 } from "./v/1"
export const STORE_IMPLS = { export const STORE_IMPLS = {
v1: storeV1, v1: storeV1,

View file

@ -1,154 +1,176 @@
import * as E from 'fp-ts/Either'; import * as E from "fp-ts/Either"
import superjson from 'superjson'; import superjson from "superjson"
import type { VersionedAPI } from '@type/versioning'; import type { VersionedAPI } from "@type/versioning"
import { import {
StoreV1, StoreV1,
StoredData, StoredData,
StoredDataSchema, StoredDataSchema,
StoreEvents, StoreEvents,
StoreEventEmitter, StoreEventEmitter,
} from '@store/v/1'; } from "@store/v/1"
class BrowserStoreManager { class BrowserStoreManager {
private static instance: BrowserStoreManager; private static instance: BrowserStoreManager
private listeners = new Map<string, Set<(payload: StoreEvents['change']) => void>>(); private listeners = new Map<
string,
Set<(payload: StoreEvents["change"]) => void>
>()
private constructor() {} private constructor() {}
static new(): BrowserStoreManager { static new(): BrowserStoreManager {
if (!BrowserStoreManager.instance) { if (!BrowserStoreManager.instance) {
BrowserStoreManager.instance = new BrowserStoreManager(); BrowserStoreManager.instance = new BrowserStoreManager()
} }
return BrowserStoreManager.instance; return BrowserStoreManager.instance
} }
private getFullKey(namespace: string, key: string): string { private getFullKey(namespace: string, key: string): string {
return `${namespace}:${key}`; return `${namespace}:${key}`
} }
private notifyListeners(namespace: string, key: string, value?: unknown) { private notifyListeners(namespace: string, key: string, value?: unknown) {
const fullKey = this.getFullKey(namespace, key); const fullKey = this.getFullKey(namespace, key)
const listeners = this.listeners.get(fullKey) || new Set(); const listeners = this.listeners.get(fullKey) || new Set()
listeners.forEach(listener => listener({ namespace, key, value })); listeners.forEach((listener) => listener({ namespace, key, value }))
} }
async set(namespace: string, key: string, value: StoredData): Promise<void> { async set(namespace: string, key: string, value: StoredData): Promise<void> {
const validated = StoredDataSchema.parse(value); const validated = StoredDataSchema.parse(value)
localStorage.setItem(this.getFullKey(namespace, key), superjson.stringify(validated)); localStorage.setItem(
this.notifyListeners(namespace, key, validated.data); this.getFullKey(namespace, key),
superjson.stringify(validated)
)
this.notifyListeners(namespace, key, validated.data)
} }
async getRaw(namespace: string, key: string): Promise<StoredData | undefined> { async getRaw(
const rawValue = localStorage.getItem(this.getFullKey(namespace, key)); namespace: string,
if (!rawValue) return undefined; key: string
): Promise<StoredData | undefined> {
const rawValue = localStorage.getItem(this.getFullKey(namespace, key))
if (!rawValue) return undefined
const parsed = superjson.parse(rawValue); const parsed = superjson.parse(rawValue)
const validated = StoredDataSchema.parse(parsed); const validated = StoredDataSchema.parse(parsed)
return validated; return validated
} }
async get<T>(namespace: string, key: string): Promise<T | undefined> { async get<T>(namespace: string, key: string): Promise<T | undefined> {
const storedData = await this.getRaw(namespace, key); const storedData = await this.getRaw(namespace, key)
return storedData?.data as T; return storedData?.data as T
} }
async has(namespace: string, key: string): Promise<boolean> { async has(namespace: string, key: string): Promise<boolean> {
return localStorage.getItem(this.getFullKey(namespace, key)) !== null; return localStorage.getItem(this.getFullKey(namespace, key)) !== null
} }
async delete(namespace: string, key: string): Promise<boolean> { async delete(namespace: string, key: string): Promise<boolean> {
const exists = await this.has(namespace, key); const exists = await this.has(namespace, key)
if (exists) { if (exists) {
localStorage.removeItem(this.getFullKey(namespace, key)); localStorage.removeItem(this.getFullKey(namespace, key))
this.notifyListeners(namespace, key, undefined); this.notifyListeners(namespace, key, undefined)
} }
return exists; return exists
} }
async clear(namespace?: string): Promise<void> { async clear(namespace?: string): Promise<void> {
if (namespace) { if (namespace) {
const keysToRemove = Object.keys(localStorage).filter(key => key.startsWith(`${namespace}:`)); const keysToRemove = Object.keys(localStorage).filter((key) =>
keysToRemove.forEach(key => localStorage.removeItem(key)); key.startsWith(`${namespace}:`)
)
keysToRemove.forEach((key) => localStorage.removeItem(key))
} else { } else {
localStorage.clear(); localStorage.clear()
} }
this.listeners.clear(); this.listeners.clear()
} }
async listNamespaces(): Promise<string[]> { async listNamespaces(): Promise<string[]> {
const namespaces = new Set<string>(); const namespaces = new Set<string>()
Object.keys(localStorage).forEach(key => { Object.keys(localStorage).forEach((key) => {
const [namespace] = key.split(':'); const [namespace] = key.split(":")
namespaces.add(namespace); namespaces.add(namespace)
}); })
return Array.from(namespaces); return Array.from(namespaces)
} }
async listKeys(namespace: string): Promise<string[]> { async listKeys(namespace: string): Promise<string[]> {
return Object.keys(localStorage) return Object.keys(localStorage)
.filter(key => key.startsWith(`${namespace}:`)) .filter((key) => key.startsWith(`${namespace}:`))
.map(key => key.replace(`${namespace}:`, '')); .map((key) => key.replace(`${namespace}:`, ""))
} }
async watch(namespace: string, key: string): Promise<StoreEventEmitter<StoreEvents>> { async watch(
const fullKey = this.getFullKey(namespace, key); namespace: string,
key: string
): Promise<StoreEventEmitter<StoreEvents>> {
const fullKey = this.getFullKey(namespace, key)
return { return {
on: (event, handler) => { on: (event, handler) => {
if (event !== 'change') return () => {}; if (event !== "change") return () => {}
if (!this.listeners.has(fullKey)) { if (!this.listeners.has(fullKey)) {
this.listeners.set(fullKey, new Set()); this.listeners.set(fullKey, new Set())
} }
this.listeners.get(fullKey)!.add(handler as (payload: StoreEvents['change']) => void); this.listeners
return () => this.listeners.get(fullKey)?.delete(handler as (payload: StoreEvents['change']) => void); .get(fullKey)!
.add(handler as (payload: StoreEvents["change"]) => void)
return () =>
this.listeners
.get(fullKey)
?.delete(handler as (payload: StoreEvents["change"]) => void)
}, },
once: (event, handler) => { once: (event, handler) => {
if (event !== 'change') return () => {}; if (event !== "change") return () => {}
const wrapper = (payload: StoreEvents['change']) => { const wrapper = (payload: StoreEvents["change"]) => {
handler(payload); handler(payload)
this.listeners.get(fullKey)?.delete(wrapper); this.listeners.get(fullKey)?.delete(wrapper)
};
if (!this.listeners.has(fullKey)) {
this.listeners.set(fullKey, new Set());
} }
this.listeners.get(fullKey)!.add(wrapper); if (!this.listeners.has(fullKey)) {
return () => this.listeners.get(fullKey)?.delete(wrapper); this.listeners.set(fullKey, new Set())
}
this.listeners.get(fullKey)!.add(wrapper)
return () => this.listeners.get(fullKey)?.delete(wrapper)
}, },
off: (event, handler) => { off: (event, handler) => {
if (event === 'change') { if (event === "change") {
this.listeners.get(fullKey)?.delete(handler as (payload: StoreEvents['change']) => void); this.listeners
.get(fullKey)
?.delete(handler as (payload: StoreEvents["change"]) => void)
} }
}, },
}; }
} }
} }
export const implementation: VersionedAPI<StoreV1> = { export const implementation: VersionedAPI<StoreV1> = {
version: { major: 1, minor: 0, patch: 0 }, version: { major: 1, minor: 0, patch: 0 },
api: { api: {
id: 'browser-store', id: "browser-store",
capabilities: new Set(['permanent', 'structured', 'watch', 'namespace']), capabilities: new Set(["permanent", "structured", "watch", "namespace"]),
// `init` and other methods in `web` don't `storePath` // `init` and other methods in `web` don't `storePath`
// but having a consistent API where first param of every method // but having a consistent API where first param of every method
// is the path that filteres to the "realm" makes it easier to reason around // is the path that filteres to the "realm" makes it easier to reason around
async init(_storePath) { async init(_storePath) {
try { try {
return E.right(undefined); return E.right(undefined)
} catch (e) { } catch (e) {
return E.left({ return E.left({
kind: 'storage', kind: "storage",
message: e instanceof Error ? e.message : 'Unknown error', message: e instanceof Error ? e.message : "Unknown error",
cause: e, cause: e,
}); })
} }
}, },
async set(_storePath, namespace, key, value, options) { async set(_storePath, namespace, key, value, options) {
try { try {
const manager = BrowserStoreManager.new(); const manager = BrowserStoreManager.new()
const existingData = await manager.getRaw(namespace, key); const existingData = await manager.getRaw(namespace, key)
const createdAt = existingData?.metadata.createdAt || new Date().toISOString() const createdAt =
existingData?.metadata.createdAt || new Date().toISOString()
const updatedAt = new Date().toISOString() const updatedAt = new Date().toISOString()
const storedData: StoredData = { const storedData: StoredData = {
@ -162,101 +184,101 @@ export const implementation: VersionedAPI<StoreV1> = {
ttl: options?.ttl, ttl: options?.ttl,
}, },
data: value, data: value,
}; }
await manager.set(namespace, key, storedData); await manager.set(namespace, key, storedData)
return E.right(undefined); return E.right(undefined)
} catch (e) { } catch (e) {
return E.left({ return E.left({
kind: 'storage', kind: "storage",
message: e instanceof Error ? e.message : 'Unknown error', message: e instanceof Error ? e.message : "Unknown error",
cause: e, cause: e,
}); })
} }
}, },
async get(_storePath, namespace, key) { async get(_storePath, namespace, key) {
try { try {
const manager = BrowserStoreManager.new(); const manager = BrowserStoreManager.new()
return E.right(await manager.get(namespace, key)); return E.right(await manager.get(namespace, key))
} catch (e) { } catch (e) {
return E.left({ return E.left({
kind: 'storage', kind: "storage",
message: e instanceof Error ? e.message : 'Unknown error', message: e instanceof Error ? e.message : "Unknown error",
cause: e, cause: e,
}); })
} }
}, },
async has(_storePath, namespace, key) { async has(_storePath, namespace, key) {
try { try {
const manager = BrowserStoreManager.new(); const manager = BrowserStoreManager.new()
return E.right(await manager.has(namespace, key)); return E.right(await manager.has(namespace, key))
} catch (e) { } catch (e) {
return E.left({ return E.left({
kind: 'storage', kind: "storage",
message: e instanceof Error ? e.message : 'Unknown error', message: e instanceof Error ? e.message : "Unknown error",
cause: e, cause: e,
}); })
} }
}, },
async remove(_storePath, namespace, key) { async remove(_storePath, namespace, key) {
try { try {
const manager = BrowserStoreManager.new(); const manager = BrowserStoreManager.new()
return E.right(await manager.delete(namespace, key)); return E.right(await manager.delete(namespace, key))
} catch (e) { } catch (e) {
return E.left({ return E.left({
kind: 'storage', kind: "storage",
message: e instanceof Error ? e.message : 'Unknown error', message: e instanceof Error ? e.message : "Unknown error",
cause: e, cause: e,
}); })
} }
}, },
async clear(_storePath, namespace) { async clear(_storePath, namespace) {
try { try {
const manager = BrowserStoreManager.new(); const manager = BrowserStoreManager.new()
await manager.clear(namespace); await manager.clear(namespace)
return E.right(undefined); return E.right(undefined)
} catch (e) { } catch (e) {
return E.left({ return E.left({
kind: 'storage', kind: "storage",
message: e instanceof Error ? e.message : 'Unknown error', message: e instanceof Error ? e.message : "Unknown error",
cause: e, cause: e,
}); })
} }
}, },
async listNamespaces(_storePath){ async listNamespaces(_storePath) {
try { try {
const manager = BrowserStoreManager.new(); const manager = BrowserStoreManager.new()
return E.right(await manager.listNamespaces()); return E.right(await manager.listNamespaces())
} catch (e) { } catch (e) {
return E.left({ return E.left({
kind: 'storage', kind: "storage",
message: e instanceof Error ? e.message : 'Unknown error', message: e instanceof Error ? e.message : "Unknown error",
cause: e, cause: e,
}); })
} }
}, },
async listKeys(_storePath, namespace) { async listKeys(_storePath, namespace) {
try { try {
const manager = BrowserStoreManager.new(); const manager = BrowserStoreManager.new()
return E.right(await manager.listKeys(namespace)); return E.right(await manager.listKeys(namespace))
} catch (e) { } catch (e) {
return E.left({ return E.left({
kind: 'storage', kind: "storage",
message: e instanceof Error ? e.message : 'Unknown error', message: e instanceof Error ? e.message : "Unknown error",
cause: e, cause: e,
}); })
} }
}, },
async watch(_storePath, namespace, key) { async watch(_storePath, namespace, key) {
const manager = BrowserStoreManager.new(); const manager = BrowserStoreManager.new()
return manager.watch(namespace, key); return manager.watch(namespace, key)
}, },
}, },
}; }

View file

@ -1,8 +1,6 @@
import { v1 } from './v/1' import { v1 } from "./v/1"
export type { export type { StoreV1 } from "./v/1"
StoreV1,
} from './v/1'
export const VERSIONS = { export const VERSIONS = {
v1, v1,

View file

@ -1,27 +1,27 @@
import type { VersionedAPI } from '@type/versioning' import type { VersionedAPI } from "@type/versioning"
import * as E from 'fp-ts/Either' import * as E from "fp-ts/Either"
import { z } from 'zod' import { z } from "zod"
export type StoreCapability = export type StoreCapability =
| 'permanent' | "permanent"
| 'temporary' | "temporary"
| 'structured' | "structured"
| 'sync' | "sync"
| 'watch' | "watch"
| 'secure' | "secure"
| 'namespace' | "namespace"
export type StoreError = export type StoreError =
| { kind: 'not_found'; message: string } | { kind: "not_found"; message: string }
| { kind: 'permission'; message: string } | { kind: "permission"; message: string }
| { kind: 'quota'; message: string } | { kind: "quota"; message: string }
| { kind: 'version'; message: string } | { kind: "version"; message: string }
| { kind: 'parse'; message: string; cause?: unknown } | { kind: "parse"; message: string; cause?: unknown }
| { kind: 'storage'; message: string; cause?: unknown } | { kind: "storage"; message: string; cause?: unknown }
| { kind: 'encrypt'; message: string; cause?: unknown } | { kind: "encrypt"; message: string; cause?: unknown }
export interface StoreFile { export interface StoreFile {
include?: boolean, include?: boolean
name: string name: string
size: number size: number
@ -48,13 +48,15 @@ export interface StoreEvents {
export const StoreMetadataSchema = z.object({ export const StoreMetadataSchema = z.object({
version: z.number(), version: z.number(),
lastUpdated: z.string().datetime(), lastUpdated: z.string().datetime(),
namespaces: z.record(z.object({ namespaces: z.record(
z.object({
name: z.string(), name: z.string(),
version: z.number(), version: z.number(),
createdAt: z.string().datetime(), createdAt: z.string().datetime(),
updatedAt: z.string().datetime(), updatedAt: z.string().datetime(),
keys: z.array(z.string()) keys: z.array(z.string()),
})) })
),
}) })
export type StoreMetadata = z.infer<typeof StoreMetadataSchema> export type StoreMetadata = z.infer<typeof StoreMetadataSchema>
@ -67,16 +69,19 @@ export const StoredDataSchema = z.object({
namespace: z.string(), namespace: z.string(),
encrypted: z.boolean().optional(), encrypted: z.boolean().optional(),
compressed: z.boolean().optional(), compressed: z.boolean().optional(),
ttl: z.number().optional() ttl: z.number().optional(),
}), }),
data: z.unknown() data: z.unknown(),
}) })
export type StoredData = z.infer<typeof StoredDataSchema> export type StoredData = z.infer<typeof StoredDataSchema>
export interface StoreEventEmitter<T> { export interface StoreEventEmitter<T> {
on<K extends keyof T>(event: K, handler: (payload: T[K]) => void): () => void on<K extends keyof T>(event: K, handler: (payload: T[K]) => void): () => void
once<K extends keyof T>(event: K, handler: (payload: T[K]) => void): () => void once<K extends keyof T>(
event: K,
handler: (payload: T[K]) => void
): () => void
off<K extends keyof T>(event: K, handler: (payload: T[K]) => void): void off<K extends keyof T>(event: K, handler: (payload: T[K]) => void): void
} }
@ -85,34 +90,64 @@ export interface StoreV1 {
readonly capabilities: Set<StoreCapability> readonly capabilities: Set<StoreCapability>
init(storePath: string): Promise<E.Either<StoreError, void>> init(storePath: string): Promise<E.Either<StoreError, void>>
set(storePath: string, namespace: string, key: string, value: unknown, options?: StorageOptions): Promise<E.Either<StoreError, void>> set(
get<T>(storePath: string, namespace: string, key: string): Promise<E.Either<StoreError, T | undefined>> storePath: string,
remove(storePath: string, namespace: string, key: string): Promise<E.Either<StoreError, boolean>> namespace: string,
clear(storePath: string, namespace?: string): Promise<E.Either<StoreError, void>> key: string,
has(storePath: string, namespace: string, key: string): Promise<E.Either<StoreError, boolean>> value: unknown,
options?: StorageOptions
): Promise<E.Either<StoreError, void>>
get<T>(
storePath: string,
namespace: string,
key: string
): Promise<E.Either<StoreError, T | undefined>>
remove(
storePath: string,
namespace: string,
key: string
): Promise<E.Either<StoreError, boolean>>
clear(
storePath: string,
namespace?: string
): Promise<E.Either<StoreError, void>>
has(
storePath: string,
namespace: string,
key: string
): Promise<E.Either<StoreError, boolean>>
listNamespaces(storePath: string): Promise<E.Either<StoreError, string[]>> listNamespaces(storePath: string): Promise<E.Either<StoreError, string[]>>
listKeys(storePath: string, namespace: string): Promise<E.Either<StoreError, string[]>> listKeys(
watch(storePath: string, namespace: string, key: string): Promise<StoreEventEmitter<StoreEvents>> storePath: string,
namespace: string
): Promise<E.Either<StoreError, string[]>>
watch(
storePath: string,
namespace: string,
key: string
): Promise<StoreEventEmitter<StoreEvents>>
} }
export const v1: VersionedAPI<StoreV1> = { export const v1: VersionedAPI<StoreV1> = {
version: { major: 1, minor: 0, patch: 0 }, version: { major: 1, minor: 0, patch: 0 },
api: { api: {
id: 'default', id: "default",
capabilities: new Set(), capabilities: new Set(),
init: async () => E.left({ kind: 'version', message: 'Not implemented' }), init: async () => E.left({ kind: "version", message: "Not implemented" }),
set: async () => E.left({ kind: 'version', message: 'Not implemented' }), set: async () => E.left({ kind: "version", message: "Not implemented" }),
get: async () => E.left({ kind: 'version', message: 'Not implemented' }), get: async () => E.left({ kind: "version", message: "Not implemented" }),
remove: async () => E.left({ kind: 'version', message: 'Not implemented' }), remove: async () => E.left({ kind: "version", message: "Not implemented" }),
clear: async () => E.left({ kind: 'version', message: 'Not implemented' }), clear: async () => E.left({ kind: "version", message: "Not implemented" }),
has: async () => E.left({ kind: 'version', message: 'Not implemented' }), has: async () => E.left({ kind: "version", message: "Not implemented" }),
listNamespaces: async () => E.left({ kind: 'version', message: 'Not implemented' }), listNamespaces: async () =>
listKeys: async () => E.left({ kind: 'version', message: 'Not implemented' }), E.left({ kind: "version", message: "Not implemented" }),
listKeys: async () =>
E.left({ kind: "version", message: "Not implemented" }),
watch: async () => ({ watch: async () => ({
on: () => () => {}, on: () => () => {},
once: () => () => {}, once: () => () => {},
off: () => {} off: () => {},
}) }),
} },
} }

View file

@ -1,6 +1,9 @@
import type { Version } from '@type/versioning' import type { Version } from "@type/versioning"
export function checkCapability(required: Version, available: Version): boolean { export function checkCapability(
required: Version,
available: Version
): boolean {
if (available.major !== required.major) return false if (available.major !== required.major) return false
if (available.minor < required.minor) return false if (available.minor < required.minor) return false
return true return true

View file

@ -1,73 +0,0 @@
/* eslint-env node */
require("@rushstack/eslint-patch/modern-module-resolution")
module.exports = {
root: true,
env: {
browser: true,
node: true,
jest: true,
},
parserOptions: {
sourceType: "module",
requireConfigFile: false,
ecmaFeatures: {
jsx: false,
},
},
extends: [
"@vue/typescript/recommended",
"plugin:vue/recommended",
"plugin:prettier/recommended",
],
ignorePatterns: [
"static/**/*",
"./helpers/backend/graphql.ts",
"**/*.d.ts",
"types/**/*",
],
plugins: ["vue", "prettier"],
// add your custom rules here
rules: {
semi: [2, "never"],
"import/named": "off", // because, named import issue with typescript see: https://github.com/typescript-eslint/typescript-eslint/issues/154
"no-console": "off",
"no-debugger": process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
"prettier/prettier":
process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
"vue/multi-word-component-names": "off",
"vue/no-side-effects-in-computed-properties": "off",
"import/no-named-as-default": "off",
"import/no-named-as-default-member": "off",
"@typescript-eslint/no-unused-vars": [
process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
}
],
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-explicit-any": "off",
"import/default": "off",
"no-undef": "off",
// localStorage block
"no-restricted-globals": [
"error",
{
name: "localStorage",
message:
"Do not use 'localStorage' directly. Please use localpersistence.ts functions or stores",
},
],
// window.localStorage block
"no-restricted-syntax": [
"error",
{
selector: "CallExpression[callee.object.property.name='localStorage']",
message:
"Do not use 'localStorage' directly. Please use localpersistence.ts functions or stores",
},
],
},
}

View file

@ -0,0 +1,85 @@
import pluginVue from "eslint-plugin-vue"
import {
defineConfigWithVueTs,
vueTsConfigs,
} from "@vue/eslint-config-typescript"
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"
import globals from "globals"
export default defineConfigWithVueTs(
{
ignores: [
"static/**",
"src/api/generated/**",
"**/*.d.ts",
"types/**",
"dist/**",
"node_modules/**",
],
},
pluginVue.configs["flat/recommended"],
vueTsConfigs.recommended,
eslintPluginPrettierRecommended,
{
files: ["**/*.ts", "**/*.js", "**/*.vue"],
linterOptions: {
reportUnusedDisableDirectives: false,
},
languageOptions: {
sourceType: "module",
ecmaVersion: "latest",
globals: {
...globals.browser,
...globals.node,
},
parserOptions: {
requireConfigFile: false,
ecmaFeatures: {
jsx: false,
},
},
},
rules: {
semi: [2, "never"],
"import/named": "off",
"no-console": "off",
"no-debugger": process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
"prettier/prettier":
process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
"vue/multi-word-component-names": "off",
"vue/no-side-effects-in-computed-properties": "off",
"import/no-named-as-default": "off",
"import/no-named-as-default-member": "off",
"@typescript-eslint/no-unused-vars": [
process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
"@typescript-eslint/no-unused-expressions": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unsafe-function-type": "off",
"import/default": "off",
"no-undef": "off",
"no-restricted-globals": [
"error",
{
name: "localStorage",
message:
"Do not use 'localStorage' directly. Please use the PersistenceService",
},
],
"no-restricted-syntax": [
"error",
{
selector: "CallExpression[callee.object.property.name='localStorage']",
message:
"Do not use 'localStorage' directly. Please use the PersistenceService",
},
],
},
}
)

View file

@ -9,9 +9,9 @@
"dev": "pnpm exec npm-run-all -p -l dev:*", "dev": "pnpm exec npm-run-all -p -l dev:*",
"build": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build", "build": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build",
"preview": "vite preview", "preview": "vite preview",
"lint": "eslint src --ext .ts,.js,.vue --ignore-path .gitignore .", "lint": "eslint src",
"lint:ts": "vue-tsc --noEmit", "lint:ts": "vue-tsc --noEmit",
"lintfix": "eslint --fix src --ext .ts,.js,.vue --ignore-path .gitignore .", "lintfix": "eslint --fix src",
"prod-lint": "cross-env HOPP_LINT_FOR_PROD=true pnpm run lint", "prod-lint": "cross-env HOPP_LINT_FOR_PROD=true pnpm run lint",
"generate": "pnpm run build", "generate": "pnpm run build",
"do-dev": "pnpm run dev", "do-dev": "pnpm run dev",
@ -51,6 +51,8 @@
"zod": "3.25.32" "zod": "3.25.32"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "3.3.3",
"@eslint/js": "9.39.2",
"@graphql-codegen/add": "6.0.0", "@graphql-codegen/add": "6.0.0",
"@graphql-codegen/cli": "6.1.0", "@graphql-codegen/cli": "6.1.0",
"@graphql-codegen/typed-document-node": "6.1.5", "@graphql-codegen/typed-document-node": "6.1.5",
@ -66,13 +68,14 @@
"@typescript-eslint/parser": "8.50.0", "@typescript-eslint/parser": "8.50.0",
"@vitejs/plugin-legacy": "7.2.1", "@vitejs/plugin-legacy": "7.2.1",
"@vitejs/plugin-vue": "6.0.3", "@vitejs/plugin-vue": "6.0.3",
"@vue/eslint-config-typescript": "13.0.0", "@vue/eslint-config-typescript": "14.6.0",
"autoprefixer": "10.4.23", "autoprefixer": "10.4.23",
"cross-env": "10.1.0", "cross-env": "10.1.0",
"dotenv": "17.2.3", "dotenv": "17.2.3",
"eslint": "8.57.0", "eslint": "9.39.2",
"eslint-plugin-prettier": "5.5.4", "eslint-plugin-prettier": "5.5.4",
"eslint-plugin-vue": "10.6.2", "eslint-plugin-vue": "10.6.2",
"globals": "16.5.0",
"npm-run-all": "4.1.5", "npm-run-all": "4.1.5",
"postcss": "8.5.6", "postcss": "8.5.6",
"prettier-plugin-tailwindcss": "0.7.1", "prettier-plugin-tailwindcss": "0.7.1",

View file

@ -139,7 +139,7 @@ async function getInitialUserDetails(): Promise<
} }
} }
return { error: "auth/cookies_not_found" } return { error: "auth/cookies_not_found" }
} catch (error) { } catch (_error) {
return { error: "auth/cookies_not_found" } return { error: "auth/cookies_not_found" }
} }
} }
@ -254,7 +254,7 @@ async function refreshToken() {
} }
return isSuccessful return isSuccessful
} catch (err) { } catch (_err) {
return false return false
} }
} }

View file

@ -167,7 +167,7 @@ async function refreshToken() {
} }
return isSuccessful return isSuccessful
} catch (error) { } catch (_error) {
return false return false
} }
} }
@ -394,7 +394,7 @@ export const def: AuthPlatformDef = {
// axios automatically throws on error status codes, so if we reach here, it was successful // axios automatically throws on error status codes, so if we reach here, it was successful
return !!response.data.isValid return !!response.data.isValid
} catch (error) { } catch (_error) {
return false return false
} }
}, },

File diff suppressed because it is too large Load diff