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>
@ -47,12 +59,12 @@
/> />
</template> </template>
<HoppButtonPrimary <HoppButtonPrimary
v-else v-else
label="Minimize to Tray" label="Minimize to Tray"
outline outline
@click="hideWindow" @click="hideWindow"
/> />
</div> </div>
</div> </div>
</div> </div>
@ -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

@ -4,12 +4,14 @@
<div class="overflow-auto"> <div class="overflow-auto">
<HoppSmartTable <HoppSmartTable
:headings="[ :headings="[
{ key: 'auth_key_hash', label: 'ID' }, { key: 'auth_key_hash', label: 'ID' },
{ key: 'registered_at', label: 'Registered At' }, { key: 'registered_at', label: 'Registered At' },
]" ]"
: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",
// @ts-expect-error TS is not able to understand the type semantics here
payload: { payload: {
// @ts-expect-error TS is not able to understand the type semantics here
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,90 +1,97 @@
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 },
api: { api: {
async saveFileWithDialog(opts: SaveFileWithDialogOptions) { async saveFileWithDialog(opts: SaveFileWithDialogOptions) {
// TODO: Revisit this because perhaps a better approach is // TODO: Revisit this because perhaps a better approach is
// ```ts // ```ts
// const data: BlobPart = typeof opts.data === 'string' // const data: BlobPart = typeof opts.data === 'string'
// ? opts.data // ? opts.data
// : new Uint8Array(opts.data); // : new Uint8Array(opts.data);
// const file = new Blob([data], { type: opts.contentType }) // const file = new Blob([data], { type: opts.contentType })
// ``` // ```
const file = new Blob([opts.data as BlobPart], { type: opts.contentType }) const file = new Blob([opts.data as BlobPart], { type: opts.contentType })
const a = document.createElement("a") const a = document.createElement("a")
const url = URL.createObjectURL(file) const url = URL.createObjectURL(file)
a.href = url a.href = url
a.download = opts.suggestedFilename ?? pipe( a.download =
url, opts.suggestedFilename ??
S.split("/"), pipe(
RNEA.last, url,
S.split("#"), S.split("/"),
RNEA.head, RNEA.last,
S.split("?"), S.split("#"),
RNEA.head RNEA.head,
) S.split("?"),
RNEA.head
)
document.body.appendChild(a) document.body.appendChild(a)
a.click() a.click()
setTimeout(() => { setTimeout(() => {
document.body.removeChild(a) document.body.removeChild(a)
URL.revokeObjectURL(url) URL.revokeObjectURL(url)
}, 1000) }, 1000)
// Browsers provide no way to know if save was successful // Browsers provide no way to know if save was successful
return { type: "unknown" as const } return { type: "unknown" as const }
}, },
async openExternalLink(opts: OpenExternalLinkOptions) { async openExternalLink(opts: OpenExternalLinkOptions) {
window.open(opts.url, "_blank") window.open(opts.url, "_blank")
// Browsers provide no way to know if save was successful // Browsers provide no way to know if save was successful
return { type: "unknown" as const } return { type: "unknown" as const }
}, },
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)
return () => window.removeEventListener('hashchange', listener)
},
async once<T>(event: string, handler: (event: Event<T>) => void) {
const listener = (e: HashChangeEvent) => {
const hash = window.location.hash
if (hash && hash.startsWith(`#${event}:`)) {
const payload = hash.slice(event.length + 2) // Remove #event:
window.removeEventListener('hashchange', listener)
handler({
event,
id: Date.now(),
payload: JSON.parse(payload) as T
})
}
}
window.addEventListener('hashchange', listener)
return () => window.removeEventListener('hashchange', listener)
},
async emit(event: string, payload?: unknown) {
window.location.hash = `${event}:${JSON.stringify(payload)}`
} }
} }
window.addEventListener("hashchange", listener)
return () => window.removeEventListener("hashchange", listener)
},
async once<T>(event: string, handler: (event: Event<T>) => void) {
const listener = (_e: HashChangeEvent) => {
const hash = window.location.hash
if (hash && hash.startsWith(`#${event}:`)) {
const payload = hash.slice(event.length + 2) // Remove #event:
window.removeEventListener("hashchange", listener)
handler({
event,
id: Date.now(),
payload: JSON.parse(payload) as T,
})
}
}
window.addEventListener("hashchange", listener)
return () => window.removeEventListener("hashchange", listener)
},
async emit(event: string, payload?: unknown) {
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,200 +1,202 @@
import type { VersionedAPI } from '@type/versioning' import type { VersionedAPI } from "@type/versioning"
import { import {
type RelayV1, type RelayV1,
type RelayRequest, type RelayRequest,
type RelayRequestEvents, type RelayRequestEvents,
type RelayEventEmitter, type RelayEventEmitter,
type RelayResponse, type RelayResponse,
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([ header: new Set(["stringvalue", "arrayvalue", "multivalue"]),
'stringvalue', content: new Set([
'arrayvalue', "text",
'multivalue' "json",
]), "xml",
content: new Set([ "form",
'text', "binary",
'json', "multipart",
'xml', "urlencoded",
'form', "stream",
'binary', "compression",
'multipart', ]),
'urlencoded', auth: new Set(["basic", "bearer", "digest", "oauth2", "apikey"]),
'stream', security: new Set([
'compression' "clientcertificates",
]), "cacertificates",
auth: new Set([ "certificatevalidation",
'basic', "hostverification",
'bearer', "peerverification",
'digest', ]),
'oauth2', proxy: new Set(["http", "https", "authentication", "certificates"]),
'apikey' advanced: new Set([
]), "retry",
security: new Set([ "redirects",
'clientcertificates', "timeout",
'cacertificates', "cookies",
'certificatevalidation', "keepalive",
'hostverification', "tcpoptions",
'peerverification' "http2",
]), "http3",
proxy: new Set([ ]),
'http', },
'https',
'authentication', canHandle(request: RelayRequest) {
'certificates' if (!this.capabilities.method.has(request.method)) {
]), return E.left({
advanced: new Set([ kind: "unsupported_feature",
'retry', feature: "method",
'redirects', message: `Method ${request.method} is not supported`,
'timeout', relay: "desktop",
'cookies', })
'keepalive', }
'tcpoptions',
'http2', if (
'http3' request.content &&
]) !this.capabilities.content.has(request.content.kind)
) {
return E.left({
kind: "unsupported_feature",
feature: "content",
message: `Content type ${request.content.kind} is not supported`,
relay: "desktop",
})
}
if (request.auth && !this.capabilities.auth.has(request.auth.kind)) {
return E.left({
kind: "unsupported_feature",
feature: "authentication",
message: `Authentication type ${request.auth.kind} is not supported`,
relay: "desktop",
})
}
if (
request.security?.certificates &&
!this.capabilities.security.has("clientcertificates")
) {
return E.left({
kind: "unsupported_feature",
feature: "security",
message: "Client certificates are not supported",
relay: "desktop",
})
}
if (
request.proxy &&
!this.capabilities.proxy.has(
request.proxy.url.startsWith("https") ? "https" : "http"
)
) {
return E.left({
kind: "unsupported_feature",
feature: "proxy",
message: `Proxy protocol ${request.proxy.url.split(":")[0]} is not supported`,
relay: "desktop",
})
}
return E.right(true)
},
execute(request: RelayRequest) {
const emitter: RelayEventEmitter<RelayRequestEvents> = {
on: () => () => {},
once: () => () => {},
off: () => {},
}
const responsePromise = relayRequestToNativeAdapter(request)
.then((request) => {
// SAFETY: Type assertion is safe because:
// 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
// 3. Authentication methods are validated through capabilities
// 4. The plugin's Request type is a subset of our Request type
const pluginRequest = {
id: request.id,
url: request.url,
method: request.method,
version: request.version,
headers: request.headers,
params: request.params,
content: request.content,
auth: request.auth,
security: request.security,
proxy: request.proxy,
meta: request.meta,
}
return execute(pluginRequest)
})
.then((result: RequestResult): E.Either<RelayError, RelayResponse> => {
if (result.kind === "success") {
const response: RelayResponse = {
id: result.response.id,
status: result.response.status,
statusText: result.response.statusText,
version: result.response.version,
headers: result.response.headers,
cookies: result.response.cookies,
body: body.body(
result.response.body.body,
result.response.body.mediaType
),
meta: {
timing: {
start: result.response.meta.timing.start,
end: result.response.meta.timing.end,
},
size: result.response.meta.size,
},
}
return E.right(response)
}
return E.left(result.error)
})
.catch((error: unknown): E.Either<RelayError, RelayResponse> => {
const networkError: RelayError = {
kind: "network",
message:
error instanceof Error ? error.message : "Unknown error occurred",
cause: error,
}
return E.left(networkError)
})
return {
cancel: async () => {
await cancel(request.id)
}, },
emitter,
canHandle(request: RelayRequest) { response: responsePromise,
if (!this.capabilities.method.has(request.method)) { }
return E.left({ },
kind: "unsupported_feature", },
feature: "method",
message: `Method ${request.method} is not supported`,
relay: "desktop"
})
}
if (request.content && !this.capabilities.content.has(request.content.kind)) {
return E.left({
kind: "unsupported_feature",
feature: "content",
message: `Content type ${request.content.kind} is not supported`,
relay: "desktop"
})
}
if (request.auth && !this.capabilities.auth.has(request.auth.kind)) {
return E.left({
kind: "unsupported_feature",
feature: "authentication",
message: `Authentication type ${request.auth.kind} is not supported`,
relay: "desktop"
})
}
if (request.security?.certificates && !this.capabilities.security.has('clientcertificates')) {
return E.left({
kind: "unsupported_feature",
feature: "security",
message: "Client certificates are not supported",
relay: "desktop"
})
}
if (request.proxy && !this.capabilities.proxy.has(request.proxy.url.startsWith('https') ? 'https' : 'http')) {
return E.left({
kind: "unsupported_feature",
feature: "proxy",
message: `Proxy protocol ${request.proxy.url.split(':')[0]} is not supported`,
relay: "desktop"
})
}
return E.right(true)
},
execute(request: RelayRequest) {
const emitter: RelayEventEmitter<RelayRequestEvents> = {
on: () => () => {},
once: () => () => {},
off: () => {}
}
const responsePromise = relayRequestToNativeAdapter(request)
.then(request => {
// SAFETY: Type assertion is safe because:
// 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
// 3. Authentication methods are validated through capabilities
// 4. The plugin's Request type is a subset of our Request type
const pluginRequest = {
id: request.id,
url: request.url,
method: request.method,
version: request.version,
headers: request.headers,
params: request.params,
content: request.content,
auth: request.auth,
security: request.security,
proxy: request.proxy,
meta: request.meta,
}
return execute(pluginRequest)
})
.then((result: RequestResult): E.Either<RelayError, RelayResponse> => {
if (result.kind === 'success') {
const response: RelayResponse = {
id: result.response.id,
status: result.response.status,
statusText: result.response.statusText,
version: result.response.version,
headers: result.response.headers,
cookies: result.response.cookies,
body: body.body(result.response.body.body, result.response.body.mediaType),
meta: {
timing: {
start: result.response.meta.timing.start,
end: result.response.meta.timing.end,
},
size: result.response.meta.size,
}
}
return E.right(response)
}
return E.left(result.error)
})
.catch((error: unknown): E.Either<RelayError, RelayResponse> => {
const networkError: RelayError = {
kind: 'network',
message: error instanceof Error ? error.message : 'Unknown error occurred',
cause: error
}
return E.left(networkError)
})
return {
cancel: async () => { await cancel(request.id) },
emitter,
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,11 +1,9 @@
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,
} as const } as const
export const latest = v1 export const latest = v1

File diff suppressed because it is too large Load diff

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,334 +1,388 @@
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,
StoreEvents, StoreEvents,
StorageOptions, StorageOptions,
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 {
if (TauriStoreManager.instances.has(storePath)) {
return TauriStoreManager.instances.get(storePath)!
} }
static new(storePath: string): TauriStoreManager { const instance = new TauriStoreManager(storePath)
if (TauriStoreManager.instances.has(storePath)) { TauriStoreManager.instances.set(storePath, instance)
return TauriStoreManager.instances.get(storePath)!; return instance
}
static async closeAll(): Promise<void> {
const closePromises = Array.from(TauriStoreManager.instances.values()).map(
(instance) => instance.close()
)
await Promise.all(closePromises)
TauriStoreManager.instances.clear()
}
static async closeStore(storePath: string): Promise<void> {
const instance = TauriStoreManager.instances.get(storePath)
if (instance) {
await instance.close()
TauriStoreManager.instances.delete(storePath)
}
}
async init(): Promise<void> {
if (!this.store) {
this.store = await Store.load(this.storePath)
const loadedData = await this.store.get<NamespacedData>("data")
this.data = loadedData ?? {}
this.store.onChange((_, value: NamespacedData | undefined) => {
if (value) {
this.data = value
this.notifyListeners()
}
})
}
}
private notifyListeners(): void {
for (const [key, listeners] of this.listeners.entries()) {
const [namespace, dataKey] = key.split(":")
const value = this.data[namespace]?.[dataKey]
listeners.forEach((listener) =>
listener({
namespace,
key: dataKey,
value: value?.data,
})
)
}
}
async set(namespace: string, key: string, value: StoredData): Promise<void> {
if (!this.store) throw new Error("Store not initialized")
const validated = StoredDataSchema.parse(value)
this.data[namespace] = this.data[namespace] || {}
this.data[namespace][key] = validated
await this.store.set("data", this.data)
await this.store.save()
}
async getRaw(
namespace: string,
key: string
): Promise<StoredData | undefined> {
const rawValue = this.data[namespace]?.[key]
if (!rawValue) return undefined
const validated = StoredDataSchema.parse(rawValue)
return validated
}
async get<T>(namespace: string, key: string): Promise<T | undefined> {
const storedData = await this.getRaw(namespace, key)
return storedData?.data as T | undefined
}
async has(namespace: string, key: string): Promise<boolean> {
return !!this.data[namespace]?.[key]
}
async delete(namespace: string, key: string): Promise<boolean> {
if (!this.store) throw new Error("Store not initialized")
if (this.data[namespace]?.[key]) {
delete this.data[namespace][key]
if (Object.keys(this.data[namespace]).length === 0) {
delete this.data[namespace]
}
await this.store.set("data", this.data)
await this.store.save()
return true
}
return false
}
async clear(namespace?: string): Promise<void> {
if (!this.store) throw new Error("Store not initialized")
if (namespace) {
delete this.data[namespace]
} else {
this.data = {}
}
await this.store.set("data", this.data)
await this.store.save()
}
async listNamespaces(): Promise<string[]> {
return Object.keys(this.data)
}
async listKeys(namespace: string): Promise<string[]> {
return Object.keys(this.data[namespace] || {})
}
async watch(
namespace: string,
key: string
): Promise<StoreEventEmitter<StoreEvents>> {
const watchKey = `${namespace}:${key}`
return {
on: <K extends keyof StoreEvents>(
event: K,
handler: (payload: StoreEvents[K]) => void
) => {
if (event !== "change") return () => {}
if (!this.listeners.has(watchKey)) {
this.listeners.set(watchKey, new Set())
}
this.listeners
.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>(
event: K,
handler: (payload: StoreEvents[K]) => void
) => {
if (event !== "change") return () => {}
const wrapper = (value: StoreEvents["change"]) => {
handler(value as StoreEvents[K])
this.listeners.get(watchKey)?.delete(wrapper)
} }
const instance = new TauriStoreManager(storePath); if (!this.listeners.has(watchKey)) {
TauriStoreManager.instances.set(storePath, instance); this.listeners.set(watchKey, new Set())
return instance;
}
static async closeAll(): Promise<void> {
const closePromises = Array.from(TauriStoreManager.instances.values())
.map(instance => instance.close());
await Promise.all(closePromises);
TauriStoreManager.instances.clear();
}
static async closeStore(storePath: string): Promise<void> {
const instance = TauriStoreManager.instances.get(storePath);
if (instance) {
await instance.close();
TauriStoreManager.instances.delete(storePath);
} }
} this.listeners.get(watchKey)!.add(wrapper)
return () => this.listeners.get(watchKey)?.delete(wrapper)
async init(): Promise<void> { },
if (!this.store) { off: <K extends keyof StoreEvents>(
this.store = await Store.load(this.storePath); event: K,
const loadedData = await this.store.get<NamespacedData>('data'); handler: (payload: StoreEvents[K]) => void
this.data = loadedData ?? {}; ) => {
if (event === "change") {
this.store.onChange((_, value: NamespacedData | undefined) => { this.listeners
if (value) { .get(watchKey)
this.data = value; ?.delete(handler as (payload: StoreEvents["change"]) => void)
this.notifyListeners();
}
});
} }
},
} }
}
private notifyListeners(): void { async close(): Promise<void> {
for (const [key, listeners] of this.listeners.entries()) { if (this.store) {
const [namespace, dataKey] = key.split(':'); await this.store.close()
const value = this.data[namespace]?.[dataKey]; this.store = null
listeners.forEach(listener => this.data = {}
listener({ this.listeners.clear()
namespace, TauriStoreManager.instances.delete(this.storePath)
key: dataKey,
value: value?.data,
})
);
}
}
async set(namespace: string, key: string, value: StoredData): Promise<void> {
if (!this.store) throw new Error('Store not initialized');
const validated = StoredDataSchema.parse(value);
this.data[namespace] = this.data[namespace] || {};
this.data[namespace][key] = validated;
await this.store.set('data', this.data);
await this.store.save();
}
async getRaw(namespace: string, key: string): Promise<StoredData | undefined> {
const rawValue = this.data[namespace]?.[key];
if (!rawValue) return undefined;
const validated = StoredDataSchema.parse(rawValue);
return validated;
}
async get<T>(namespace: string, key: string): Promise<T | undefined> {
const storedData = await this.getRaw(namespace, key);
return storedData?.data as T | undefined;
}
async has(namespace: string, key: string): Promise<boolean> {
return !!this.data[namespace]?.[key];
}
async delete(namespace: string, key: string): Promise<boolean> {
if (!this.store) throw new Error('Store not initialized');
if (this.data[namespace]?.[key]) {
delete this.data[namespace][key];
if (Object.keys(this.data[namespace]).length === 0) {
delete this.data[namespace];
}
await this.store.set('data', this.data);
await this.store.save();
return true;
}
return false;
}
async clear(namespace?: string): Promise<void> {
if (!this.store) throw new Error('Store not initialized');
if (namespace) {
delete this.data[namespace];
} else {
this.data = {};
}
await this.store.set('data', this.data);
await this.store.save();
}
async listNamespaces(): Promise<string[]> {
return Object.keys(this.data);
}
async listKeys(namespace: string): Promise<string[]> {
return Object.keys(this.data[namespace] || {});
}
async watch(namespace: string, key: string): Promise<StoreEventEmitter<StoreEvents>> {
const watchKey = `${namespace}:${key}`;
return {
on: <K extends keyof StoreEvents>(
event: K,
handler: (payload: StoreEvents[K]) => void
) => {
if (event !== 'change') return () => {};
if (!this.listeners.has(watchKey)) {
this.listeners.set(watchKey, new Set());
}
this.listeners.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>(
event: K,
handler: (payload: StoreEvents[K]) => void
) => {
if (event !== 'change') return () => {};
const wrapper = (value: StoreEvents['change']) => {
handler(value as StoreEvents[K]);
this.listeners.get(watchKey)?.delete(wrapper);
};
if (!this.listeners.has(watchKey)) {
this.listeners.set(watchKey, new Set());
}
this.listeners.get(watchKey)!.add(wrapper);
return () => this.listeners.get(watchKey)?.delete(wrapper);
},
off: <K extends keyof StoreEvents>(
event: K,
handler: (payload: StoreEvents[K]) => void
) => {
if (event === 'change') {
this.listeners.get(watchKey)?.delete(handler as (payload: StoreEvents['change']) => void);
}
},
};
}
async close(): Promise<void> {
if (this.store) {
await this.store.close();
this.store = null;
this.data = {};
this.listeners.clear();
TauriStoreManager.instances.delete(this.storePath);
}
} }
}
} }
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>> {
try {
const manager = TauriStoreManager.new(storePath);
const existingData = await manager.getRaw(namespace, key);
const createdAt = existingData?.metadata.createdAt || new Date().toISOString()
const updatedAt = new Date().toISOString()
const storedData: StoredData = {
schemaVersion: 1,
metadata: {
createdAt,
updatedAt,
namespace,
encrypted: options?.encrypt,
compressed: options?.compress,
ttl: options?.ttl,
},
data: value,
};
await manager.set(namespace, key, storedData);
return E.right(undefined);
} catch (error) {
return E.left({
kind: 'storage',
message: error instanceof Error ? error.message : 'Unknown error',
cause: error,
});
}
},
async get<T>(storePath: string, namespace: string, key: string): Promise<E.Either<StoreError, T | undefined>> {
try {
const manager = TauriStoreManager.new(storePath);
return E.right(await manager.get<T>(namespace, key));
} catch (error) {
return E.left({
kind: 'storage',
message: error instanceof Error ? error.message : 'Unknown error',
cause: error,
});
}
},
async has(storePath: string, namespace: string, key: string): Promise<E.Either<StoreError, boolean>> {
try {
const manager = TauriStoreManager.new(storePath);
return E.right(await manager.has(namespace, key));
} catch (error) {
return E.left({
kind: 'storage',
message: error instanceof Error ? error.message : 'Unknown error',
cause: error,
});
}
},
async remove(storePath: string, namespace: string, key: string): Promise<E.Either<StoreError, boolean>> {
try {
const manager = TauriStoreManager.new(storePath);
return E.right(await manager.delete(namespace, key));
} catch (error) {
return E.left({
kind: 'storage',
message: error instanceof Error ? error.message : 'Unknown error',
cause: error,
});
}
},
async clear(storePath: string, namespace?: string): Promise<E.Either<StoreError, void>> {
try {
const manager = TauriStoreManager.new(storePath);
await manager.clear(namespace);
return E.right(undefined);
} catch (error) {
return E.left({
kind: 'storage',
message: error instanceof Error ? error.message : 'Unknown error',
cause: error,
});
}
},
async listNamespaces(storePath: string): Promise<E.Either<StoreError, string[]>> {
try {
const manager = TauriStoreManager.new(storePath);
return E.right(await manager.listNamespaces());
} catch (error) {
return E.left({
kind: 'storage',
message: error instanceof Error ? error.message : 'Unknown error',
cause: error,
});
}
},
async listKeys(storePath: string, namespace: string): Promise<E.Either<StoreError, string[]>> {
try {
const manager = TauriStoreManager.new(storePath);
return E.right(await manager.listKeys(namespace));
} catch (error) {
return E.left({
kind: 'storage',
message: error instanceof Error ? error.message : 'Unknown error',
cause: error,
});
}
},
async watch(storePath: string, namespace: string, key: string): Promise<StoreEventEmitter<StoreEvents>> {
const manager = TauriStoreManager.new(storePath);
return manager.watch(namespace, key);
},
}, },
};
async set(
storePath: string,
namespace: string,
key: string,
value: unknown,
options?: StorageOptions
): Promise<E.Either<StoreError, void>> {
try {
const manager = TauriStoreManager.new(storePath)
const existingData = await manager.getRaw(namespace, key)
const createdAt =
existingData?.metadata.createdAt || new Date().toISOString()
const updatedAt = new Date().toISOString()
const storedData: StoredData = {
schemaVersion: 1,
metadata: {
createdAt,
updatedAt,
namespace,
encrypted: options?.encrypt,
compressed: options?.compress,
ttl: options?.ttl,
},
data: value,
}
await manager.set(namespace, key, storedData)
return E.right(undefined)
} catch (error) {
return E.left({
kind: "storage",
message: error instanceof Error ? error.message : "Unknown error",
cause: error,
})
}
},
async get<T>(
storePath: string,
namespace: string,
key: string
): Promise<E.Either<StoreError, T | undefined>> {
try {
const manager = TauriStoreManager.new(storePath)
return E.right(await manager.get<T>(namespace, key))
} catch (error) {
return E.left({
kind: "storage",
message: error instanceof Error ? error.message : "Unknown error",
cause: error,
})
}
},
async has(
storePath: string,
namespace: string,
key: string
): Promise<E.Either<StoreError, boolean>> {
try {
const manager = TauriStoreManager.new(storePath)
return E.right(await manager.has(namespace, key))
} catch (error) {
return E.left({
kind: "storage",
message: error instanceof Error ? error.message : "Unknown error",
cause: error,
})
}
},
async remove(
storePath: string,
namespace: string,
key: string
): Promise<E.Either<StoreError, boolean>> {
try {
const manager = TauriStoreManager.new(storePath)
return E.right(await manager.delete(namespace, key))
} catch (error) {
return E.left({
kind: "storage",
message: error instanceof Error ? error.message : "Unknown error",
cause: error,
})
}
},
async clear(
storePath: string,
namespace?: string
): Promise<E.Either<StoreError, void>> {
try {
const manager = TauriStoreManager.new(storePath)
await manager.clear(namespace)
return E.right(undefined)
} catch (error) {
return E.left({
kind: "storage",
message: error instanceof Error ? error.message : "Unknown error",
cause: error,
})
}
},
async listNamespaces(
storePath: string
): Promise<E.Either<StoreError, string[]>> {
try {
const manager = TauriStoreManager.new(storePath)
return E.right(await manager.listNamespaces())
} catch (error) {
return E.left({
kind: "storage",
message: error instanceof Error ? error.message : "Unknown error",
cause: error,
})
}
},
async listKeys(
storePath: string,
namespace: string
): Promise<E.Either<StoreError, string[]>> {
try {
const manager = TauriStoreManager.new(storePath)
return E.right(await manager.listKeys(namespace))
} catch (error) {
return E.left({
kind: "storage",
message: error instanceof Error ? error.message : "Unknown error",
cause: error,
})
}
},
async watch(
storePath: string,
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,262 +1,284 @@
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
}
private getFullKey(namespace: string, key: string): string {
return `${namespace}:${key}`
}
private notifyListeners(namespace: string, key: string, value?: unknown) {
const fullKey = this.getFullKey(namespace, key)
const listeners = this.listeners.get(fullKey) || new Set()
listeners.forEach((listener) => listener({ namespace, key, value }))
}
async set(namespace: string, key: string, value: StoredData): Promise<void> {
const validated = StoredDataSchema.parse(value)
localStorage.setItem(
this.getFullKey(namespace, key),
superjson.stringify(validated)
)
this.notifyListeners(namespace, key, validated.data)
}
async getRaw(
namespace: string,
key: string
): Promise<StoredData | undefined> {
const rawValue = localStorage.getItem(this.getFullKey(namespace, key))
if (!rawValue) return undefined
const parsed = superjson.parse(rawValue)
const validated = StoredDataSchema.parse(parsed)
return validated
}
async get<T>(namespace: string, key: string): Promise<T | undefined> {
const storedData = await this.getRaw(namespace, key)
return storedData?.data as T
}
async has(namespace: string, key: string): Promise<boolean> {
return localStorage.getItem(this.getFullKey(namespace, key)) !== null
}
async delete(namespace: string, key: string): Promise<boolean> {
const exists = await this.has(namespace, key)
if (exists) {
localStorage.removeItem(this.getFullKey(namespace, key))
this.notifyListeners(namespace, key, undefined)
}
return exists
}
async clear(namespace?: string): Promise<void> {
if (namespace) {
const keysToRemove = Object.keys(localStorage).filter((key) =>
key.startsWith(`${namespace}:`)
)
keysToRemove.forEach((key) => localStorage.removeItem(key))
} else {
localStorage.clear()
}
this.listeners.clear()
}
async listNamespaces(): Promise<string[]> {
const namespaces = new Set<string>()
Object.keys(localStorage).forEach((key) => {
const [namespace] = key.split(":")
namespaces.add(namespace)
})
return Array.from(namespaces)
}
async listKeys(namespace: string): Promise<string[]> {
return Object.keys(localStorage)
.filter((key) => key.startsWith(`${namespace}:`))
.map((key) => key.replace(`${namespace}:`, ""))
}
async watch(
namespace: string,
key: string
): Promise<StoreEventEmitter<StoreEvents>> {
const fullKey = this.getFullKey(namespace, key)
return {
on: (event, handler) => {
if (event !== "change") return () => {}
if (!this.listeners.has(fullKey)) {
this.listeners.set(fullKey, new Set())
} }
return BrowserStoreManager.instance; this.listeners
} .get(fullKey)!
.add(handler as (payload: StoreEvents["change"]) => void)
private getFullKey(namespace: string, key: string): string { return () =>
return `${namespace}:${key}`; this.listeners
} .get(fullKey)
?.delete(handler as (payload: StoreEvents["change"]) => void)
private notifyListeners(namespace: string, key: string, value?: unknown) { },
const fullKey = this.getFullKey(namespace, key); once: (event, handler) => {
const listeners = this.listeners.get(fullKey) || new Set(); if (event !== "change") return () => {}
listeners.forEach(listener => listener({ namespace, key, value })); const wrapper = (payload: StoreEvents["change"]) => {
} handler(payload)
this.listeners.get(fullKey)?.delete(wrapper)
async set(namespace: string, key: string, value: StoredData): Promise<void> {
const validated = StoredDataSchema.parse(value);
localStorage.setItem(this.getFullKey(namespace, key), superjson.stringify(validated));
this.notifyListeners(namespace, key, validated.data);
}
async getRaw(namespace: string, key: string): Promise<StoredData | undefined> {
const rawValue = localStorage.getItem(this.getFullKey(namespace, key));
if (!rawValue) return undefined;
const parsed = superjson.parse(rawValue);
const validated = StoredDataSchema.parse(parsed);
return validated;
}
async get<T>(namespace: string, key: string): Promise<T | undefined> {
const storedData = await this.getRaw(namespace, key);
return storedData?.data as T;
}
async has(namespace: string, key: string): Promise<boolean> {
return localStorage.getItem(this.getFullKey(namespace, key)) !== null;
}
async delete(namespace: string, key: string): Promise<boolean> {
const exists = await this.has(namespace, key);
if (exists) {
localStorage.removeItem(this.getFullKey(namespace, key));
this.notifyListeners(namespace, key, undefined);
} }
return exists; if (!this.listeners.has(fullKey)) {
} this.listeners.set(fullKey, new Set())
async clear(namespace?: string): Promise<void> {
if (namespace) {
const keysToRemove = Object.keys(localStorage).filter(key => key.startsWith(`${namespace}:`));
keysToRemove.forEach(key => localStorage.removeItem(key));
} else {
localStorage.clear();
} }
this.listeners.clear(); this.listeners.get(fullKey)!.add(wrapper)
} return () => this.listeners.get(fullKey)?.delete(wrapper)
},
async listNamespaces(): Promise<string[]> { off: (event, handler) => {
const namespaces = new Set<string>(); if (event === "change") {
Object.keys(localStorage).forEach(key => { this.listeners
const [namespace] = key.split(':'); .get(fullKey)
namespaces.add(namespace); ?.delete(handler as (payload: StoreEvents["change"]) => void)
}); }
return Array.from(namespaces); },
}
async listKeys(namespace: string): Promise<string[]> {
return Object.keys(localStorage)
.filter(key => key.startsWith(`${namespace}:`))
.map(key => key.replace(`${namespace}:`, ''));
}
async watch(namespace: string, key: string): Promise<StoreEventEmitter<StoreEvents>> {
const fullKey = this.getFullKey(namespace, key);
return {
on: (event, handler) => {
if (event !== 'change') return () => {};
if (!this.listeners.has(fullKey)) {
this.listeners.set(fullKey, new Set());
}
this.listeners.get(fullKey)!.add(handler as (payload: StoreEvents['change']) => void);
return () => this.listeners.get(fullKey)?.delete(handler as (payload: StoreEvents['change']) => void);
},
once: (event, handler) => {
if (event !== 'change') return () => {};
const wrapper = (payload: StoreEvents['change']) => {
handler(payload);
this.listeners.get(fullKey)?.delete(wrapper);
};
if (!this.listeners.has(fullKey)) {
this.listeners.set(fullKey, new Set());
}
this.listeners.get(fullKey)!.add(wrapper);
return () => this.listeners.get(fullKey)?.delete(wrapper);
},
off: (event, handler) => {
if (event === 'change') {
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) {
try {
const manager = BrowserStoreManager.new();
const existingData = await manager.getRaw(namespace, key);
const createdAt = existingData?.metadata.createdAt || new Date().toISOString()
const updatedAt = new Date().toISOString()
const storedData: StoredData = {
schemaVersion: 1,
metadata: {
createdAt,
updatedAt,
namespace,
encrypted: options?.encrypt,
compressed: options?.compress,
ttl: options?.ttl,
},
data: value,
};
await manager.set(namespace, key, storedData);
return E.right(undefined);
} catch (e) {
return E.left({
kind: 'storage',
message: e instanceof Error ? e.message : 'Unknown error',
cause: e,
});
}
},
async get(_storePath, namespace, key) {
try {
const manager = BrowserStoreManager.new();
return E.right(await manager.get(namespace, key));
} catch (e) {
return E.left({
kind: 'storage',
message: e instanceof Error ? e.message : 'Unknown error',
cause: e,
});
}
},
async has(_storePath, namespace, key) {
try {
const manager = BrowserStoreManager.new();
return E.right(await manager.has(namespace, key));
} catch (e) {
return E.left({
kind: 'storage',
message: e instanceof Error ? e.message : 'Unknown error',
cause: e,
});
}
},
async remove(_storePath, namespace, key) {
try {
const manager = BrowserStoreManager.new();
return E.right(await manager.delete(namespace, key));
} catch (e) {
return E.left({
kind: 'storage',
message: e instanceof Error ? e.message : 'Unknown error',
cause: e,
});
}
},
async clear(_storePath, namespace) {
try {
const manager = BrowserStoreManager.new();
await manager.clear(namespace);
return E.right(undefined);
} catch (e) {
return E.left({
kind: 'storage',
message: e instanceof Error ? e.message : 'Unknown error',
cause: e,
});
}
},
async listNamespaces(_storePath){
try {
const manager = BrowserStoreManager.new();
return E.right(await manager.listNamespaces());
} catch (e) {
return E.left({
kind: 'storage',
message: e instanceof Error ? e.message : 'Unknown error',
cause: e,
});
}
},
async listKeys(_storePath, namespace) {
try {
const manager = BrowserStoreManager.new();
return E.right(await manager.listKeys(namespace));
} catch (e) {
return E.left({
kind: 'storage',
message: e instanceof Error ? e.message : 'Unknown error',
cause: e,
});
}
},
async watch(_storePath, namespace, key) {
const manager = BrowserStoreManager.new();
return manager.watch(namespace, key);
},
}, },
};
async set(_storePath, namespace, key, value, options) {
try {
const manager = BrowserStoreManager.new()
const existingData = await manager.getRaw(namespace, key)
const createdAt =
existingData?.metadata.createdAt || new Date().toISOString()
const updatedAt = new Date().toISOString()
const storedData: StoredData = {
schemaVersion: 1,
metadata: {
createdAt,
updatedAt,
namespace,
encrypted: options?.encrypt,
compressed: options?.compress,
ttl: options?.ttl,
},
data: value,
}
await manager.set(namespace, key, storedData)
return E.right(undefined)
} catch (e) {
return E.left({
kind: "storage",
message: e instanceof Error ? e.message : "Unknown error",
cause: e,
})
}
},
async get(_storePath, namespace, key) {
try {
const manager = BrowserStoreManager.new()
return E.right(await manager.get(namespace, key))
} catch (e) {
return E.left({
kind: "storage",
message: e instanceof Error ? e.message : "Unknown error",
cause: e,
})
}
},
async has(_storePath, namespace, key) {
try {
const manager = BrowserStoreManager.new()
return E.right(await manager.has(namespace, key))
} catch (e) {
return E.left({
kind: "storage",
message: e instanceof Error ? e.message : "Unknown error",
cause: e,
})
}
},
async remove(_storePath, namespace, key) {
try {
const manager = BrowserStoreManager.new()
return E.right(await manager.delete(namespace, key))
} catch (e) {
return E.left({
kind: "storage",
message: e instanceof Error ? e.message : "Unknown error",
cause: e,
})
}
},
async clear(_storePath, namespace) {
try {
const manager = BrowserStoreManager.new()
await manager.clear(namespace)
return E.right(undefined)
} catch (e) {
return E.left({
kind: "storage",
message: e instanceof Error ? e.message : "Unknown error",
cause: e,
})
}
},
async listNamespaces(_storePath) {
try {
const manager = BrowserStoreManager.new()
return E.right(await manager.listNamespaces())
} catch (e) {
return E.left({
kind: "storage",
message: e instanceof Error ? e.message : "Unknown error",
cause: e,
})
}
},
async listKeys(_storePath, namespace) {
try {
const manager = BrowserStoreManager.new()
return E.right(await manager.listKeys(namespace))
} catch (e) {
return E.left({
kind: "storage",
message: e instanceof Error ? e.message : "Unknown error",
cause: e,
})
}
},
async watch(_storePath, namespace, key) {
const manager = BrowserStoreManager.new()
return manager.watch(namespace, key)
},
},
}

View file

@ -1,11 +1,9 @@
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,
} as const } as const
export const latest = v1 export const latest = 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(
name: z.string(), z.object({
version: z.number(), name: z.string(),
createdAt: z.string().datetime(), version: z.number(),
updatedAt: z.string().datetime(), createdAt: z.string().datetime(),
keys: z.array(z.string()) updatedAt: z.string().datetime(),
})) 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